mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Scroll inertia cancel for iPadOS (flutter/engine#34929)
This commit is contained in:
parent
c5482e1b56
commit
4713e320b5
@ -114,6 +114,15 @@ typedef struct MouseState {
|
||||
fml::scoped_nsobject<UIScrollView> _scrollView;
|
||||
fml::scoped_nsobject<UIView> _keyboardAnimationView;
|
||||
MouseState _mouseState;
|
||||
// Timestamp after which a scroll inertia cancel event should be inferred.
|
||||
NSTimeInterval _scrollInertiaEventStartline;
|
||||
// When an iOS app is running in emulation on an Apple Silicon Mac, trackpad input goes through
|
||||
// a translation layer, and events are not received with precise deltas. Due to this, we can't
|
||||
// rely on checking for a stationary trackpad event. Fortunately, AppKit will send an event of
|
||||
// type UIEventTypeScroll following a scroll when inertia should stop. This field is needed to
|
||||
// estimate if such an event represents the natural end of scrolling inertia or a user-initiated
|
||||
// cancellation.
|
||||
NSTimeInterval _scrollInertiaEventAppKitDeadline;
|
||||
}
|
||||
|
||||
@synthesize displayingFlutterUI = _displayingFlutterUI;
|
||||
@ -1832,9 +1841,33 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
|
||||
shouldReceiveEvent:(UIEvent*)event API_AVAILABLE(ios(13.4)) {
|
||||
if (gestureRecognizer == _continuousScrollingPanGestureRecognizer &&
|
||||
event.type == UIEventTypeScroll) {
|
||||
// Events with type UIEventTypeScroll are only received when running on macOS under emulation.
|
||||
flutter::PointerData pointer_data = [self generatePointerDataAtLastMouseLocation];
|
||||
pointer_data.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
|
||||
pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
|
||||
pointer_data.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
|
||||
|
||||
if (event.timestamp < _scrollInertiaEventAppKitDeadline) {
|
||||
// Only send the event if it occured before the expected natural end of gesture momentum.
|
||||
// If received after the deadline, it's not likely the event is from a user-initiated cancel.
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
_scrollInertiaEventAppKitDeadline = 0;
|
||||
}
|
||||
}
|
||||
// This method is also called for UITouches, should return YES to process all touches.
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)hoverEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
CGPoint location = [recognizer locationInView:self.view];
|
||||
CGFloat scale = [UIScreen mainScreen].scale;
|
||||
CGPoint oldLocation = _mouseState.location;
|
||||
_mouseState.location = {location.x * scale, location.y * scale};
|
||||
|
||||
flutter::PointerData pointer_data = [self generatePointerDataAtLastMouseLocation];
|
||||
@ -1859,9 +1892,33 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
break;
|
||||
}
|
||||
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
NSTimeInterval time = [NSProcessInfo processInfo].systemUptime;
|
||||
BOOL isRunningOnMac = NO;
|
||||
if (@available(iOS 14.0, *)) {
|
||||
// This "stationary pointer" heuristic is not reliable when running within macOS.
|
||||
// We instead receive a scroll cancel event directly from AppKit.
|
||||
// See gestureRecognizer:shouldReceiveEvent:
|
||||
isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac;
|
||||
}
|
||||
if (!isRunningOnMac && CGPointEqualToPoint(oldLocation, _mouseState.location) &&
|
||||
time > _scrollInertiaEventStartline) {
|
||||
// iPadOS reports trackpad movements events with high (sub-pixel) precision. When an event
|
||||
// is received with the same position as the previous one, it can only be from a finger
|
||||
// making or breaking contact with the trackpad surface.
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(2);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
flutter::PointerData inertia_cancel = pointer_data;
|
||||
inertia_cancel.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
|
||||
inertia_cancel.kind = flutter::PointerData::DeviceKind::kTrackpad;
|
||||
inertia_cancel.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
|
||||
packet->SetPointerData(/*index=*/1, inertia_cancel);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
_scrollInertiaEventStartline = DBL_MAX;
|
||||
} else {
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
@ -1914,6 +1971,22 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
_scrollInertiaEventStartline =
|
||||
[[NSProcessInfo processInfo] systemUptime] +
|
||||
0.1; // Time to lift fingers off trackpad (experimentally determined)
|
||||
// When running an iOS app on an Apple Silicon Mac, AppKit will send an event
|
||||
// of type UIEventTypeScroll when trackpad scroll momentum has ended. This event
|
||||
// is sent whether the momentum ended normally or was cancelled by a trackpad touch.
|
||||
// Since Flutter scrolling inertia will likely not match the system inertia, we should
|
||||
// only send a PointerScrollInertiaCancel event for user-initiated cancellations.
|
||||
// The following (curve-fitted) calculation provides a cutoff point after which any
|
||||
// UIEventTypeScroll event will likely be from the system instead of the user.
|
||||
// See https://github.com/flutter/engine/pull/34929.
|
||||
_scrollInertiaEventAppKitDeadline =
|
||||
[[NSProcessInfo processInfo] systemUptime] +
|
||||
(0.1821 * log(fmax([recognizer velocityInView:self.view].x,
|
||||
[recognizer velocityInView:self.view].y))) -
|
||||
0.4825;
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
|
||||
break;
|
||||
default:
|
||||
|
||||
@ -31,14 +31,18 @@
|
||||
}
|
||||
|
||||
XCUIElement* addTextField =
|
||||
self.application.textFields[@"0,PointerChange.add,device=0,buttons=0"];
|
||||
self.application
|
||||
.textFields[@"0,PointerChange.add,device=0,buttons=0,signalKind=PointerSignalKind.none"];
|
||||
BOOL exists = [addTextField waitForExistenceWithTimeout:1];
|
||||
XCTAssertTrue(exists, @"");
|
||||
XCUIElement* downTextField =
|
||||
self.application.textFields[@"1,PointerChange.down,device=0,buttons=0"];
|
||||
self.application
|
||||
.textFields[@"1,PointerChange.down,device=0,buttons=0,signalKind=PointerSignalKind.none"];
|
||||
exists = [downTextField waitForExistenceWithTimeout:1];
|
||||
XCTAssertTrue(exists, @"");
|
||||
XCUIElement* upTextField = self.application.textFields[@"2,PointerChange.up,device=0,buttons=0"];
|
||||
XCUIElement* upTextField =
|
||||
self.application
|
||||
.textFields[@"2,PointerChange.up,device=0,buttons=0,signalKind=PointerSignalKind.none"];
|
||||
exists = [upTextField waitForExistenceWithTimeout:1];
|
||||
XCTAssertTrue(exists, @"");
|
||||
}
|
||||
|
||||
@ -236,6 +236,12 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
[invocation setArgument:&deltaY atIndex:3];
|
||||
[invocation invokeWithTarget:flutterView];
|
||||
|
||||
// Hover immediately following the scroll to cause an inertia cancel event.
|
||||
SEL hover = @selector(hover:);
|
||||
XCTAssertTrue([flutterView respondsToSelector:hover],
|
||||
@"If supportsPointerInteraction is true, this should be true too.");
|
||||
[flutterView performSelector:hover];
|
||||
|
||||
// The hover pointer is observed to be removed by the system after ~3.5 seconds of inactivity.
|
||||
// While this is not a documented behavior, it is the only way to test for the removal of the
|
||||
// hover pointer. Waiting for 5 seconds will ensure that all events are received before
|
||||
@ -265,21 +271,24 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
[messageSequenceNumberList addObject:@(messageSequenceNumber)];
|
||||
}
|
||||
// The number of hover events is not consistent, there could be one or many.
|
||||
int hoverAddedSequenceNumber =
|
||||
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.add,device=0,buttons=0");
|
||||
int hoverAddedSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.add,device=0,buttons=0,signalKind=PointerSignalKind.none");
|
||||
NSMutableArray<NSNumber*>* hoverSequenceNumbers =
|
||||
messages[@"PointerChange.hover,device=0,buttons=0"];
|
||||
int hoverRemovedSequenceNumber =
|
||||
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.remove,device=0,buttons=0");
|
||||
int panZoomAddedSequenceNumber =
|
||||
assertOneMessageAndGetSequenceNumber(messages, @"PointerChange.add,device=1,buttons=0");
|
||||
messages[@"PointerChange.hover,device=0,buttons=0,signalKind=PointerSignalKind.none"];
|
||||
int hoverRemovedSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.remove,device=0,buttons=0,signalKind=PointerSignalKind.none");
|
||||
int panZoomAddedSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.add,device=1,buttons=0,signalKind=PointerSignalKind.none");
|
||||
int panZoomStartSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.panZoomStart,device=1,buttons=0");
|
||||
messages, @"PointerChange.panZoomStart,device=1,buttons=0,signalKind=PointerSignalKind.none");
|
||||
// The number of pan/zoom update events is not consistent, there could be one or many.
|
||||
NSMutableArray<NSNumber*>* panZoomUpdateSequenceNumbers =
|
||||
messages[@"PointerChange.panZoomUpdate,device=1,buttons=0"];
|
||||
messages[@"PointerChange.panZoomUpdate,device=1,buttons=0,signalKind=PointerSignalKind.none"];
|
||||
int panZoomEndSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.panZoomEnd,device=1,buttons=0");
|
||||
messages, @"PointerChange.panZoomEnd,device=1,buttons=0,signalKind=PointerSignalKind.none");
|
||||
int inertiaCancelSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages,
|
||||
@"PointerChange.cancel,device=2,buttons=0,signalKind=PointerSignalKind.scrollInertiaCancel");
|
||||
|
||||
XCTAssertGreaterThan(panZoomStartSequenceNumber, panZoomAddedSequenceNumber,
|
||||
@"PanZoomStart occured before pointer was added");
|
||||
@ -287,7 +296,9 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
panZoomStartSequenceNumber, @"PanZoomUpdate occured before PanZoomStart");
|
||||
XCTAssertGreaterThan(panZoomEndSequenceNumber,
|
||||
[[panZoomUpdateSequenceNumbers lastObject] intValue],
|
||||
@"PanZoomUpdate occured after PanZoomUpdate");
|
||||
@"PanZoomUpdate occured after PanZoomEnd");
|
||||
XCTAssertGreaterThan(inertiaCancelSequenceNumber, panZoomEndSequenceNumber,
|
||||
@"ScrollInertiaCancel occured before PanZoomEnd");
|
||||
|
||||
XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], hoverAddedSequenceNumber,
|
||||
@"Hover occured before pointer was added");
|
||||
|
||||
@ -32,7 +32,7 @@ class TouchesScenario extends Scenario {
|
||||
dispatcher: dispatcher,
|
||||
channel: 'display_data',
|
||||
json: <String, dynamic>{
|
||||
'data': '$_sequenceNo,${datum.change},device=$deviceId,buttons=${datum.buttons}',
|
||||
'data': '$_sequenceNo,${datum.change},device=$deviceId,buttons=${datum.buttons},signalKind=${datum.signalKind}',
|
||||
},
|
||||
);
|
||||
_sequenceNo++;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user