Update recycler plugin

This commit is contained in:
chengjunzhang61 2021-12-08 10:00:33 -05:00
parent 50d331bc87
commit 1531588453
7 changed files with 279 additions and 47 deletions

View File

@ -138,8 +138,7 @@ public class TableEntry extends MarkwonAdapter.Entry<TableBlock, TableEntry.Hold
}
@Override
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull TableBlock node) {
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull TableBlock node, int depth) {
Table table = map.get(node);
if (table == null) {
table = Table.parse(markwon, node);

View File

@ -16,6 +16,8 @@ android {
dependencies {
api project(':markwon-core')
implementation project(path: ':markwon-round-textview')
implementation project(path: ':markwon-iframe-ext')
deps.with {
api it['x-recycler-view']

View File

@ -0,0 +1,32 @@
package io.noties.markwon.recycler;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
public class CustomMovementMethod extends LinkMovementMethod {
@Override
public boolean canSelectArbitrarily () {
return true;
}
@Override
public void initialize(TextView widget, Spannable text) {
Selection.setSelection(text, text.length());
}
@Override
public void onTakeFocus(TextView view, Spannable text, int dir) {
if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
if (view.getLayout() == null) {
// This shouldn't be null, but do something sensible if it is.
Selection.setSelection(text, text.length());
}
} else {
Selection.setSelection(text, text.length());
}
}
}

View File

@ -1,5 +1,6 @@
package io.noties.markwon.recycler;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -23,9 +24,9 @@ import io.noties.markwon.MarkwonReducer;
* ability to customize rendering of blocks. For example display certain blocks in a horizontal
* scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}).
*
* @see #builder(int, int)
* @see #builder(int, int, String, int, String)
* @see #builder(Entry)
* @see #create(int, int)
* @see #create(int, int, String, String)
* @see #create(Entry)
* @see #setMarkdown(Markwon, String)
* @see #setParsedMarkdown(Markwon, Node)
@ -34,22 +35,31 @@ import io.noties.markwon.MarkwonReducer;
*/
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {
@NonNull
public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId));
}
/**
* Factory method to obtain {@link Builder} instance.
*
* @see Builder
*/
@NonNull
public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId));
}
private static String ENTRY_TYPE_IFRAME = "IFRAME";
private static String ENTRY_TYPE_TEXT = "TEXT";
@NonNull
public static Builder builder(
@LayoutRes int defaultEntryLayoutResId,
@IdRes int defaultEntryTextViewResId
@IdRes int defaultEntryTextViewResId,
@NonNull String type,
@NonNull int textColor,
@NonNull String theme
) {
return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId));
if (type.equalsIgnoreCase(ENTRY_TYPE_IFRAME)) {
return builder(SimpleEntryWebView.create(defaultEntryLayoutResId, defaultEntryTextViewResId));
} else {
return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId, textColor, theme));
}
}
@NonNull
@ -70,15 +80,17 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
* be specified explicitly.
*
* @see #create(Entry)
* @see #builder(int, int)
* @see #builder(int, int, String, int, String)
* @see SimpleEntry
*/
@NonNull
public static MarkwonAdapter create(
@LayoutRes int defaultEntryLayoutResId,
@IdRes int defaultEntryTextViewResId
@IdRes int defaultEntryTextViewResId,
@NonNull String type,
@NonNull String theme
) {
return builder(defaultEntryLayoutResId, defaultEntryTextViewResId).build();
return builder(defaultEntryLayoutResId, defaultEntryTextViewResId, type, Color.BLACK, theme).build();
}
/**
@ -144,7 +156,7 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
@NonNull
public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);
public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node, int depth);
/**
* Will be called when new content is available (clear internal cache if any)
@ -164,11 +176,13 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown);
public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown, int depth);
public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document);
public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List<Node> nodes);
public abstract int getNodeViewType(@NonNull Class<? extends Node> node);
public abstract int getNodeViewType(Node node);
@SuppressWarnings("WeakerAccess")
public static class Holder extends RecyclerView.ViewHolder {

View File

@ -1,5 +1,6 @@
package io.noties.markwon.recycler;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@ -13,6 +14,7 @@ import java.util.List;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonReducer;
import io.noties.markwon.iframe.ext.IFrameNode;
class MarkwonAdapterImpl extends MarkwonAdapter {
@ -24,6 +26,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
private Markwon markwon;
private List<Node> nodes;
private int depth = 0;
@SuppressWarnings("WeakerAccess")
MarkwonAdapterImpl(
@ -42,6 +45,12 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
setParsedMarkdown(markwon, markwon.parse(markdown));
}
@Override
public void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown, int depth) {
this.depth = depth;
setParsedMarkdown(markwon, markwon.parse(markdown));
}
@Override
public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document) {
setParsedMarkdown(markwon, reducer.reduce(document));
@ -50,15 +59,16 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
@Override
public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List<Node> nodes) {
// clear all entries before applying
try {
defaultEntry.clear();
for (int i = 0, size = entries.size(); i < size; i++) {
entries.valueAt(i).clear();
}
this.markwon = markwon;
this.nodes = nodes;
} catch (Exception e) {
Log.e("Markdown issue", nodes.toString());
}
}
@NonNull
@ -68,21 +78,22 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
if (layoutInflater == null) {
layoutInflater = LayoutInflater.from(parent.getContext());
}
final Entry<Node, Holder> entry = getEntry(viewType);
return entry.createHolder(layoutInflater, parent);
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
final Node node = nodes.get(position);
final int viewType = getNodeViewType(node.getClass());
int viewType = getNodeViewType(node);
final Entry<Node, Holder> entry = getEntry(viewType);
entry.bindHolder(markwon, holder, node);
if (isIFrameTypeType(node)) {
if(node.getFirstChild() != null) {
entry.bindHolder(markwon, holder, node.getFirstChild().getFirstChild(), this.depth);
}
} else {
entry.bindHolder(markwon, holder, node, this.depth);
}
}
@Override
@ -110,22 +121,37 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
@Override
public int getItemViewType(int position) {
return getNodeViewType(nodes.get(position).getClass());
final Node node = nodes.get(position);
return getNodeViewType(node);
}
@Override
public long getItemId(int position) {
final Node node = nodes.get(position);
final int type = getNodeViewType(node.getClass());
int type = getNodeViewType(node);
final Entry<Node, Holder> entry = getEntry(type);
return entry.id(node);
}
private boolean isIFrameTypeType(Node node) {
if (node.getFirstChild() != null) {
if (node.getFirstChild().toString().contains("IFrameGroupNode") &&
node.getFirstChild().toString().contains("IFrameGroupNode")) {
return true;
}
}
return false;
}
@Override
public int getNodeViewType(@NonNull Class<? extends Node> node) {
public int getNodeViewType(@NonNull Node node) {
// if has registered -> then return it, else 0
final int hash = node.hashCode();
if (isIFrameTypeType(node)) {
return IFrameNode.class.hashCode();
}
final int hash = node.getClass().hashCode();
if (entries.indexOfKey(hash) > -1) {
Log.d("NodeType1", String.valueOf(hash));
return hash;
}
return 0;

View File

@ -1,5 +1,6 @@
package io.noties.markwon.recycler;
import android.graphics.Color;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
@ -15,6 +16,7 @@ import org.commonmark.node.Node;
import java.util.HashMap;
import java.util.Map;
import io.noties.markdown.boundarytext.RoundedBgTextView;
import io.noties.markwon.Markwon;
import io.noties.markwon.utils.NoCopySpannableFactory;
@ -30,12 +32,12 @@ public class SimpleEntry extends MarkwonAdapter.Entry<Node, SimpleEntry.Holder>
*/
@NonNull
public static SimpleEntry createTextViewIsRoot(@LayoutRes int layoutResId) {
return new SimpleEntry(layoutResId, 0);
return new SimpleEntry(layoutResId, 0, Color.BLACK, "light");
}
@NonNull
public static SimpleEntry create(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
return new SimpleEntry(layoutResId, textViewIdRes);
public static SimpleEntry create(@LayoutRes int layoutResId, @IdRes int textViewIdRes, @NonNull int textColor, @NonNull String theme) {
return new SimpleEntry(layoutResId, textViewIdRes, textColor, theme);
}
// small cache for already rendered nodes
@ -43,20 +45,24 @@ public class SimpleEntry extends MarkwonAdapter.Entry<Node, SimpleEntry.Holder>
private final int layoutResId;
private final int textViewIdRes;
private final int textColor;
private String theme = "light";
public SimpleEntry(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
public SimpleEntry(@LayoutRes int layoutResId, @IdRes int textViewIdRes, @NonNull int textColor, String theme) {
this.layoutResId = layoutResId;
this.textViewIdRes = textViewIdRes;
this.textColor = textColor;
this.theme = theme;
}
@NonNull
@Override
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
return new Holder(textViewIdRes, inflater.inflate(layoutResId, parent, false));
return new Holder(textViewIdRes, inflater.inflate(layoutResId, parent, false), textColor, theme);
}
@Override
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull Node node) {
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull Node node, int depth) {
Spanned spanned = cache.get(node);
if (spanned == null) {
spanned = markwon.render(node);
@ -72,22 +78,25 @@ public class SimpleEntry extends MarkwonAdapter.Entry<Node, SimpleEntry.Holder>
public static class Holder extends MarkwonAdapter.Holder {
final TextView textView;
final RoundedBgTextView textView;
protected Holder(@IdRes int textViewIdRes, @NonNull View itemView) {
protected Holder(@IdRes int textViewIdRes, @NonNull View itemView, @NonNull int textColor, String theme) {
super(itemView);
final TextView textView;
final RoundedBgTextView textView;
if (textViewIdRes == 0) {
if (!(itemView instanceof TextView)) {
throw new IllegalStateException("TextView is not root of layout " +
"(specify TextView ID explicitly): " + itemView);
}
textView = (TextView) itemView;
textView = (RoundedBgTextView) itemView;
} else {
textView = requireView(textViewIdRes);
}
textView.setThemeChange(theme);
textView.setTextColor(textColor);
this.textView = textView;
this.textView.setMovementMethod(new CustomMovementMethod());
this.textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
}
}

View File

@ -0,0 +1,150 @@
package io.noties.markwon.recycler;
import static io.noties.markwon.iframe.ext.IFrameUtils.getDesmosId;
import static io.noties.markwon.iframe.ext.IFrameUtils.getVimeoVideoId;
import static io.noties.markwon.iframe.ext.IFrameUtils.getYoutubeVideoId;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import java.util.HashMap;
import java.util.Map;
import io.noties.markwon.Markwon;
import io.noties.markwon.iframe.ext.IFrameNode;
/**
* @since 3.0.0
*/
@SuppressWarnings("WeakerAccess")
public class SimpleEntryWebView extends MarkwonAdapter.Entry<IFrameNode, SimpleEntryWebView.Holder> {
@NonNull
public static SimpleEntryWebView create(@LayoutRes int layoutResId, @IdRes int webViewIdRes) {
return new SimpleEntryWebView(layoutResId, webViewIdRes);
}
// small cache for already rendered nodes
private final Map<Node, Spanned> cache = new HashMap<>();
private final int layoutResId;
private final int webViewIdRes;
public SimpleEntryWebView(@LayoutRes int layoutResId, @IdRes int webViewIdRes) {
this.layoutResId = layoutResId;
this.webViewIdRes = webViewIdRes;
}
@NonNull
@Override
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
return new Holder(webViewIdRes, inflater.inflate(layoutResId, parent, false));
}
@Override
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull IFrameNode node, int depth) {
Spanned spanned = cache.get(node);
if (spanned == null) {
spanned = markwon.render(node);
cache.put(node, spanned);
}
renderWebView(holder, node, depth);
}
private void renderWebView(Holder holder, IFrameNode node, int depth) {
String videoLink = "";
float deviceWidth = 1080;
float marginH = 10;
int videoWidth = (int)(deviceWidth - marginH * 2 - depth * marginH);
int videoHeight = (int)(videoWidth * 9 / 16);
if (node.link().contains("youtu")) {
String youtubeVideoId = getYoutubeVideoId(node.link());
if (!TextUtils.isEmpty(youtubeVideoId)) {
videoLink = "https://www.youtube.com/embed/" + youtubeVideoId;
}
} else if (node.link().contains("vimeo")) {
String vimeoId = getVimeoVideoId(node.link());
if (!TextUtils.isEmpty(vimeoId)) {
videoLink = "https://player.vimeo.com/video/" + vimeoId;
videoHeight = (int)(videoWidth * 3 / 4);
}
} else {
String desmosId = getDesmosId(node.link());
if (!TextUtils.isEmpty(desmosId)) {
videoLink = "https://www.desmos.com/calculator/" + desmosId + "?embed";
}
}
WebSettings webSettings = holder.webView.getSettings();
webSettings.setJavaScriptEnabled(true);
ViewGroup.LayoutParams layoutParams = (ViewGroup.LayoutParams) holder.webView.getLayoutParams();
layoutParams.height = (int) videoHeight;
holder.webView.setLayoutParams(layoutParams);
holder.webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
});
holder.webView.setWebChromeClient(new WebChromeClient());
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
webSettings.setLoadWithOverviewMode(true);
webSettings.setUseWideViewPort(true);
webSettings.setTextZoom(100);
holder.webView.setVerticalScrollBarEnabled(false);
holder.webView.setHorizontalScrollBarEnabled(false);
holder.webView.setInitialScale(100);
if (!videoLink.isEmpty()) {
String dataURL = "<iframe id=\"player\" type=\"text/html\" width=\"" + videoWidth +"\" height=\"" + videoHeight + "\" " +
"src=\"" + videoLink + "\"frameborder=\"0\" allowfullscreen webkitallowfullscreen/>";
holder.webView.loadData(dataURL, "text/html", "utf-8");
}
holder.webView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return (event.getAction() == MotionEvent.ACTION_MOVE);
}
});
}
@Override
public void clear() {
cache.clear();
}
public static class Holder extends MarkwonAdapter.Holder {
final WebView webView;
protected Holder(@IdRes int webViewIdRes, @NonNull View itemView) {
super(itemView);
final WebView webView;
if (webViewIdRes == 0) {
if (!(itemView instanceof WebView)) {
throw new IllegalStateException("WebView is not root of layout " +
"(specify TextView ID explicitly): " + itemView);
}
webView = (WebView) itemView;
} else {
webView = requireView(webViewIdRes);
}
this.webView = webView;
}
}
}