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.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<RecyclerVi
private ArrayList<Comment> 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<RecyclerVi
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
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver((view, link) -> {
@ -207,7 +225,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
})
.usePlugin(SpoilerParserPlugin.create(mCommentTextColor, commentSpoilerBackgroundColor))
.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()) {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle();
@ -218,7 +236,9 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
return true;
})))
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.usePlugin(TableEntryPlugin.create(mActivity))
.build();
recycledViewPool = new RecyclerView.RecycledViewPool();
mAccessToken = accessToken;
mAccountName = accountName;
mPost = post;
@ -447,7 +467,8 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
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,
comment.getScore() + comment.getVoteType()));
((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)
TextView awardsTextView;
@BindView(R.id.comment_markdown_view_item_post_comment)
SpoilerOnClickTextView commentMarkdownView;
RecyclerView commentMarkdownView;
@BindView(R.id.bottom_constraint_layout_item_post_comment)
ConstraintLayout bottomConstraintLayout;
@BindView(R.id.up_vote_button_item_post_comment)
@ -1166,6 +1187,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
CommentIndentationView commentIndentationView;
@BindView(R.id.divider_item_comment)
View commentDivider;
CustomMarkwonAdapter mMarkwonAdapter;
CommentViewHolder(View itemView) {
super(itemView);
@ -1225,9 +1247,6 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
scoreTextView.setTypeface(mActivity.typeface);
expandButton.setTypeface(mActivity.typeface);
}
if (mActivity.contentTypeface != null) {
commentMarkdownView.setTypeface(mActivity.contentTypeface);
}
if (mShowAuthorAvatar) {
authorIconImageView.setVisibility(View.VISIBLE);
@ -1236,10 +1255,29 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
((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);
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<RecyclerVi
if (mCommentToolbarHideOnClick) {
View.OnLongClickListener hideToolbarOnLongClickListener = view -> 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<RecyclerVi
itemView.setOnClickListener(hideToolbarOnClickListener);
commentTimeTextView.setOnClickListener(hideToolbarOnClickListener);
}
commentMarkdownView.setOnLongClickListener(view -> {
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<RecyclerVi
return true;
});
}
commentMarkdownView.setHighlightColor(Color.TRANSPARENT);
}
private boolean expandComments() {

View File

@ -259,7 +259,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
if (mActivity.contentTypeface != null) {
textView.setTypeface(mActivity.typeface);
textView.setTypeface(mActivity.contentTypeface);
}
textView.setTextColor(markdownColor);
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"?>
<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:layout_width="match_parent"
android:layout_height="wrap_content"

View File

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