mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
iPad trackpad gestures (flutter/engine#31591)
This commit is contained in:
parent
ad0b442030
commit
20d8f266ac
@ -300,9 +300,21 @@ void PointerDataPacketConverter::ConvertPointerData(
|
||||
case PointerData::SignalKind::kScroll: {
|
||||
// Makes sure we have an existing pointer
|
||||
auto iter = states_.find(pointer_data.device);
|
||||
FML_DCHECK(iter != states_.end());
|
||||
PointerState state;
|
||||
|
||||
if (iter == states_.end()) {
|
||||
// Synthesizes a add event if the pointer is not previously added.
|
||||
PointerData synthesized_add_event = pointer_data;
|
||||
synthesized_add_event.signal_kind = PointerData::SignalKind::kNone;
|
||||
synthesized_add_event.change = PointerData::Change::kAdd;
|
||||
synthesized_add_event.synthesized = 1;
|
||||
synthesized_add_event.buttons = 0;
|
||||
state = EnsurePointerState(synthesized_add_event);
|
||||
converted_pointers.push_back(synthesized_add_event);
|
||||
} else {
|
||||
state = iter->second;
|
||||
}
|
||||
|
||||
PointerState state = iter->second;
|
||||
if (LocationNeedsUpdate(pointer_data, state)) {
|
||||
if (state.is_down) {
|
||||
// Synthesizes a move event if the pointer is down.
|
||||
|
||||
@ -556,7 +556,7 @@ TEST(PointerDataPacketConverterTest, CanHandleThreeFingerGesture) {
|
||||
|
||||
TEST(PointerDataPacketConverterTest, CanConvetScroll) {
|
||||
PointerDataPacketConverter converter;
|
||||
auto packet = std::make_unique<PointerDataPacket>(5);
|
||||
auto packet = std::make_unique<PointerDataPacket>(6);
|
||||
PointerData data;
|
||||
CreateSimulatedMousePointerData(data, PointerData::Change::kAdd,
|
||||
PointerData::SignalKind::kNone, 0, 0.0, 0.0,
|
||||
@ -578,12 +578,16 @@ TEST(PointerDataPacketConverterTest, CanConvetScroll) {
|
||||
PointerData::SignalKind::kScroll, 1, 49.0,
|
||||
49.0, 50.0, 0.0, 0);
|
||||
packet->SetPointerData(4, data);
|
||||
CreateSimulatedMousePointerData(data, PointerData::Change::kHover,
|
||||
PointerData::SignalKind::kScroll, 2, 10.0,
|
||||
20.0, 30.0, 40.0, 0);
|
||||
packet->SetPointerData(5, data);
|
||||
auto converted_packet = converter.Convert(std::move(packet));
|
||||
|
||||
std::vector<PointerData> result;
|
||||
UnpackPointerPacket(result, std::move(converted_packet));
|
||||
|
||||
ASSERT_EQ(result.size(), (size_t)7);
|
||||
ASSERT_EQ(result.size(), (size_t)9);
|
||||
ASSERT_EQ(result[0].change, PointerData::Change::kAdd);
|
||||
ASSERT_EQ(result[0].signal_kind, PointerData::SignalKind::kNone);
|
||||
ASSERT_EQ(result[0].device, 0);
|
||||
@ -642,6 +646,22 @@ TEST(PointerDataPacketConverterTest, CanConvetScroll) {
|
||||
ASSERT_EQ(result[6].physical_y, 49.0);
|
||||
ASSERT_EQ(result[6].scroll_delta_x, 50.0);
|
||||
ASSERT_EQ(result[6].scroll_delta_y, 0.0);
|
||||
|
||||
// Converter will synthesize an add for device 2.
|
||||
ASSERT_EQ(result[7].change, PointerData::Change::kAdd);
|
||||
ASSERT_EQ(result[7].signal_kind, PointerData::SignalKind::kNone);
|
||||
ASSERT_EQ(result[7].device, 2);
|
||||
ASSERT_EQ(result[7].physical_x, 10.0);
|
||||
ASSERT_EQ(result[7].physical_y, 20.0);
|
||||
ASSERT_EQ(result[7].synthesized, 1);
|
||||
|
||||
ASSERT_EQ(result[8].change, PointerData::Change::kHover);
|
||||
ASSERT_EQ(result[8].signal_kind, PointerData::SignalKind::kScroll);
|
||||
ASSERT_EQ(result[8].device, 2);
|
||||
ASSERT_EQ(result[8].physical_x, 10.0);
|
||||
ASSERT_EQ(result[8].physical_y, 20.0);
|
||||
ASSERT_EQ(result[8].scroll_delta_x, 30.0);
|
||||
ASSERT_EQ(result[8].scroll_delta_y, 40.0);
|
||||
}
|
||||
|
||||
TEST(PointerDataPacketConverterTest, CanConvertTrackpadGesture) {
|
||||
|
||||
@ -41,7 +41,7 @@ NSNotificationName const FlutterViewControllerHideHomeIndicator =
|
||||
NSNotificationName const FlutterViewControllerShowHomeIndicator =
|
||||
@"FlutterViewControllerShowHomeIndicator";
|
||||
|
||||
// Struct holding the mouse state.
|
||||
// Struct holding data to help adapt system mouse/trackpad events to embedder events.
|
||||
typedef struct MouseState {
|
||||
// Current coordinate of the mouse cursor in physical device pixels.
|
||||
CGPoint location = CGPointZero;
|
||||
@ -64,6 +64,25 @@ typedef struct MouseState {
|
||||
@property(nonatomic, assign) double targetViewInsetBottom;
|
||||
@property(nonatomic, retain) CADisplayLink* displayLink;
|
||||
|
||||
/*
|
||||
* Mouse and trackpad gesture recognizers
|
||||
*/
|
||||
// Mouse and trackpad hover
|
||||
@property(nonatomic, retain)
|
||||
UIHoverGestureRecognizer* hoverGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
// Mouse wheel scrolling
|
||||
@property(nonatomic, retain)
|
||||
UIPanGestureRecognizer* discreteScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
// Trackpad and Magic Mouse scrolling
|
||||
@property(nonatomic, retain)
|
||||
UIPanGestureRecognizer* continuousScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
// Trackpad pinching
|
||||
@property(nonatomic, retain)
|
||||
UIPinchGestureRecognizer* pinchGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
// Trackpad rotating
|
||||
@property(nonatomic, retain)
|
||||
UIRotationGestureRecognizer* rotationGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
|
||||
/**
|
||||
* Creates and registers plugins used by this view controller.
|
||||
*/
|
||||
@ -106,8 +125,6 @@ typedef enum UIAccessibilityContrast : NSInteger {
|
||||
// UIScrollView with height zero and a content offset so we can get those events. See also:
|
||||
// https://github.com/flutter/flutter/issues/35050
|
||||
fml::scoped_nsobject<UIScrollView> _scrollView;
|
||||
fml::scoped_nsobject<UIHoverGestureRecognizer> _hoverGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
fml::scoped_nsobject<UIPanGestureRecognizer> _panGestureRecognizer API_AVAILABLE(ios(13.4));
|
||||
fml::scoped_nsobject<UIView> _keyboardAnimationView;
|
||||
MouseState _mouseState;
|
||||
}
|
||||
@ -669,17 +686,37 @@ static void SendFakeTouchEvent(FlutterEngine* engine,
|
||||
}
|
||||
|
||||
if (@available(iOS 13.4, *)) {
|
||||
_hoverGestureRecognizer.reset(
|
||||
[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)]);
|
||||
_hoverGestureRecognizer.get().delegate = self;
|
||||
[_flutterView.get() addGestureRecognizer:_hoverGestureRecognizer.get()];
|
||||
_hoverGestureRecognizer =
|
||||
[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
|
||||
_hoverGestureRecognizer.delegate = self;
|
||||
[_flutterView.get() addGestureRecognizer:_hoverGestureRecognizer];
|
||||
|
||||
_panGestureRecognizer.reset(
|
||||
[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(scrollEvent:)]);
|
||||
_panGestureRecognizer.get().delegate = self;
|
||||
_panGestureRecognizer.get().allowedScrollTypesMask = UIScrollTypeMaskAll;
|
||||
_panGestureRecognizer.get().allowedTouchTypes = @[ @(UITouchTypeIndirectPointer) ];
|
||||
[_flutterView.get() addGestureRecognizer:_panGestureRecognizer.get()];
|
||||
_discreteScrollingPanGestureRecognizer =
|
||||
[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)];
|
||||
_discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
|
||||
// Disallowing all touch types. If touch events are allowed here, touches to the screen will be
|
||||
// consumed by the UIGestureRecognizer instead of being passed through to flutter via
|
||||
// touchesBegan. Trackpad and mouse scrolls are sent by the platform as scroll events rather
|
||||
// than touch events, so they will still be received.
|
||||
_discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[];
|
||||
_discreteScrollingPanGestureRecognizer.delegate = self;
|
||||
[_flutterView.get() addGestureRecognizer:_discreteScrollingPanGestureRecognizer];
|
||||
_continuousScrollingPanGestureRecognizer =
|
||||
[[UIPanGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(continuousScrollEvent:)];
|
||||
_continuousScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
|
||||
_continuousScrollingPanGestureRecognizer.allowedTouchTypes = @[];
|
||||
_continuousScrollingPanGestureRecognizer.delegate = self;
|
||||
[_flutterView.get() addGestureRecognizer:_continuousScrollingPanGestureRecognizer];
|
||||
_pinchGestureRecognizer =
|
||||
[[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchEvent:)];
|
||||
_pinchGestureRecognizer.allowedTouchTypes = @[];
|
||||
_pinchGestureRecognizer.delegate = self;
|
||||
[_flutterView.get() addGestureRecognizer:_pinchGestureRecognizer];
|
||||
_rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] init];
|
||||
_rotationGestureRecognizer.allowedTouchTypes = @[];
|
||||
_rotationGestureRecognizer.delegate = self;
|
||||
[_flutterView.get() addGestureRecognizer:_rotationGestureRecognizer];
|
||||
}
|
||||
|
||||
[super viewDidLoad];
|
||||
@ -808,8 +845,16 @@ static void SendFakeTouchEvent(FlutterEngine* engine,
|
||||
|
||||
[_displayLink release];
|
||||
_scrollView.get().delegate = nil;
|
||||
_hoverGestureRecognizer.get().delegate = nil;
|
||||
_panGestureRecognizer.get().delegate = nil;
|
||||
_hoverGestureRecognizer.delegate = nil;
|
||||
[_hoverGestureRecognizer release];
|
||||
_discreteScrollingPanGestureRecognizer.delegate = nil;
|
||||
[_discreteScrollingPanGestureRecognizer release];
|
||||
_continuousScrollingPanGestureRecognizer.delegate = nil;
|
||||
[_continuousScrollingPanGestureRecognizer release];
|
||||
_pinchGestureRecognizer.delegate = nil;
|
||||
[_pinchGestureRecognizer release];
|
||||
_rotationGestureRecognizer.delegate = nil;
|
||||
[_rotationGestureRecognizer release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@ -911,8 +956,36 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
return;
|
||||
}
|
||||
|
||||
// If the UIApplicationSupportsIndirectInputEvents in Info.plist returns YES, then the platform
|
||||
// dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
|
||||
// UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
|
||||
// Flutter pointer events with type of kMouse and different device IDs. These devices must be
|
||||
// terminated with kRemove events when the touches end, otherwise they will keep triggering hover
|
||||
// events.
|
||||
//
|
||||
// If the UIApplicationSupportsIndirectInputEvents in Info.plist returns NO, then the platform
|
||||
// dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
|
||||
// UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
|
||||
// Flutter pointer events with type of kTouch and different device IDs. Removing these devices is
|
||||
// neither necessary nor harmful.
|
||||
//
|
||||
// Therefore Flutter always removes these devices. The touches_to_remove_count tracks how many
|
||||
// remove events are needed in this group of touches to properly allocate space for the packet.
|
||||
// The remove event of a touch is synthesized immediately after its normal event.
|
||||
//
|
||||
// See also:
|
||||
// https://developer.apple.com/documentation/uikit/pointer_interactions?language=objc
|
||||
// https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputevents?language=objc
|
||||
NSUInteger touches_to_remove_count = 0;
|
||||
for (UITouch* touch in touches) {
|
||||
if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
|
||||
touches_to_remove_count++;
|
||||
}
|
||||
}
|
||||
|
||||
const CGFloat scale = [UIScreen mainScreen].scale;
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(touches.count);
|
||||
auto packet =
|
||||
std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
|
||||
|
||||
size_t pointer_index = 0;
|
||||
|
||||
@ -1034,6 +1107,12 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
}
|
||||
|
||||
packet->SetPointerData(pointer_index++, pointer_data);
|
||||
|
||||
if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
|
||||
flutter::PointerData remove_pointer_data = pointer_data;
|
||||
remove_pointer_data.change = flutter::PointerData::Change::kRemove;
|
||||
packet->SetPointerData(pointer_index++, remove_pointer_data);
|
||||
}
|
||||
}
|
||||
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
@ -1756,13 +1835,31 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
|
||||
}
|
||||
|
||||
- (flutter::PointerData)generatePointerDataForMouse API_AVAILABLE(ios(13.4)) {
|
||||
- (flutter::PointerData)generatePointerDataAtLastMouseLocation API_AVAILABLE(ios(13.4)) {
|
||||
flutter::PointerData pointer_data;
|
||||
|
||||
pointer_data.Clear();
|
||||
pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
|
||||
pointer_data.physical_x = _mouseState.location.x;
|
||||
pointer_data.physical_y = _mouseState.location.y;
|
||||
return pointer_data;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
|
||||
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
|
||||
API_AVAILABLE(ios(13.4)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)hoverEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
CGPoint location = [recognizer locationInView:self.view];
|
||||
CGFloat scale = [UIScreen mainScreen].scale;
|
||||
_mouseState.location = {location.x * scale, location.y * scale};
|
||||
|
||||
flutter::PointerData pointer_data = [self generatePointerDataAtLastMouseLocation];
|
||||
pointer_data.device = reinterpret_cast<int64_t>(recognizer);
|
||||
pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
|
||||
switch (_hoverGestureRecognizer.get().state) {
|
||||
|
||||
switch (_hoverGestureRecognizer.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
pointer_data.change = flutter::PointerData::Change::kAdd;
|
||||
break;
|
||||
@ -1779,45 +1876,22 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
pointer_data.change = flutter::PointerData::Change::kHover;
|
||||
break;
|
||||
}
|
||||
pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
|
||||
pointer_data.device = reinterpret_cast<int64_t>(_hoverGestureRecognizer.get());
|
||||
|
||||
pointer_data.physical_x = _mouseState.location.x;
|
||||
pointer_data.physical_y = _mouseState.location.y;
|
||||
|
||||
return pointer_data;
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
|
||||
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
|
||||
API_AVAILABLE(ios(13.4)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)hoverEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
CGPoint location = [recognizer locationInView:self.view];
|
||||
CGFloat scale = [UIScreen mainScreen].scale;
|
||||
_mouseState.location = {location.x * scale, location.y * scale};
|
||||
|
||||
flutter::PointerData pointer_data = [self generatePointerDataForMouse];
|
||||
|
||||
pointer_data.signal_kind = flutter::PointerData::SignalKind::kNone;
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
}
|
||||
|
||||
- (void)scrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
- (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
CGPoint translation = [recognizer translationInView:self.view];
|
||||
const CGFloat scale = [UIScreen mainScreen].scale;
|
||||
|
||||
translation.x *= scale;
|
||||
translation.y *= scale;
|
||||
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
|
||||
flutter::PointerData pointer_data = [self generatePointerDataForMouse];
|
||||
flutter::PointerData pointer_data = [self generatePointerDataAtLastMouseLocation];
|
||||
pointer_data.device = reinterpret_cast<int64_t>(recognizer);
|
||||
pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
|
||||
pointer_data.signal_kind = flutter::PointerData::SignalKind::kScroll;
|
||||
pointer_data.scroll_delta_x = (translation.x - _mouseState.last_translation.x);
|
||||
pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y);
|
||||
@ -1832,8 +1906,72 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch)
|
||||
_mouseState.last_translation = CGPointZero;
|
||||
}
|
||||
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
}
|
||||
|
||||
- (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
CGPoint translation = [recognizer translationInView:self.view];
|
||||
const CGFloat scale = [UIScreen mainScreen].scale;
|
||||
|
||||
flutter::PointerData pointer_data = [self generatePointerDataAtLastMouseLocation];
|
||||
pointer_data.device = reinterpret_cast<int64_t>(recognizer);
|
||||
pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
|
||||
switch (recognizer.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
|
||||
break;
|
||||
case UIGestureRecognizerStateChanged:
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
|
||||
pointer_data.pan_x = translation.x * scale;
|
||||
pointer_data.pan_y = translation.y * scale;
|
||||
pointer_data.pan_delta_x = 0; // Delta will be generated in pointer_data_packet_converter.cc.
|
||||
pointer_data.pan_delta_y = 0; // Delta will be generated in pointer_data_packet_converter.cc.
|
||||
pointer_data.scale = 1;
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
|
||||
break;
|
||||
default:
|
||||
// continuousScrollEvent: should only ever be triggered with the above phases
|
||||
NSAssert(false, @"Trackpad pan event occured with unexpected phase 0x%lx",
|
||||
(long)recognizer.state);
|
||||
break;
|
||||
}
|
||||
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
}
|
||||
|
||||
- (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
|
||||
flutter::PointerData pointer_data = [self generatePointerDataAtLastMouseLocation];
|
||||
pointer_data.device = reinterpret_cast<int64_t>(recognizer);
|
||||
pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
|
||||
switch (recognizer.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
|
||||
break;
|
||||
case UIGestureRecognizerStateChanged:
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
|
||||
pointer_data.scale = recognizer.scale;
|
||||
pointer_data.rotation = _rotationGestureRecognizer.rotation;
|
||||
break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
|
||||
break;
|
||||
default:
|
||||
// pinchEvent: should only ever be triggered with the above phases
|
||||
NSAssert(false, @"Trackpad pinch event occured with unexpected phase 0x%lx",
|
||||
(long)recognizer.state);
|
||||
break;
|
||||
}
|
||||
|
||||
auto packet = std::make_unique<flutter::PointerDataPacket>(1);
|
||||
packet->SetPointerData(/*index=*/0, pointer_data);
|
||||
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
|
||||
}
|
||||
|
||||
|
||||
@ -132,7 +132,7 @@ typedef enum UIAccessibilityContrast : NSInteger {
|
||||
- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
|
||||
- (void)handlePressEvent:(FlutterUIPressProxy*)press
|
||||
nextAction:(void (^)())next API_AVAILABLE(ios(13.4));
|
||||
- (void)scrollEvent:(UIPanGestureRecognizer*)recognizer;
|
||||
- (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer;
|
||||
- (void)updateViewportMetrics;
|
||||
- (void)onUserSettingsChanged:(NSNotification*)notification;
|
||||
- (void)applicationWillTerminate:(NSNotification*)notification;
|
||||
@ -1156,7 +1156,7 @@ typedef enum UIAccessibilityContrast : NSInteger {
|
||||
id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]);
|
||||
XCTAssertNotNil(mockPanGestureRecognizer);
|
||||
|
||||
[vc scrollEvent:mockPanGestureRecognizer];
|
||||
[vc discreteScrollEvent:mockPanGestureRecognizer];
|
||||
|
||||
[[[self.mockEngine verify] ignoringNonObjectArgs]
|
||||
dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>(0)];
|
||||
|
||||
@ -75,13 +75,16 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
XCTAssertTrue(
|
||||
[app.textFields[@"2,PointerChange.up,device=0,buttons=0"] waitForExistenceWithTimeout:1],
|
||||
@"PointerChange.up event did not occur for a normal tap");
|
||||
XCTAssertTrue(
|
||||
[app.textFields[@"3,PointerChange.remove,device=0,buttons=0"] waitForExistenceWithTimeout:1],
|
||||
@"PointerChange.remove event did not occur for a normal tap");
|
||||
SEL rightClick = @selector(rightClick);
|
||||
XCTAssertTrue([flutterView respondsToSelector:rightClick],
|
||||
@"If supportsPointerInteraction is true, this should be true too.");
|
||||
[flutterView performSelector:rightClick];
|
||||
// On simulated right click, a hover also occurs, so the hover pointer is added
|
||||
XCTAssertTrue(
|
||||
[app.textFields[@"3,PointerChange.add,device=1,buttons=0"] waitForExistenceWithTimeout:1],
|
||||
[app.textFields[@"4,PointerChange.add,device=1,buttons=0"] waitForExistenceWithTimeout:1],
|
||||
@"PointerChange.add event did not occur for a right-click's hover pointer");
|
||||
|
||||
// The hover pointer is removed after ~3.5 seconds, this ensures that all events are received
|
||||
@ -89,8 +92,8 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
sleepExpectation.inverted = true;
|
||||
[self waitForExpectations:@[ sleepExpectation ] timeout:5.0];
|
||||
|
||||
// The hover events are interspersed with the right-click events in a varying order
|
||||
// Ensure the individual orderings are respected without hardcoding the absolute sequence
|
||||
// The hover events are interspersed with the right-click events in a varying order.
|
||||
// Ensure the individual orderings are respected without hardcoding the absolute sequence.
|
||||
NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* messages =
|
||||
[[NSMutableDictionary alloc] init];
|
||||
for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
|
||||
@ -109,7 +112,7 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
}
|
||||
[messageSequenceNumberList addObject:@(messageSequenceNumber)];
|
||||
}
|
||||
// The number of hover events is not consistent, there could be one or many
|
||||
// The number of hover events is not consistent, there could be one or many.
|
||||
NSMutableArray<NSNumber*>* hoverSequenceNumbers =
|
||||
messages[@"PointerChange.hover,device=1,buttons=0"];
|
||||
int hoverRemovedSequenceNumber =
|
||||
@ -137,7 +140,7 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
@"Right-click pointer was pressed before it was added");
|
||||
XCTAssertGreaterThan(rightClickUpSequenceNumber, rightClickDownSequenceNumber,
|
||||
@"Right-click pointer was released before it was pressed");
|
||||
XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], 3,
|
||||
XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], 4,
|
||||
@"Hover occured before hover pointer was added");
|
||||
XCTAssertGreaterThan(hoverRemovedSequenceNumber, [[hoverSequenceNumbers lastObject] intValue],
|
||||
@"Hover occured after hover pointer was removed");
|
||||
@ -196,6 +199,101 @@ static int assertOneMessageAndGetSequenceNumber(NSMutableDictionary* messages, N
|
||||
XCTAssertTrue([app.textFields[removeMessage] waitForExistenceWithTimeout:1],
|
||||
@"PointerChange.remove event did not occur for a hover");
|
||||
}
|
||||
|
||||
- (void)testPointerScroll {
|
||||
BOOL supportsPointerInteraction = NO;
|
||||
SEL supportsPointerInteractionSelector = @selector(supportsPointerInteraction);
|
||||
if ([XCUIDevice.sharedDevice respondsToSelector:supportsPointerInteractionSelector]) {
|
||||
supportsPointerInteraction =
|
||||
performBoolSelector(XCUIDevice.sharedDevice, supportsPointerInteractionSelector);
|
||||
}
|
||||
XCTSkipUnless(supportsPointerInteraction, "Device does not support pointer interaction.");
|
||||
XCUIApplication* app = [[XCUIApplication alloc] init];
|
||||
app.launchArguments = @[ @"--pointer-events" ];
|
||||
[app launch];
|
||||
|
||||
NSPredicate* predicateToFindFlutterView =
|
||||
[NSPredicate predicateWithFormat:@"identifier BEGINSWITH 'flutter_view'"];
|
||||
XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
|
||||
elementMatchingPredicate:predicateToFindFlutterView];
|
||||
if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
|
||||
XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
|
||||
@(kSecondsToWaitForFlutterView));
|
||||
}
|
||||
|
||||
XCTAssertNotNil(flutterView);
|
||||
|
||||
SEL scroll = @selector(scrollByDeltaX:deltaY:);
|
||||
XCTAssertTrue([flutterView respondsToSelector:scroll],
|
||||
@"If supportsPointerInteraction is true, this should be true too.");
|
||||
// Need to use NSInvocation in order to send primitive arguments to selector.
|
||||
NSInvocation* invocation = [NSInvocation
|
||||
invocationWithMethodSignature:[XCUIElement instanceMethodSignatureForSelector:scroll]];
|
||||
[invocation setSelector:scroll];
|
||||
CGFloat deltaX = 0.0;
|
||||
CGFloat deltaY = 1000.0;
|
||||
[invocation setArgument:&deltaX atIndex:2];
|
||||
[invocation setArgument:&deltaY atIndex:3];
|
||||
[invocation invokeWithTarget:flutterView];
|
||||
|
||||
// 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
|
||||
// processing.
|
||||
XCTestExpectation* sleepExpectation = [self expectationWithDescription:@"never fires"];
|
||||
sleepExpectation.inverted = true;
|
||||
[self waitForExpectations:@[ sleepExpectation ] timeout:5.0];
|
||||
|
||||
// There are hover events interspersed with the scroll events in a varying order.
|
||||
// Ensure the individual orderings are respected without hardcoding the absolute sequence.
|
||||
NSMutableDictionary<NSString*, NSMutableArray<NSNumber*>*>* messages =
|
||||
[[NSMutableDictionary alloc] init];
|
||||
for (XCUIElement* element in [app.textFields allElementsBoundByIndex]) {
|
||||
NSString* rawMessage = element.value;
|
||||
// Parse out the sequence number
|
||||
NSUInteger commaIndex = [rawMessage rangeOfString:@","].location;
|
||||
NSInteger messageSequenceNumber =
|
||||
[rawMessage substringWithRange:NSMakeRange(0, commaIndex)].integerValue;
|
||||
// Parse out the rest of the message
|
||||
NSString* message = [rawMessage
|
||||
substringWithRange:NSMakeRange(commaIndex + 1, rawMessage.length - (commaIndex + 1))];
|
||||
NSMutableArray<NSNumber*>* messageSequenceNumberList = messages[message];
|
||||
if (messageSequenceNumberList == nil) {
|
||||
messageSequenceNumberList = [[NSMutableArray alloc] init];
|
||||
messages[message] = messageSequenceNumberList;
|
||||
}
|
||||
[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");
|
||||
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");
|
||||
int panZoomStartSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.panZoomStart,device=1,buttons=0");
|
||||
// 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"];
|
||||
int panZoomEndSequenceNumber = assertOneMessageAndGetSequenceNumber(
|
||||
messages, @"PointerChange.panZoomEnd,device=1,buttons=0");
|
||||
|
||||
XCTAssertGreaterThan(panZoomStartSequenceNumber, panZoomAddedSequenceNumber,
|
||||
@"PanZoomStart occured before pointer was added");
|
||||
XCTAssertGreaterThan([[panZoomUpdateSequenceNumbers firstObject] intValue],
|
||||
panZoomStartSequenceNumber, @"PanZoomUpdate occured before PanZoomStart");
|
||||
XCTAssertGreaterThan(panZoomEndSequenceNumber,
|
||||
[[panZoomUpdateSequenceNumbers lastObject] intValue],
|
||||
@"PanZoomUpdate occured after PanZoomUpdate");
|
||||
|
||||
XCTAssertGreaterThan([[hoverSequenceNumbers firstObject] intValue], hoverAddedSequenceNumber,
|
||||
@"Hover occured before pointer was added");
|
||||
XCTAssertGreaterThan(hoverRemovedSequenceNumber, [[hoverSequenceNumbers lastObject] intValue],
|
||||
@"Hover occured after pointer was removed");
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user