Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve interop touches by using UIScrollView-like strategy #1440

Merged
merged 31 commits into from
Jul 12, 2024

Conversation

elijah-semyonov
Copy link

@elijah-semyonov elijah-semyonov commented Jul 11, 2024

Description

In this approach CMPGestureRecognizer delays touchesBegan until explicitly failed, and is required to fail by UIGestureRecognizer of children views (aka interop views). Failure happens after the first touch started and no motion above scroll slop happens within 150ms. If this happens, intercepted touches are delivered to children views (and their gesture recognisers), Compose itself gets all tracked touches as cancelled and ignores them until the touch sequence ends (imposed by UIKit).
This behavior is inspired by UIScrollView implementation.

Screen.Recording.2024-07-11.at.13.46.48.mov

Fixes

Improves behavior of touches in certain scenarios

Testing

This should be tested by QA

Release Notes

iOS - Features

  • Improvements in touches processing to detect if touches were meant to be delivered to interop views, or should be processed by Compose.

@elijah-semyonov elijah-semyonov marked this pull request as ready for review July 11, 2024 12:00
- (void)handleStateChange {
switch (self.state) {
case UIGestureRecognizerStateBegan:
NSLog(@"state = Began");
Copy link
Collaborator

Choose a reason for hiding this comment

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

it seems like logs should be removed

Copy link
Author

Choose a reason for hiding this comment

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

Indeed

@protocol CMPGestureRecognizerHandler <NSObject>

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sry, it looks like here some formatting issues with spaces.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

_scheduledFailureBlock = dispatchBlock;

// Calculate the delay time in dispatch_time_t
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please, move 0.15 to constants. Or add some meaningful comment because it looks like it require some explanation.

Copy link
Author

Choose a reason for hiding this comment

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

Moved to a local variable with an explanation

* [CMPGestureRecognizer.scheduleFailure], which will pass intercepted touches to the interop
* views if the gesture recognizer is not recognized within a certain time frame
* (UIScrollView reverse-engineered 150ms is used).
* The similar approach is used by [UIScrollView](https://developer.apple.com/documentation/uikit/uiscrollview?language=objc)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Link suggestion: https://developer.apple.com/documentation/uikit/uiscrollview

Copy link
Author

Choose a reason for hiding this comment

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

Aha, changed

* scroll and compose horizontal scroll with different fingers)
*
* 4. Those are not the first touches in the sequence. A gesture is not recognized.
* See if centroid of the tracked touches has moved enough to recognize the gesture.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm worry regarding this scenario. The second touch will cause significant offset of centroid, and with high probability it will trigger pan gesture, instead of multi-touch gesture, like pinch zoom. Just wonder if it will be a problem on interop maps.

Copy link
Author

Choose a reason for hiding this comment

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

If second touch lands after the gesture was failed, it's not even delivered to it, so the gesture will properly be recognised by maps, for example.

Copy link

Choose a reason for hiding this comment

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

I'm afraid this might break the behavior of detectTransformGestures. On 1.7.0-alpha02, it seemed detectTransformGestures always call onGesture with zoom = 1.0f on iOS.
I have create an issue on youtrack: https://youtrack.jetbrains.com/issue/CMP-5879/detectTransformGestures-doesnt-work-properly-on-iOS-with-Compose-Multiplatform-1.7.0-alpha02

Copy link
Author

Choose a reason for hiding this comment

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

Hi, thanks, already looking into it.

* location to null.
*/
private fun stopTrackingTouches(touches: Set<*>) {
onTouchesCountChanged(-touches.size)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would move this call on after we actually remove touches from trackedTouches

Copy link
Author

Choose a reason for hiding this comment

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

They are functionally equivalent now, because effects of this call are dispatched later on main thread by OS itself (display link changes). But let's do it for sake of consistency with onFailure.

* @return `true` if the touches are initial, `false` otherwise.
*/
private fun startTrackingTouches(touches: Set<*>): Boolean {
onTouchesCountChanged(touches.size)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would move this call on after we actually remove touches from trackedTouches

Copy link
Author

Choose a reason for hiding this comment

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

Done

*/
var gestureRecognizer: CMPGestureRecognizer? = null

private var state: UIGestureRecognizerState
Copy link
Collaborator

Choose a reason for hiding this comment

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

Name suggestion: gestureRecognizerState

Copy link
Author

Choose a reason for hiding this comment

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

Done

val initialLocation = initialLocation ?: return false
val centroidLocation = trackedTouchesCentroidLocation ?: return false

val slop = 10.0
Copy link
Member

Choose a reason for hiding this comment

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

As far as I remember we already had such constant. Can it be reused?

Copy link
Author

Choose a reason for hiding this comment

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

Extracted to CUPERTINO_PAN_GESTURE_SLOP_VALUE

Comment on lines +120 to +123
val dx = centroidLocation.useContents { x - initialLocation.useContents { x } }
val dy = centroidLocation.useContents { y - initialLocation.useContents { y } }

return dx * dx + dy * dy > slop * slop
Copy link
Member

Choose a reason for hiding this comment

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

A good candidate for utility/helper function

Copy link
Author

Choose a reason for hiding this comment

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

Comparing squared lengths?
I doubt it happens again, let it be here for now.

* that it can be used later to determine if the gesture recognizer should be recognized
* or failed.
*/
private fun rememberHitTestResult(hitTestBlock: () -> UIView?): UIView? {
Copy link
Member

Choose a reason for hiding this comment

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

rememberX has very specific semantic in Compose, please rename to avoid confusions

Copy link
Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

@MatkovIvan MatkovIvan left a comment

Choose a reason for hiding this comment

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

The main change/delay logic LGTM, I just have a couple of nitpicks

/**
* iOS default value in scale-independent points for the pan gesture slop.
*/
internal const val CUPERTINO_PAN_GESTURE_SLOP_VALUE = 10
Copy link
Member

Choose a reason for hiding this comment

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

Let's make it typed and align naming (it's used not only in "pan" gestures)

Suggested change
internal const val CUPERTINO_PAN_GESTURE_SLOP_VALUE = 10
internal const val DefaultCupertinoTouchSlop = 10.dp

Copy link
Author

Choose a reason for hiding this comment

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

No DP here, new logic uses CGFloat(aka Double)

Copy link
Member

Choose a reason for hiding this comment

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

Just get raw value from dp there - we need to keep such things typed

Copy link
Author

@elijah-semyonov elijah-semyonov Jul 12, 2024

Choose a reason for hiding this comment

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

It doesn't work well here.
Dp is not primitive - turns into just val instead of const val.
Let's leave it as it is. Renamed to CUPERTINO_TOUCH_SLOP. It's not Default, it's what it is.

// 150ms is a timer delay for notifying a handler that the gesture was failed to recognize.
// `handler` implementtion is responsible for cancelling this via calling `cancelFailure` and transitioning
// this gesture recognizer to a proper state.
double failureInterval = 0.15;
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: mixing ms and s

Copy link
Author

Choose a reason for hiding this comment

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

Nitpick indeed

elijah-semyonov and others added 7 commits July 12, 2024 11:10
…tils/CMPGestureRecognizer.m

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>
…InteractionUIView.uikit.kt

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>
…InteractionUIView.uikit.kt

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>
@elijah-semyonov elijah-semyonov merged commit 174766b into jb-main Jul 12, 2024
6 checks passed
@elijah-semyonov elijah-semyonov deleted the es/interop-gestures-improvements branch July 12, 2024 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants