From 95d792779e98b8a4180ed50f3d945865b406b127 Mon Sep 17 00:00:00 2001 From: Docile-Alligator Date: Sat, 2 Apr 2022 15:50:12 +0800 Subject: Render full markdown in CommentsRecyclerViewAdapter. --- .../adapters/CommentsRecyclerViewAdapter.java | 90 ++++++-- .../adapters/PostDetailRecyclerViewAdapter.java | 2 +- .../customviews/CustomMarkwonAdapter.java | 235 +++++++++++++++++++++ app/src/main/res/layout/adapter_default_entry.xml | 2 +- app/src/main/res/layout/item_comment.xml | 16 +- 5 files changed, 314 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomMarkwonAdapter.java (limited to 'app/src/main') diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java index 19fad657..6a0d3c53 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsRecyclerViewAdapter.java @@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.text.Spanned; import android.text.util.Linkify; import android.view.LayoutInflater; import android.view.View; @@ -33,6 +34,8 @@ import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.RequestOptions; import com.lsjwzh.widget.materialloadingprogressbar.CircleProgressBar; +import org.commonmark.ext.gfm.tables.TableBlock; + import java.util.ArrayList; import java.util.Locale; import java.util.concurrent.Executor; @@ -52,6 +55,8 @@ import io.noties.markwon.inlineparser.HtmlInlineProcessor; import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.movement.MovementMethodPlugin; +import io.noties.markwon.recycler.table.TableEntry; +import io.noties.markwon.recycler.table.TableEntryPlugin; import jp.wasabeef.glide.transformations.RoundedCornersTransformation; import me.saket.bettermovementmethod.BetterLinkMovementMethod; import ml.docilealligator.infinityforreddit.R; @@ -68,6 +73,9 @@ import ml.docilealligator.infinityforreddit.comment.Comment; import ml.docilealligator.infinityforreddit.comment.FetchComment; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customviews.CommentIndentationView; +import ml.docilealligator.infinityforreddit.customviews.CustomMarkwonAdapter; +import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed; +import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManager; import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView; import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment; import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin; @@ -101,6 +109,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter mVisibleComments; private Locale mLocale; private RequestManager mGlide; + private RecyclerView.RecycledViewPool recycledViewPool; private String mSingleCommentId; private boolean mIsSingleCommentThreadMode; private boolean mVoteButtonsOnTheRight; @@ -189,6 +198,15 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter { @@ -207,7 +225,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter { + .usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> { if (!activity.isDestroyed() && !activity.isFinishing()) { UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment(); Bundle bundle = new Bundle(); @@ -218,7 +236,9 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter builder + .tableLayout(R.layout.adapter_table_block, R.id.table_layout) + .textLayoutIsRoot(R.layout.view_table_entry_cell))) + .build(); + commentMarkdownView.setAdapter(mMarkwonAdapter); + itemView.setBackgroundColor(mCommentBackgroundColor); authorTextView.setTextColor(mUsernameColor); commentTimeTextView.setTextColor(mSecondaryTextColor); - commentMarkdownView.setTextColor(mCommentTextColor); authorFlairTextView.setTextColor(mAuthorFlairTextColor); topScoreTextView.setTextColor(mSecondaryTextColor); awardsTextView.setTextColor(mSecondaryTextColor); @@ -1572,23 +1610,34 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter hideToolbar(); itemView.setOnLongClickListener(hideToolbarOnLongClickListener); - commentMarkdownView.setOnLongClickListener(hideToolbarOnLongClickListener); commentTimeTextView.setOnLongClickListener(hideToolbarOnLongClickListener); + mMarkwonAdapter.setOnLongClickListener(v -> { + if (v instanceof TextView) { + if (((TextView) v).getSelectionStart() == -1 && ((TextView) v).getSelectionEnd() == -1) { + hideToolbar(); + } + } + return true; + }); } - commentMarkdownView.setOnClickListener(view -> { - if (commentMarkdownView.isSpoilerOnClick()) { - commentMarkdownView.setSpoilerOnClick(false); - return; + mMarkwonAdapter.setOnClickListener(v -> { + if (v instanceof SpoilerOnClickTextView) { + if (((SpoilerOnClickTextView) v).isSpoilerOnClick()) { + ((SpoilerOnClickTextView) v).setSpoilerOnClick(false); + return; + } } expandComments(); }); itemView.setOnClickListener(view -> expandComments()); } else { if (mCommentToolbarHideOnClick) { - commentMarkdownView.setOnClickListener(view -> { - if (commentMarkdownView.isSpoilerOnClick()) { - commentMarkdownView.setSpoilerOnClick(false); - return; + mMarkwonAdapter.setOnClickListener(view -> { + if (view instanceof SpoilerOnClickTextView) { + if (((SpoilerOnClickTextView) view).isSpoilerOnClick()) { + ((SpoilerOnClickTextView) view).setSpoilerOnClick(false); + return; + } } hideToolbar(); }); @@ -1596,9 +1645,11 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter { - if (commentMarkdownView.getSelectionStart() == -1 && commentMarkdownView.getSelectionEnd() == -1) { - expandComments(); + mMarkwonAdapter.setOnLongClickListener(view -> { + if (view instanceof TextView) { + if (((TextView) view).getSelectionStart() == -1 && ((TextView) view).getSelectionEnd() == -1) { + expandComments(); + } } return true; }); @@ -1607,7 +1658,6 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomMarkwonAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomMarkwonAdapter.java new file mode 100644 index 00000000..fb10d036 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/customviews/CustomMarkwonAdapter.java @@ -0,0 +1,235 @@ +package ml.docilealligator.infinityforreddit.customviews; + +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; + +import org.commonmark.node.Node; + +import java.util.Collections; +import java.util.List; + +import io.noties.markwon.Markwon; +import io.noties.markwon.MarkwonReducer; +import io.noties.markwon.recycler.MarkwonAdapter; +import io.noties.markwon.recycler.SimpleEntry; +import ml.docilealligator.infinityforreddit.R; + +public class CustomMarkwonAdapter extends MarkwonAdapter { + private final SparseArray> entries; + private final Entry defaultEntry; + private final MarkwonReducer reducer; + + private LayoutInflater layoutInflater; + + private Markwon markwon; + private List nodes; + + private View.OnClickListener onClickListener; + private View.OnLongClickListener onLongClickListener; + + @SuppressWarnings("WeakerAccess") + CustomMarkwonAdapter( + @NonNull SparseArray> entries, + @NonNull Entry defaultEntry, + @NonNull MarkwonReducer reducer) { + this.entries = entries; + this.defaultEntry = defaultEntry; + this.reducer = reducer; + + setHasStableIds(true); + } + + public void setOnClickListener(View.OnClickListener onClickListener) { + this.onClickListener = onClickListener; + } + + public void setOnLongClickListener(View.OnLongClickListener onLongClickListener) { + this.onLongClickListener = onLongClickListener; + } + + @NonNull + public static CustomBuilderImpl builder( + @LayoutRes int defaultEntryLayoutResId, + @IdRes int defaultEntryTextViewResId + ) { + return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId)); + } + + @NonNull + public static CustomBuilderImpl builder(@NonNull Entry defaultEntry) { + //noinspection unchecked + return new CustomBuilderImpl((Entry) defaultEntry); + } + + @Override + public void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown) { + setParsedMarkdown(markwon, markwon.parse(markdown)); + } + + @Override + public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull Node document) { + setParsedMarkdown(markwon, reducer.reduce(document)); + } + + @Override + public void setParsedMarkdown(@NonNull Markwon markwon, @NonNull List nodes) { + // clear all entries before applying + + defaultEntry.clear(); + + for (int i = 0, size = entries.size(); i < size; i++) { + entries.valueAt(i).clear(); + } + + this.markwon = markwon; + this.nodes = nodes; + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + if (layoutInflater == null) { + layoutInflater = LayoutInflater.from(parent.getContext()); + } + + final Entry 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()); + + final Entry entry = getEntry(viewType); + + entry.bindHolder(markwon, holder, node); + + if (holder.itemView instanceof SpoilerOnClickTextView) { + holder.itemView.setOnClickListener(onClickListener); + holder.itemView.setOnLongClickListener(onLongClickListener); + } else if (holder.itemView instanceof HorizontalScrollView) { + TableLayout tableLayout = holder.itemView.findViewById(R.id.table_layout); + if (tableLayout != null) { + for (int i = 0; i < tableLayout.getChildCount(); i++) { + if (tableLayout.getChildAt(i) instanceof TableRow) { + TableRow tableRow = ((TableRow) tableLayout.getChildAt(i)); + for (int j = 0; j < tableRow.getChildCount(); j++) { + if (tableRow.getChildAt(j) instanceof TextView) { + tableRow.getChildAt(j).setOnClickListener(onClickListener); + tableRow.getChildAt(j).setOnLongClickListener(onLongClickListener); + } + } + } + } + } + } + } + + @Override + public int getItemCount() { + return nodes != null + ? nodes.size() + : 0; + } + + @Override + public void onViewRecycled(@NonNull Holder holder) { + super.onViewRecycled(holder); + + final Entry entry = getEntry(holder.getItemViewType()); + entry.onViewRecycled(holder); + } + + @SuppressWarnings("unused") + @NonNull + public List getItems() { + return nodes != null + ? Collections.unmodifiableList(nodes) + : Collections.emptyList(); + } + + @Override + public int getItemViewType(int position) { + return getNodeViewType(nodes.get(position).getClass()); + } + + @Override + public long getItemId(int position) { + final Node node = nodes.get(position); + final int type = getNodeViewType(node.getClass()); + final Entry entry = getEntry(type); + return entry.id(node); + } + + @Override + public int getNodeViewType(@NonNull Class node) { + // if has registered -> then return it, else 0 + final int hash = node.hashCode(); + if (entries.indexOfKey(hash) > -1) { + return hash; + } + return 0; + } + + @NonNull + private Entry getEntry(int viewType) { + return viewType == 0 + ? defaultEntry + : entries.get(viewType); + } + + public static class CustomBuilderImpl implements Builder { + + private final SparseArray> entries = new SparseArray<>(3); + + private final Entry defaultEntry; + + private MarkwonReducer reducer; + + CustomBuilderImpl(@NonNull Entry defaultEntry) { + this.defaultEntry = defaultEntry; + } + + @NonNull + @Override + public CustomBuilderImpl include( + @NonNull Class node, + @NonNull Entry entry) { + //noinspection unchecked + entries.append(node.hashCode(), (Entry) entry); + return this; + } + + @NonNull + @Override + public CustomBuilderImpl reducer(@NonNull MarkwonReducer reducer) { + this.reducer = reducer; + return this; + } + + @NonNull + @Override + public CustomMarkwonAdapter build() { + + if (reducer == null) { + reducer = MarkwonReducer.directChildren(); + } + + return new CustomMarkwonAdapter(entries, defaultEntry, reducer); + } + } +} diff --git a/app/src/main/res/layout/adapter_default_entry.xml b/app/src/main/res/layout/adapter_default_entry.xml index a02d03f0..b011fbf5 100644 --- a/app/src/main/res/layout/adapter_default_entry.xml +++ b/app/src/main/res/layout/adapter_default_entry.xml @@ -1,5 +1,5 @@ - - + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="8dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:nestedScrollingEnabled="false" />