Render full markdown in CommentsRecyclerViewAdapter.

This commit is contained in:
Docile-Alligator
2022-04-02 15:50:12 +08:00
parent f0e2090db9
commit 95d792779e
5 changed files with 314 additions and 31 deletions

View File

@@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.Spanned;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -33,6 +34,8 @@ import com.bumptech.glide.RequestManager;
import com.bumptech.glide.request.RequestOptions; import com.bumptech.glide.request.RequestOptions;
import com.lsjwzh.widget.materialloadingprogressbar.CircleProgressBar; import com.lsjwzh.widget.materialloadingprogressbar.CircleProgressBar;
import org.commonmark.ext.gfm.tables.TableBlock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.Executor; 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.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.movement.MovementMethodPlugin; 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 jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import me.saket.bettermovementmethod.BetterLinkMovementMethod; import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import ml.docilealligator.infinityforreddit.R; 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.comment.FetchComment;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper; import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.customviews.CommentIndentationView; 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.customviews.SpoilerOnClickTextView;
import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment; import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin; import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
@@ -101,6 +109,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
private ArrayList<Comment> mVisibleComments; private ArrayList<Comment> mVisibleComments;
private Locale mLocale; private Locale mLocale;
private RequestManager mGlide; private RequestManager mGlide;
private RecyclerView.RecycledViewPool recycledViewPool;
private String mSingleCommentId; private String mSingleCommentId;
private boolean mIsSingleCommentThreadMode; private boolean mIsSingleCommentThreadMode;
private boolean mVoteButtonsOnTheRight; private boolean mVoteButtonsOnTheRight;
@@ -189,6 +198,15 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
return Utils.fixSuperScript(markdown); return Utils.fixSuperScript(markdown);
} }
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
if (mActivity.contentTypeface != null) {
textView.setTypeface(mActivity.contentTypeface);
}
textView.setTextColor(mCommentTextColor);
textView.setHighlightColor(Color.TRANSPARENT);
}
@Override @Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver((view, link) -> { builder.linkResolver((view, link) -> {
@@ -207,7 +225,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
}) })
.usePlugin(SpoilerParserPlugin.create(mCommentTextColor, commentSpoilerBackgroundColor)) .usePlugin(SpoilerParserPlugin.create(mCommentTextColor, commentSpoilerBackgroundColor))
.usePlugin(StrikethroughPlugin.create()) .usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.newInstance().setOnLinkLongClickListener((textView, url) -> { .usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> {
if (!activity.isDestroyed() && !activity.isFinishing()) { if (!activity.isDestroyed() && !activity.isFinishing()) {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment(); UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
@@ -218,7 +236,9 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
return true; return true;
}))) })))
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS)) .usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.usePlugin(TableEntryPlugin.create(mActivity))
.build(); .build();
recycledViewPool = new RecyclerView.RecycledViewPool();
mAccessToken = accessToken; mAccessToken = accessToken;
mAccountName = accountName; mAccountName = accountName;
mPost = post; mPost = post;
@@ -447,7 +467,8 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
Utils.setHTMLWithImageToTextView(((CommentViewHolder) holder).awardsTextView, comment.getAwards(), true); Utils.setHTMLWithImageToTextView(((CommentViewHolder) holder).awardsTextView, comment.getAwards(), true);
} }
mCommentMarkwon.setMarkdown(((CommentViewHolder) holder).commentMarkdownView, comment.getCommentMarkdown()); ((CommentViewHolder) holder).mMarkwonAdapter.setMarkdown(mCommentMarkwon, comment.getCommentMarkdown());
((CommentViewHolder) holder).mMarkwonAdapter.notifyDataSetChanged();
((CommentViewHolder) holder).scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes, ((CommentViewHolder) holder).scoreTextView.setText(Utils.getNVotes(mShowAbsoluteNumberOfVotes,
comment.getScore() + comment.getVoteType())); comment.getScore() + comment.getVoteType()));
((CommentViewHolder) holder).topScoreTextView.setText(mActivity.getString(R.string.top_score, ((CommentViewHolder) holder).topScoreTextView.setText(mActivity.getString(R.string.top_score,
@@ -1143,7 +1164,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
@BindView(R.id.awards_text_view_item_comment) @BindView(R.id.awards_text_view_item_comment)
TextView awardsTextView; TextView awardsTextView;
@BindView(R.id.comment_markdown_view_item_post_comment) @BindView(R.id.comment_markdown_view_item_post_comment)
SpoilerOnClickTextView commentMarkdownView; RecyclerView commentMarkdownView;
@BindView(R.id.bottom_constraint_layout_item_post_comment) @BindView(R.id.bottom_constraint_layout_item_post_comment)
ConstraintLayout bottomConstraintLayout; ConstraintLayout bottomConstraintLayout;
@BindView(R.id.up_vote_button_item_post_comment) @BindView(R.id.up_vote_button_item_post_comment)
@@ -1166,6 +1187,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
CommentIndentationView commentIndentationView; CommentIndentationView commentIndentationView;
@BindView(R.id.divider_item_comment) @BindView(R.id.divider_item_comment)
View commentDivider; View commentDivider;
CustomMarkwonAdapter mMarkwonAdapter;
CommentViewHolder(View itemView) { CommentViewHolder(View itemView) {
super(itemView); super(itemView);
@@ -1225,9 +1247,6 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
scoreTextView.setTypeface(mActivity.typeface); scoreTextView.setTypeface(mActivity.typeface);
expandButton.setTypeface(mActivity.typeface); expandButton.setTypeface(mActivity.typeface);
} }
if (mActivity.contentTypeface != null) {
commentMarkdownView.setTypeface(mActivity.contentTypeface);
}
if (mShowAuthorAvatar) { if (mShowAuthorAvatar) {
authorIconImageView.setVisibility(View.VISIBLE); authorIconImageView.setVisibility(View.VISIBLE);
@@ -1236,10 +1255,29 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
((ConstraintLayout.LayoutParams) authorFlairTextView.getLayoutParams()).leftMargin = 0; ((ConstraintLayout.LayoutParams) authorFlairTextView.getLayoutParams()).leftMargin = 0;
} }
commentMarkdownView.setRecycledViewPool(recycledViewPool);
LinearLayoutManagerBugFixed linearLayoutManager = new MarkwonLinearLayoutManager(mActivity, new MarkwonLinearLayoutManager.HorizontalScrollViewScrolledListener() {
@Override
public void onScrolledLeft() {
((ViewPostDetailActivity) mActivity).lockSwipeRightToGoBack();
}
@Override
public void onScrolledRight() {
((ViewPostDetailActivity) mActivity).unlockSwipeRightToGoBack();
}
});
commentMarkdownView.setLayoutManager(linearLayoutManager);
mMarkwonAdapter = CustomMarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
.include(TableBlock.class, TableEntry.create(builder -> 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); itemView.setBackgroundColor(mCommentBackgroundColor);
authorTextView.setTextColor(mUsernameColor); authorTextView.setTextColor(mUsernameColor);
commentTimeTextView.setTextColor(mSecondaryTextColor); commentTimeTextView.setTextColor(mSecondaryTextColor);
commentMarkdownView.setTextColor(mCommentTextColor);
authorFlairTextView.setTextColor(mAuthorFlairTextColor); authorFlairTextView.setTextColor(mAuthorFlairTextColor);
topScoreTextView.setTextColor(mSecondaryTextColor); topScoreTextView.setTextColor(mSecondaryTextColor);
awardsTextView.setTextColor(mSecondaryTextColor); awardsTextView.setTextColor(mSecondaryTextColor);
@@ -1572,23 +1610,34 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
if (mCommentToolbarHideOnClick) { if (mCommentToolbarHideOnClick) {
View.OnLongClickListener hideToolbarOnLongClickListener = view -> hideToolbar(); View.OnLongClickListener hideToolbarOnLongClickListener = view -> hideToolbar();
itemView.setOnLongClickListener(hideToolbarOnLongClickListener); itemView.setOnLongClickListener(hideToolbarOnLongClickListener);
commentMarkdownView.setOnLongClickListener(hideToolbarOnLongClickListener);
commentTimeTextView.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 -> { mMarkwonAdapter.setOnClickListener(v -> {
if (commentMarkdownView.isSpoilerOnClick()) { if (v instanceof SpoilerOnClickTextView) {
commentMarkdownView.setSpoilerOnClick(false); if (((SpoilerOnClickTextView) v).isSpoilerOnClick()) {
return; ((SpoilerOnClickTextView) v).setSpoilerOnClick(false);
return;
}
} }
expandComments(); expandComments();
}); });
itemView.setOnClickListener(view -> expandComments()); itemView.setOnClickListener(view -> expandComments());
} else { } else {
if (mCommentToolbarHideOnClick) { if (mCommentToolbarHideOnClick) {
commentMarkdownView.setOnClickListener(view -> { mMarkwonAdapter.setOnClickListener(view -> {
if (commentMarkdownView.isSpoilerOnClick()) { if (view instanceof SpoilerOnClickTextView) {
commentMarkdownView.setSpoilerOnClick(false); if (((SpoilerOnClickTextView) view).isSpoilerOnClick()) {
return; ((SpoilerOnClickTextView) view).setSpoilerOnClick(false);
return;
}
} }
hideToolbar(); hideToolbar();
}); });
@@ -1596,9 +1645,11 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
itemView.setOnClickListener(hideToolbarOnClickListener); itemView.setOnClickListener(hideToolbarOnClickListener);
commentTimeTextView.setOnClickListener(hideToolbarOnClickListener); commentTimeTextView.setOnClickListener(hideToolbarOnClickListener);
} }
commentMarkdownView.setOnLongClickListener(view -> { mMarkwonAdapter.setOnLongClickListener(view -> {
if (commentMarkdownView.getSelectionStart() == -1 && commentMarkdownView.getSelectionEnd() == -1) { if (view instanceof TextView) {
expandComments(); if (((TextView) view).getSelectionStart() == -1 && ((TextView) view).getSelectionEnd() == -1) {
expandComments();
}
} }
return true; return true;
}); });
@@ -1607,7 +1658,6 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
return true; return true;
}); });
} }
commentMarkdownView.setHighlightColor(Color.TRANSPARENT);
} }
private boolean expandComments() { private boolean expandComments() {

View File

@@ -259,7 +259,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
@Override @Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
if (mActivity.contentTypeface != null) { if (mActivity.contentTypeface != null) {
textView.setTypeface(mActivity.typeface); textView.setTypeface(mActivity.contentTypeface);
} }
textView.setTextColor(markdownColor); textView.setTextColor(markdownColor);
textView.setOnLongClickListener(view -> { textView.setOnLongClickListener(view -> {

View File

@@ -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<Entry<Node, Holder>> entries;
private final Entry<Node, Holder> defaultEntry;
private final MarkwonReducer reducer;
private LayoutInflater layoutInflater;
private Markwon markwon;
private List<Node> nodes;
private View.OnClickListener onClickListener;
private View.OnLongClickListener onLongClickListener;
@SuppressWarnings("WeakerAccess")
CustomMarkwonAdapter(
@NonNull SparseArray<Entry<Node, Holder>> entries,
@NonNull Entry<Node, Holder> 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<? extends Node, ? extends Holder> defaultEntry) {
//noinspection unchecked
return new CustomBuilderImpl((Entry<Node, Holder>) 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<Node> 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<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());
final Entry<Node, Holder> 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<Node, Holder> entry = getEntry(holder.getItemViewType());
entry.onViewRecycled(holder);
}
@SuppressWarnings("unused")
@NonNull
public List<Node> getItems() {
return nodes != null
? Collections.unmodifiableList(nodes)
: Collections.<Node>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<Node, Holder> entry = getEntry(type);
return entry.id(node);
}
@Override
public int getNodeViewType(@NonNull Class<? extends Node> 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<Node, Holder> getEntry(int viewType) {
return viewType == 0
? defaultEntry
: entries.get(viewType);
}
public static class CustomBuilderImpl implements Builder {
private final SparseArray<Entry<Node, Holder>> entries = new SparseArray<>(3);
private final Entry<Node, Holder> defaultEntry;
private MarkwonReducer reducer;
CustomBuilderImpl(@NonNull Entry<Node, Holder> defaultEntry) {
this.defaultEntry = defaultEntry;
}
@NonNull
@Override
public <N extends Node> CustomBuilderImpl include(
@NonNull Class<N> node,
@NonNull Entry<? super N, ? extends Holder> entry) {
//noinspection unchecked
entries.append(node.hashCode(), (Entry<Node, Holder>) 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);
}
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -119,16 +119,14 @@
android:fontFamily="?attr/font_family" android:fontFamily="?attr/font_family"
tools:visibility="visible" /> tools:visibility="visible" />
<ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/comment_markdown_view_item_post_comment" android:id="@+id/comment_markdown_view_item_post_comment"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_margin="8dp" android:layout_marginTop="8dp"
android:paddingStart="8dp" android:layout_marginStart="8dp"
android:paddingEnd="8dp" android:layout_marginEnd="8dp"
android:textColor="?attr/primaryTextColor" android:nestedScrollingEnabled="false" />
android:textSize="?attr/content_font_default"
android:fontFamily="?attr/content_font_family" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bottom_constraint_layout_item_post_comment" android:id="@+id/bottom_constraint_layout_item_post_comment"