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
This commit is contained in:
Nik Clayton 2023-02-27 08:54:51 +01:00 committed by GitHub
parent 9340e7a6f4
commit 10f983c953
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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 <http://www.gnu.org/licenses>. */
* see <http://www.gnu.org/licenses>.
*/
@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<Menti
setClickableText(it, this, mentions, tags, listener)
}
}
view.movementMethod = LinkMovementMethod.getInstance()
view.movementMethod = NoTrailingSpaceLinkMovementMethod.getInstance()
}
@VisibleForTesting
@ -125,13 +129,6 @@ fun setClickableText(
removeSpan(span)
setSpan(customSpan, start, end, flags)
/* Add zero-width space after links in end of line to fix its too large hitbox.
* See also : https://github.com/tuskyapp/Tusky/issues/846
* https://github.com/tuskyapp/Tusky/pull/916 */
if (end >= length || subSequence(end, end + 1).toString() == "\n") {
insert(end, "\u200B")
}
}
@VisibleForTesting
@ -199,12 +196,10 @@ fun setClickableMentions(view: TextView, mentions: List<Mention>?, 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
}