Skip to content
This repository has been archived by the owner on Sep 27, 2024. It is now read-only.

[Android] Make some spans clickable (pills, links) in TextView #860

Merged
merged 3 commits into from
Nov 9, 2023

Conversation

jmartinesp
Copy link
Contributor

  • Add an associated url to PillSpan and CustomReplacementSpan.
  • Rename CustomReplacementSpan to CustomMentionSpan.
  • Override EditorStyledTextView.onTouchEvent to detect clicks in these clickable spans.
  • Add onLinkClickedListener to get notified when they are clicked.

@codecov-commenter
Copy link

codecov-commenter commented Nov 6, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (d0ba997) 87.44% compared to head (5eeefc4) 89.34%.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #860      +/-   ##
============================================
+ Coverage     87.44%   89.34%   +1.90%     
============================================
  Files           165       86      -79     
  Lines         18816    15247    -3569     
  Branches       1024        0    -1024     
============================================
- Hits          16453    13622    -2831     
+ Misses         2077     1625     -452     
+ Partials        286        0     -286     
Flag Coverage Δ
uitests ?
uitests-android ?
uitests-ios ?
unittests 89.34% <ø> (+3.77%) ⬆️
unittests-android ?
unittests-ios ?
unittests-rust 89.34% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

see 79 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@jonnyandrew jonnyandrew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a question on the behaviour.

Comment on lines 145 to 149
MotionEvent.ACTION_UP -> {
// Find selection matching the pointer coordinates
val offset = getOffsetForPosition(event.x, event.y)
// Look for clickable spans in that position
val spans = (text as? Spanned)?.getSpans(offset, offset, Any::class.java) ?: return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea! A few thoughts which may or may not not be issues:

  • Have you tested if there are any conflicts using setMovementMethod(LinkMovementMethod) after this change?
  • What happens if an app returns a custom clickable span in the mention display handler?
  • Could this trigger if a user scrolls by dragging a link/pill/mention?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Have you tested if there are any conflicts using setMovementMethod(LinkMovementMethod) after this change?

I think LinkMovementMethod needs the TextView to be selectable, which we don't allow at the moment, but I'd have to double check. Maybe instead of returning true in this case I should call the super function. Nice catch.

  • What happens if an app returns a custom clickable span in the mention display handler?

I think internally it would be represented by a CustomMentionSpan, which is a ReplacementSpan and can't contain a ClickableSpan. With TextDisplay.Custom you can just pass a ReplacementSpan if I'm not mistaken.

  • Could this trigger if a user scrolls by dragging a link/pill/mention?

Oh, nice catch. Maybe I should perform this same search in ACTION_DOWN, keep a reference to the selected spans and then check if I get the same ones in ACTION_UP.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't add any automated tests for the 1st question, but I can confirm it works. I also added some new tests and made sure scrolling won't trigger a click (which apparently does happen with the default ClickableSpan).

@@ -128,4 +134,38 @@ open class EditorStyledTextView : AppCompatTextView {
}
)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please could you also add a UI test for the new behaviour?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to, but it's not that simple to select a pill in the TextView. Maybe if I make the only content of the TextView it'll make things easier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem - thanks for adding these tests!

- Made sure autodetected links are clickable.
- Make sure touch events are properly recognised and can't affect scrolling.
- Add missing tests.
if (span.url == null) continue
onLinkClickedListener?.invoke(span.url)
MotionEvent.ACTION_MOVE -> {
val distance = initialTouchCoordinates?.let { hypot(it.first - event.x, it.second - event.y) } ?: 0f
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could skip a this calculation too by doing if (!maybeClickEvent) return here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could, but I think ACTION_MOVE can only happen after ACTION_DOWN, so maybeClickEvent should always be true at that point. I can add it, just in case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought ACTION_MOVE could be called many times during scrolling before ACTION_UP so thought of skipping any after maybeClickEvent gets reset here. Maybe I missed something though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you're right, I completely forgot it could be called several times. In any case, using the GestureDetector makes this code no longer needed, so I'll remove it.

onLinkClickedListener?.invoke(span.url)
MotionEvent.ACTION_MOVE -> {
val distance = initialTouchCoordinates?.let { hypot(it.first - event.x, it.second - event.y) } ?: 0f
// If the distance between the initial coordinates and the current ones is too big (4dp), we can assume it's not a click
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am worried that manual detection of scroll / click gestures could be a bit error prone and lead to something which is either too sensitive or not sensitive enough. I wonder if there is a way to avoid implementing this ourselves manually?

I saw GestureDetector which looks like it does something similar - is it worth looking into?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleGestureDetector does just this and simplifies the code a lot. Thanks for the suggestion!

@@ -128,4 +134,38 @@ open class EditorStyledTextView : AppCompatTextView {
}
)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem - thanks for adding these tests!

Copy link
Contributor

@jonnyandrew jonnyandrew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement, thank you!

Copy link

sonarqubecloud bot commented Nov 9, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
No Duplication information No Duplication information

@jmartinesp jmartinesp merged commit 06463e6 into main Nov 9, 2023
6 of 7 checks passed
@jmartinesp jmartinesp deleted the android/make-pills-clickable branch November 9, 2023 10:03
@jmartinesp
Copy link
Contributor Author

Oops, I merged this before the CI passed for a couple of jobs. I guess I'm too used to the 'auto merge when all required jobs pass' feature on the EXA repo 😅 .

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants