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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2e7f94e
Move state logic to Kotlin
elijah-semyonov Jul 9, 2024
0df0c23
Setup golden path
elijah-semyonov Jul 9, 2024
8b0a412
Setup golden path
elijah-semyonov Jul 9, 2024
200df35
Manually stop tracking when failing
elijah-semyonov Jul 9, 2024
f4f0ebd
Minor refactor
elijah-semyonov Jul 9, 2024
3554031
Minor refactor
elijah-semyonov Jul 9, 2024
0f6c275
Setup failure scheduling
elijah-semyonov Jul 10, 2024
5740501
Add doc
elijah-semyonov Jul 10, 2024
f286942
Minor change
elijah-semyonov Jul 10, 2024
c0e22b8
Minor change
elijah-semyonov Jul 10, 2024
f5bfacc
Implement everything
elijah-semyonov Jul 11, 2024
b5b672d
Remove prints
elijah-semyonov Jul 11, 2024
f97fc46
Remove prints
elijah-semyonov Jul 11, 2024
1678443
Remove prints
elijah-semyonov Jul 11, 2024
3a08659
Remove redundant import
elijah-semyonov Jul 11, 2024
733fb01
Remove comment
elijah-semyonov Jul 11, 2024
40d8d03
Remove debug prints
elijah-semyonov Jul 12, 2024
288b3f9
Remove spaces
elijah-semyonov Jul 12, 2024
82dde9f
Add comment
elijah-semyonov Jul 12, 2024
cc88ed0
Change the link
elijah-semyonov Jul 12, 2024
d5caf40
Move expressions for consistency
elijah-semyonov Jul 12, 2024
9c9fa41
Rename for clarity
elijah-semyonov Jul 12, 2024
f2ebb8f
Extract CUPERTINO_PAN_GESTURE_SLOP_VALUE
elijah-semyonov Jul 12, 2024
325473f
Change name
elijah-semyonov Jul 12, 2024
65338d0
Update compose/ui/ui-uikit/src/uikitMain/objc/CMPUIKitUtils/CMPUIKitU…
elijah-semyonov Jul 12, 2024
65b8397
Rename
elijah-semyonov Jul 12, 2024
ced8856
Modify
elijah-semyonov Jul 12, 2024
7d59be2
Update compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/…
elijah-semyonov Jul 12, 2024
db4cc40
Update compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/…
elijah-semyonov Jul 12, 2024
ac5a031
Rename
elijah-semyonov Jul 12, 2024
92ea0a3
Fix typo
elijah-semyonov Jul 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ NS_ASSUME_NONNULL_BEGIN
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent * _Nullable)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent * _Nullable)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent * _Nullable)event;
- (BOOL)shouldRecognizeSimultaneously:(UIGestureRecognizer *)first withOther:(UIGestureRecognizer *)second;
- (void)onFailure;

@end

@interface CMPGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>

@property (weak, nonatomic) id <CMPGestureRecognizerHandler> handler;

- (void)cancelFailure;
- (void)scheduleFailure;

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -7,82 +7,100 @@

#import "CMPGestureRecognizer.h"

@implementation CMPGestureRecognizer
@implementation CMPGestureRecognizer {
dispatch_block_t _scheduledFailureBlock;
}

- (instancetype)init {
self = [super init];

if (self) {
self.cancelsTouchesInView = false;
if (self) {
self.delegate = self;
[self addTarget:self action:@selector(handleStateChange)];
}

return self;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
id <CMPGestureRecognizerHandler> handler = self.handler;
- (void)handleStateChange {
switch (self.state) {
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:
[self cancelFailure];
break;

default:
break;
}
}

- (BOOL)shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
UIView *view = self.view;
UIView *otherView = otherGestureRecognizer.view;

if (handler) {
return [handler shouldRecognizeSimultaneously:gestureRecognizer withOther:otherGestureRecognizer];
} else {
if (view == nil || otherView == nil) {
return NO;
}

// Allow simultaneous recognition only if otherGestureRecognizer is attached to the view up in the hierarchy
return ![otherView isDescendantOfView:view];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.handler touchesBegan:touches withEvent:event];
- (BOOL)shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return NO;
}

- (BOOL)shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}

- (void)cancelFailure {
if (_scheduledFailureBlock) {
dispatch_block_cancel(_scheduledFailureBlock);
_scheduledFailureBlock = NULL;
}
}

- (void)fail {
[self.handler onFailure];
}

