Fix spoiler interactions with links and long clicks (#1129)

* Prioritize clicks on hidden spoilers over links

Extend BetterLinkMovementMethod to override selection of span that will be
touched and give spoilers and links following priorities:
1. Hidden spoiler
2. Non-spoiler span (i.e. link)
3. Shown spoiler

#609

* Ignore long clicks on spoilers

Ignore long clicks if it is a SpoilerSpan. Also don't apply highlight
because it breaks spoiler effect.

#529
This commit is contained in:
Sergei Kozelko 2022-09-25 15:13:59 +07:00 committed by GitHub
parent 85debf62f3
commit fad978432e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 135 additions and 10 deletions

View File

@ -56,6 +56,7 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
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.MarkwonAdapter;
import io.noties.markwon.recycler.table.TableEntry;
import io.noties.markwon.recycler.table.TableEntryPlugin;
@ -78,6 +79,7 @@ import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFi
import ml.docilealligator.infinityforreddit.databinding.ActivityCommentBinding;
import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
@ -239,6 +241,7 @@ public class CommentActivity extends BaseActivity implements UploadImageEnabledA
.usePlugin(SpoilerParserPlugin.create(parentTextColor, parentSpoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod()))
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.usePlugin(TableEntryPlugin.create(this))
.build();

View File

@ -45,6 +45,7 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
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.MarkwonAdapter;
import io.noties.markwon.recycler.table.TableEntry;
import io.noties.markwon.recycler.table.TableEntryPlugin;
@ -55,6 +56,7 @@ import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFi
import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManager;
import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
@ -172,6 +174,7 @@ public class FullMarkdownActivity extends BaseActivity {
.usePlugin(SpoilerParserPlugin.create(markdownColor, spoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod()))
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.usePlugin(TableEntryPlugin.create(this))
.build();

View File

@ -55,7 +55,6 @@ import io.noties.markwon.movement.MovementMethodPlugin;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.table.TableEntry;
import io.noties.markwon.recycler.table.TableEntryPlugin;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import ml.docilealligator.infinityforreddit.Infinity;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.apis.RedditAPI;
@ -65,6 +64,7 @@ import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFi
import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManager;
import ml.docilealligator.infinityforreddit.events.SwitchAccountEvent;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.utils.JSONUtils;
@ -201,7 +201,7 @@ public class WikiActivity extends BaseActivity {
.usePlugin(SpoilerParserPlugin.create(markdownColor, spoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> {
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod().setOnLinkLongClickListener((textView, url) -> {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle();
bundle.putString(UrlMenuBottomSheetFragment.EXTRA_URL, url);

View File

@ -48,7 +48,6 @@ 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 me.saket.bettermovementmethod.BetterLinkMovementMethod;
import ml.docilealligator.infinityforreddit.NetworkState;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.SaveThing;
@ -69,6 +68,7 @@ import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFi
import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManager;
import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
@ -195,7 +195,7 @@ public class CommentsListingRecyclerViewAdapter extends PagedListAdapter<Comment
.usePlugin(SpoilerParserPlugin.create(mCommentColor, commentSpoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> {
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod().setOnLinkLongClickListener((textView, url) -> {
if (!activity.isDestroyed() && !activity.isFinishing()) {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle();

View File

@ -58,7 +58,6 @@ 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;
import ml.docilealligator.infinityforreddit.SaveThing;
import ml.docilealligator.infinityforreddit.VoteThing;
@ -79,6 +78,7 @@ import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManag
import ml.docilealligator.infinityforreddit.customviews.SpoilerOnClickTextView;
import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.post.Post;
@ -227,7 +227,7 @@ public class CommentsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVi
.usePlugin(SpoilerParserPlugin.create(mCommentTextColor, commentSpoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> {
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod().setOnLinkLongClickListener((textView, url) -> {
if (!activity.isDestroyed() && !activity.isFinishing()) {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle();

View File

@ -35,6 +35,7 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
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 ml.docilealligator.infinityforreddit.NetworkState;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.activities.BaseActivity;
@ -44,6 +45,7 @@ import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.events.ChangeInboxCountEvent;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.message.FetchMessage;
@ -138,6 +140,7 @@ public class MessageRecyclerViewAdapter extends PagedListAdapter<Message, Recycl
.usePlugin(SpoilerParserPlugin.create(mSecondaryTextColor, spoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod()))
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.build();
mAccessToken = accessToken;

View File

@ -72,7 +72,6 @@ import io.noties.markwon.recycler.table.TableEntry;
import io.noties.markwon.recycler.table.TableEntryPlugin;
import jp.wasabeef.glide.transformations.BlurTransformation;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import ml.docilealligator.infinityforreddit.FetchGfycatOrRedgifsVideoLinks;
import ml.docilealligator.infinityforreddit.FetchStreamableVideo;
import ml.docilealligator.infinityforreddit.R;
@ -104,6 +103,7 @@ import ml.docilealligator.infinityforreddit.customviews.AspectRatioGifImageView;
import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManager;
import ml.docilealligator.infinityforreddit.fragments.ViewPostDetailFragment;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.post.Post;
@ -297,7 +297,7 @@ public class PostDetailRecyclerViewAdapter extends RecyclerView.Adapter<Recycler
.usePlugin(SpoilerParserPlugin.create(markdownColor, postSpoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> {
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod().setOnLinkLongClickListener((textView, url) -> {
if (activity != null && !activity.isDestroyed() && !activity.isFinishing()) {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle();

View File

@ -39,6 +39,7 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
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 jp.wasabeef.glide.transformations.RoundedCornersTransformation;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.activities.LinkResolverActivity;
@ -46,6 +47,7 @@ import ml.docilealligator.infinityforreddit.activities.ViewPrivateMessagesActivi
import ml.docilealligator.infinityforreddit.activities.ViewUserDetailActivity;
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.message.Message;
@ -121,6 +123,7 @@ public class PrivateMessagesDetailRecyclerViewAdapter extends RecyclerView.Adapt
.usePlugin(StrikethroughPlugin.create())
.usePlugin(SpoilerParserPlugin.create(commentColor, commentColor | 0xFF000000))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod()))
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
.build();
mShowElapsedTime = sharedPreferences.getBoolean(SharedPreferencesUtils.SHOW_ELAPSED_TIME_KEY, false);

View File

@ -38,7 +38,6 @@ import io.noties.markwon.movement.MovementMethodPlugin;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.recycler.table.TableEntry;
import io.noties.markwon.recycler.table.TableEntryPlugin;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
import ml.docilealligator.infinityforreddit.R;
import ml.docilealligator.infinityforreddit.Rule;
import ml.docilealligator.infinityforreddit.activities.BaseActivity;
@ -48,6 +47,7 @@ import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper;
import ml.docilealligator.infinityforreddit.customviews.LinearLayoutManagerBugFixed;
import ml.docilealligator.infinityforreddit.customviews.MarkwonLinearLayoutManager;
import ml.docilealligator.infinityforreddit.markdown.RedditHeadingPlugin;
import ml.docilealligator.infinityforreddit.markdown.SpoilerAwareMovementMethod;
import ml.docilealligator.infinityforreddit.markdown.SpoilerParserPlugin;
import ml.docilealligator.infinityforreddit.markdown.SuperscriptInlineProcessor;
import ml.docilealligator.infinityforreddit.utils.Utils;
@ -108,7 +108,7 @@ public class RulesRecyclerViewAdapter extends RecyclerView.Adapter<RulesRecycler
builder.linkColor(customThemeWrapper.getLinkColor());
}
})
.usePlugin(MovementMethodPlugin.create(BetterLinkMovementMethod.linkify(Linkify.WEB_URLS).setOnLinkLongClickListener((textView, url) -> {
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod().setOnLinkLongClickListener((textView, url) -> {
if (activity != null && !activity.isDestroyed() && !activity.isFinishing()) {
UrlMenuBottomSheetFragment urlMenuBottomSheetFragment = new UrlMenuBottomSheetFragment();
Bundle bundle = new Bundle();
@ -122,6 +122,7 @@ public class RulesRecyclerViewAdapter extends RecyclerView.Adapter<RulesRecycler
.usePlugin(SpoilerParserPlugin.create(mPrimaryTextColor, spoilerBackgroundColor))
.usePlugin(RedditHeadingPlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(MovementMethodPlugin.create(new SpoilerAwareMovementMethod()))
.usePlugin(TableEntryPlugin.create(activity))
.build();
}

View File

@ -0,0 +1,102 @@
package ml.docilealligator.infinityforreddit.markdown;
import android.graphics.RectF;
import android.text.Layout;
import android.text.Spannable;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.saket.bettermovementmethod.BetterLinkMovementMethod;
/**
* Extension of {@link BetterLinkMovementMethod} that handles {@link SpoilerSpan}s
*/
public class SpoilerAwareMovementMethod extends BetterLinkMovementMethod {
private final RectF touchedLineBounds = new RectF();
@Override
protected ClickableSpan findClickableSpanUnderTouch(TextView textView, Spannable text, MotionEvent event) {
// A copy of super method. Logic for selecting correct clickable span was moved to selectClickableSpan
// So we need to find the location in text where touch was made, regardless of whether the TextView
// has scrollable text. That is, not the entire text is currently visible.
int touchX = (int) event.getX();
int touchY = (int) event.getY();
// Ignore padding.
touchX -= textView.getTotalPaddingLeft();
touchY -= textView.getTotalPaddingTop();
// Account for scrollable text.
touchX += textView.getScrollX();
touchY += textView.getScrollY();
final Layout layout = textView.getLayout();
final int touchedLine = layout.getLineForVertical(touchY);
final int touchOffset = layout.getOffsetForHorizontal(touchedLine, touchX);
touchedLineBounds.left = layout.getLineLeft(touchedLine);
touchedLineBounds.top = layout.getLineTop(touchedLine);
touchedLineBounds.right = layout.getLineWidth(touchedLine) + touchedLineBounds.left;
touchedLineBounds.bottom = layout.getLineBottom(touchedLine);
if (touchedLineBounds.contains(touchX, touchY)) {
// Find a ClickableSpan that lies under the touched area.
final Object[] spans = text.getSpans(touchOffset, touchOffset, ClickableSpan.class);
// BEGIN Infinity changed
return selectClickableSpan(spans);
// END Infinity changed
} else {
// Touch lies outside the line's horizontal bounds where no spans should exist.
return null;
}
}
/**
* Select a span according to priorities:
* 1. Hidden spoiler
* 2. Non-spoiler span (i.e. link)
* 3. Shown spoiler
*/
@Nullable
private ClickableSpan selectClickableSpan(@NonNull Object[] spans) {
SpoilerSpan spoilerSpan = null;
ClickableSpan nonSpoilerSpan = null;
for (final Object span : spans) {
if (span instanceof SpoilerSpan) {
spoilerSpan = (SpoilerSpan) span;
} else if (span instanceof ClickableSpan) {
nonSpoilerSpan = (ClickableSpan) span;
}
}
if (spoilerSpan != null && !spoilerSpan.isShowing()) {
return spoilerSpan;
} else if (nonSpoilerSpan != null){
return nonSpoilerSpan;
} else {
return spoilerSpan;
}
}
@Override
protected void dispatchUrlLongClick(TextView textView, ClickableSpan clickableSpan) {
if (clickableSpan instanceof SpoilerSpan) {
((SpoilerSpan) clickableSpan).onLongClick(textView);
return;
}
super.dispatchUrlLongClick(textView, clickableSpan);
}
@Override
protected void highlightUrl(TextView textView, ClickableSpan clickableSpan, Spannable text) {
if (clickableSpan instanceof SpoilerSpan) {
return;
}
super.highlightUrl(textView, clickableSpan, text);
}
}

View File

@ -50,6 +50,16 @@ public class SpoilerSpan extends ClickableSpan {
widget.invalidate();
}
public void onLongClick(@NonNull View widget) {
if (widget instanceof SpoilerOnClickTextView) {
((SpoilerOnClickTextView) widget).setSpoilerOnClick(true);
}
}
public boolean isShowing() {
return isShowing;
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
if (isShowing) {