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 @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); Table table = map.get(node);
if (table == null) { if (table == null) {
table = Table.parse(markwon, node); table = Table.parse(markwon, node);

View File

@ -16,6 +16,8 @@ android {
dependencies { dependencies {
api project(':markwon-core') api project(':markwon-core')
implementation project(path: ':markwon-round-textview')
implementation project(path: ':markwon-iframe-ext')
deps.with { deps.with {
api it['x-recycler-view'] 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; package io.noties.markwon.recycler;
import android.graphics.Color;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; 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 * 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)}). * 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 #builder(Entry)
* @see #create(int, int) * @see #create(int, int, String, String)
* @see #create(Entry) * @see #create(Entry)
* @see #setMarkdown(Markwon, String) * @see #setMarkdown(Markwon, String)
* @see #setParsedMarkdown(Markwon, Node) * @see #setParsedMarkdown(Markwon, Node)
@ -34,22 +35,31 @@ import io.noties.markwon.MarkwonReducer;
*/ */
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> { 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. * Factory method to obtain {@link Builder} instance.
* *
* @see Builder * @see Builder
*/ */
@NonNull @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( public static Builder builder(
@LayoutRes int defaultEntryLayoutResId, @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 @NonNull
@ -70,15 +80,17 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
* be specified explicitly. * be specified explicitly.
* *
* @see #create(Entry) * @see #create(Entry)
* @see #builder(int, int) * @see #builder(int, int, String, int, String)
* @see SimpleEntry * @see SimpleEntry
*/ */
@NonNull @NonNull
public static MarkwonAdapter create( public static MarkwonAdapter create(
@LayoutRes int defaultEntryLayoutResId, @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 @NonNull
public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); 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) * 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);
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 Node document);
public abstract void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List<Node> nodes); 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") @SuppressWarnings("WeakerAccess")
public static class Holder extends RecyclerView.ViewHolder { public static class Holder extends RecyclerView.ViewHolder {

View File

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

View File

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