- (void)scheduleFailure {
__weak typeof(self) weakSelf = self;
dispatch_block_t dispatchBlock = dispatch_block_create(0, ^{
[weakSelf fail];
});

if (self.state == UIGestureRecognizerStatePossible) {
self.state = UIGestureRecognizerStateBegan;
if (_scheduledFailureBlock) {
dispatch_block_cancel(_scheduledFailureBlock);
}
_scheduledFailureBlock = dispatchBlock;

// 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 failureDelay = 0.15;

dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(failureDelay * NSEC_PER_SEC));

// Schedule the block to be executed at `dispatchTime`
dispatch_after(dispatchTime, dispatch_get_main_queue(), dispatchBlock);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.handler touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.handler touchesMoved:touches withEvent:event];

switch (self.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
self.state = UIGestureRecognizerStateChanged;
break;
default:
break;
}
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.handler touchesEnded:touches withEvent:event];

switch (self.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
if (self.numberOfTouches == 0) {
self.state = UIGestureRecognizerStateEnded;
} else {
self.state = UIGestureRecognizerStateChanged;
}
break;
default:
break;
}
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.handler touchesCancelled:touches withEvent:event];

switch (self.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
if (self.numberOfTouches == 0) {
self.state = UIGestureRecognizerStateCancelled;
} else {
self.state = UIGestureRecognizerStateChanged;
}
break;
default:
break;
}
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.ui.platform

/**
* iOS default value in scale-independent points for touch slop that recognizes as scroll/pan gesture.
*/
internal const val CUPERTINO_TOUCH_SLOP = 10
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import androidx.compose.ui.interop.UIKitInteropContainer
import androidx.compose.ui.node.TrackInteropContainer
import androidx.compose.ui.platform.AccessibilityMediator
import androidx.compose.ui.platform.AccessibilitySyncOptions
import androidx.compose.ui.platform.CUPERTINO_TOUCH_SLOP
import androidx.compose.ui.platform.DefaultInputModeManager
import androidx.compose.ui.platform.EmptyViewConfiguration
import androidx.compose.ui.platform.LocalLayoutMargins
Expand Down Expand Up @@ -87,6 +88,7 @@ import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGRectZero
import platform.CoreGraphics.CGSize
import platform.QuartzCore.CACurrentMediaTime
import platform.QuartzCore.CATransaction
import platform.UIKit.NSLayoutConstraint
import platform.UIKit.UIEvent
Expand Down Expand Up @@ -229,7 +231,7 @@ internal class ComposeSceneMediator(
override val touchSlop: Float
get() = with(density) {
// this value is originating from iOS 16 drag behavior reverse engineering
10.dp.toPx()
CUPERTINO_TOUCH_SLOP.dp.toPx()
}
}

Expand Down Expand Up @@ -367,29 +369,41 @@ internal class ComposeSceneMediator(
* @param event the [UIEvent] associated with the touches
* @param phase the [CupertinoTouchesPhase] of the touches
*/
private fun onTouchesEvent(view: UIView, touches: Set<*>, event: UIEvent, phase: CupertinoTouchesPhase) {
private fun onTouchesEvent(view: UIView, touches: Set<*>, event: UIEvent?, phase: CupertinoTouchesPhase) {
val pointers = touches.map {
val touch = it as UITouch
val id = touch.hashCode().toLong()
val position = touch.offsetInView(view, density.density)
ComposeScenePointer(
id = PointerId(id),
position = position,
pressed = touch.isPressed,
pressed = when (phase) {
// When CMPGestureRecognizer is failed, all tracked touches are sent immediately
// as CANCELLED. In this case, we should not consider the touch as pressed
// despite them being on the screen. This is the last event for Compose in a
// given gesture sequence and should be treated as such.
CupertinoTouchesPhase.CANCELLED -> false
else -> touch.isPressed
},
type = PointerType.Touch,
pressure = touch.force.toFloat(),
historical = event.historicalChangesForTouch(
historical = event?.historicalChangesForTouch(
touch,
view,
density.density
)
) ?: emptyList()
)
} ?: emptyList()
}

// If the touches were cancelled due to gesture failure, the timestamp is not available,
// because no actual event with touch updates happened. We just use the current time in
// this case.
val timestamp = event?.timestamp ?: CACurrentMediaTime()

scene.sendPointerEvent(
eventType = phase.toPointerEventType(),
pointers = pointers,
timeMillis = (event.timestamp * 1e3).toLong(),
timeMillis = (timestamp * 1e3).toLong(),
nativeEvent = event
)
}
Expand Down
Loading