From 15315884533eccb63b3e2ec3df7fc906d997d9ee Mon Sep 17 00:00:00 2001 From: chengjunzhang61 <chengjun@finerpoint.com> Date: Wed, 8 Dec 2021 10:00:33 -0500 Subject: [PATCH] Update recycler plugin --- .../markwon/recycler/table/TableEntry.java | 3 +- markwon-recycler/build.gradle | 2 + .../recycler/CustomMovementMethod.java | 32 ++++ .../markwon/recycler/MarkwonAdapter.java | 42 +++-- .../markwon/recycler/MarkwonAdapterImpl.java | 66 +++++--- .../noties/markwon/recycler/SimpleEntry.java | 31 ++-- .../markwon/recycler/SimpleEntryWebView.java | 150 ++++++++++++++++++ 7 files changed, 279 insertions(+), 47 deletions(-) create mode 100644 markwon-recycler/src/main/java/io/noties/markwon/recycler/CustomMovementMethod.java create mode 100644 markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntryWebView.java diff --git a/markwon-recycler-table/src/main/java/io/noties/markwon/recycler/table/TableEntry.java b/markwon-recycler-table/src/main/java/io/noties/markwon/recycler/table/TableEntry.java index 2784221f..c52d22fa 100644 --- a/markwon-recycler-table/src/main/java/io/noties/markwon/recycler/table/TableEntry.java +++ b/markwon-recycler-table/src/main/java/io/noties/markwon/recycler/table/TableEntry.java @@ -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); diff --git a/markwon-recycler/build.gradle b/markwon-recycler/build.gradle index 63c7742e..9edb468c 100644 --- a/markwon-recycler/build.gradle +++ b/markwon-recycler/build.gradle @@ -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'] diff --git a/markwon-recycler/src/main/java/io/noties/markwon/recycler/CustomMovementMethod.java b/markwon-recycler/src/main/java/io/noties/markwon/recycler/CustomMovementMethod.java new file mode 100644 index 00000000..2487456a --- /dev/null +++ b/markwon-recycler/src/main/java/io/noties/markwon/recycler/CustomMovementMethod.java @@ -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()); + } + } +} + diff --git a/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapter.java b/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapter.java index 265c9273..edb8c2f4 100644 --- a/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapter.java +++ b/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapter.java @@ -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 { diff --git a/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapterImpl.java b/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapterImpl.java index d773edcc..d16bb425 100644 --- a/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapterImpl.java +++ b/markwon-recycler/src/main/java/io/noties/markwon/recycler/MarkwonAdapterImpl.java @@ -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 - - defaultEntry.clear(); - - for (int i = 0, size = entries.size(); i < size; i++) { - entries.valueAt(i).clear(); + 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()); } - - this.markwon = markwon; - this.nodes = nodes; } @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; @@ -178,4 +204,4 @@ class MarkwonAdapterImpl extends MarkwonAdapter { return new MarkwonAdapterImpl(entries, defaultEntry, reducer); } } -} +} \ No newline at end of file diff --git a/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntry.java b/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntry.java index b55be1fb..a150917e 100644 --- a/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntry.java +++ b/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntry.java @@ -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,23 +78,26 @@ 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()); } } -} +} \ No newline at end of file diff --git a/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntryWebView.java b/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntryWebView.java new file mode 100644 index 00000000..e3258eb7 --- /dev/null +++ b/markwon-recycler/src/main/java/io/noties/markwon/recycler/SimpleEntryWebView.java @@ -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; + } + } +}