From 10f983c953d650432cc5bfffcd95a1c4f8d1628e Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Mon, 27 Feb 2023 08:54:51 +0100 Subject: [PATCH] Ignore clicks outside the start/end of a line (#3380) * Ignore clicks outside the start/end of a line `LinkMovementMethod` has a bug in its calculation of the clickable width of a span on a line. If the span is the last thing on the line the clickable area extends to the end of the view. So the user can tap what appears to be whitespace and open a link. Previous code tried to fix this by adding a zero-width space after the link so that `LinkMovementMethod` wouldn't consider it empty. However the ZWS was selected by copy/paste operations, resulting in junk results if users tried to copy the link. Fix this by subclassing `LinkMovementMethod` and fixing the click detection code to ignore clicks that are outside the bounds of the line that was clicked on. Remove the code that adds the ZWS. Fixes https://github.com/tuskyapp/Tusky/issues/1567 * Assume arguments are all non-null * Use `object` for singleton * getInstance as a one-liner --- .../keylesspalace/tusky/util/LinkHelper.kt | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt index ff225951a..7e23e45d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt @@ -11,7 +11,8 @@ * Public License for more details. * * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ + * see . + */ @file:JvmName("LinkHelper") package com.keylesspalace.tusky.util @@ -21,12 +22,15 @@ import android.content.Context import android.content.Intent import android.graphics.Color import android.net.Uri +import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.method.LinkMovementMethod import android.text.style.ClickableSpan import android.text.style.URLSpan import android.util.Log +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_UP import android.view.View import android.widget.TextView import androidx.annotation.VisibleForTesting @@ -68,7 +72,7 @@ fun setClickableText(view: TextView, content: CharSequence, mentions: List= length || subSequence(end, end + 1).toString() == "\n") { - insert(end, "\u200B") - } } @VisibleForTesting @@ -199,12 +196,10 @@ fun setClickableMentions(view: TextView, mentions: List?, listener: Lin append("@") append(mention.localUsername) setSpan(customSpan, start, end, flags) - append("\u200B") // same reasoning as in setClickableText - end += 1 // shift position to take the previous character into account start = end } } - view.movementMethod = LinkMovementMethod.getInstance() + view.movementMethod = NoTrailingSpaceLinkMovementMethod.getInstance() } fun createClickableText(text: String, link: String): CharSequence { @@ -322,3 +317,35 @@ fun looksLikeMastodonUrl(urlString: String): Boolean { } private const val TAG = "LinkHelper" + +/** + * [LinkMovementMethod] that doesn't add a leading/trailing clickable area. + * + * [LinkMovementMethod] has a bug in its calculation of the clickable width of a span on a line. If + * the span is the last thing on the line the clickable area extends to the end of the view. So the + * user can tap what appears to be whitespace and open a link. + * + * Fix this by overriding ACTION_UP touch events and calculating the true start and end of the + * content on the line that was tapped. Then ignore clicks that are outside this area. + * + * See https://github.com/tuskyapp/Tusky/issues/1567. + */ +object NoTrailingSpaceLinkMovementMethod : LinkMovementMethod() { + override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean { + val action = event.action + if (action != ACTION_UP) return super.onTouchEvent(widget, buffer, event) + + val x = event.x.toInt() + val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY + val line = widget.layout.getLineForVertical(y) + val lineLeft = widget.layout.getLineLeft(line) + val lineRight = widget.layout.getLineRight(line) + if (x > lineRight || x >= 0 && x < lineLeft) { + return true + } + + return super.onTouchEvent(widget, buffer, event) + } + + fun getInstance() = NoTrailingSpaceLinkMovementMethod +}