Skip to content

Commit bb9c3ef

Browse files
luckysmgOleh
authored andcommitted
[iOS] Avoid jitter and laggy when user is dragging on iOS Promotion devices (flutter#35592)
1 parent 40e87b9 commit bb9c3ef

6 files changed

Lines changed: 227 additions & 0 deletions

File tree

shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
6565
@property(nonatomic, assign) double targetViewInsetBottom;
6666
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;
6767

68+
/// VSyncClient for touch events delivery frame rate correction.
69+
///
70+
/// On promotion devices(eg: iPhone13 Pro), the delivery frame rate of touch events is 60HZ
71+
/// but the frame rate of rendering is 120HZ, which is different and will leads jitter and laggy.
72+
/// With this VSyncClient, it can correct the delivery frame rate of touch events to let it keep
73+
/// the same with frame rate of rendering.
74+
@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;
75+
6876
/*
6977
* Mouse and trackpad gesture recognizers
7078
*/
@@ -680,6 +688,9 @@ - (void)viewDidLoad {
680688
// Register internal plugins.
681689
[self addInternalPlugins];
682690

691+
// Create a vsync client to correct delivery frame rate of touch events if needed.
692+
[self createTouchRateCorrectionVSyncClientIfNeeded];
693+
683694
if (@available(iOS 13.4, *)) {
684695
_hoverGestureRecognizer =
685696
[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
@@ -842,6 +853,7 @@ - (void)dealloc {
842853
[self deregisterNotifications];
843854

844855
[self invalidateKeyboardAnimationVSyncClient];
856+
[self invalidateTouchRateCorrectionVSyncClient];
845857
_scrollView.get().delegate = nil;
846858
_hoverGestureRecognizer.delegate = nil;
847859
[_hoverGestureRecognizer release];
@@ -975,6 +987,9 @@ - (void)dispatchTouches:(NSSet*)touches
975987
}
976988
}
977989

990+
// Activate or pause the correction of delivery frame rate of touch events.
991+
[self triggerTouchRateCorrectionIfNeeded:touches];
992+
978993
const CGFloat scale = [UIScreen mainScreen].scale;
979994
auto packet =
980995
std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
@@ -1120,6 +1135,63 @@ - (void)forceTouchesCancelled:(NSSet*)touches {
11201135
[self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
11211136
}
11221137

1138+
#pragma mark - Touch events rate correction
1139+
1140+
- (void)createTouchRateCorrectionVSyncClientIfNeeded {
1141+
if (_touchRateCorrectionVSyncClient != nil) {
1142+
return;
1143+
}
1144+
1145+
double displayRefreshRate = [DisplayLinkManager displayRefreshRate];
1146+
const double epsilon = 0.1;
1147+
if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0
1148+
1149+
// If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events
1150+
// is the same with render vsync rate. So it is unnecessary to create
1151+
// _touchRateCorrectionVSyncClient to correct touch callback's rate.
1152+
return;
1153+
}
1154+
1155+
flutter::Shell& shell = [_engine.get() shell];
1156+
auto callback = [](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1157+
// Do nothing in this block. Just trigger system to callback touch events with correct rate.
1158+
};
1159+
_touchRateCorrectionVSyncClient =
1160+
[[VSyncClient alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner()
1161+
callback:callback];
1162+
_touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
1163+
}
1164+
1165+
- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
1166+
if (_touchRateCorrectionVSyncClient == nil) {
1167+
// If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't
1168+
// need to correct the touch rate. So just return.
1169+
return;
1170+
}
1171+
1172+
// As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved,
1173+
// activate the correction. Otherwise pause the correction.
1174+
BOOL isUserInteracting = NO;
1175+
for (UITouch* touch in touches) {
1176+
if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
1177+
isUserInteracting = YES;
1178+
break;
1179+
}
1180+
}
1181+
1182+
if (isUserInteracting && [_engine.get() viewController] == self) {
1183+
[_touchRateCorrectionVSyncClient await];
1184+
} else {
1185+
[_touchRateCorrectionVSyncClient pause];
1186+
}
1187+
}
1188+
1189+
- (void)invalidateTouchRateCorrectionVSyncClient {
1190+
[_touchRateCorrectionVSyncClient invalidate];
1191+
[_touchRateCorrectionVSyncClient release];
1192+
_touchRateCorrectionVSyncClient = nil;
1193+
}
1194+
11231195
#pragma mark - Handle view resizing
11241196

11251197
- (void)updateViewportMetrics {

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ @interface FlutterViewController (Tests)
114114

115115
@property(nonatomic, assign) double targetViewInsetBottom;
116116

117+
- (void)createTouchRateCorrectionVSyncClientIfNeeded;
117118
- (void)surfaceUpdated:(BOOL)appeared;
118119
- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
119120
- (void)handlePressEvent:(FlutterUIPressProxy*)press
@@ -160,6 +161,18 @@ - (void)tearDown {
160161
self.messageSent = nil;
161162
}
162163

164+
- (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient {
165+
FlutterEngine* engine = [[FlutterEngine alloc] init];
166+
[engine runWithEntrypoint:nil];
167+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
168+
nibName:nil
169+
bundle:nil];
170+
FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
171+
[viewControllerMock loadView];
172+
[viewControllerMock viewDidLoad];
173+
OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]);
174+
}
175+
163176
- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardAnimationVsyncClient {
164177
FlutterEngine* engine = [[FlutterEngine alloc] init];
165178
[engine runWithEntrypoint:nil];

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest_mrc.mm

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
FLUTTER_ASSERT_NOT_ARC
1313

14+
@interface UITouch ()
15+
16+
@property(nonatomic, readwrite) UITouchPhase phase;
17+
18+
@end
19+
1420
@interface VSyncClient (Testing)
1521

1622
- (CADisplayLink*)getDisplayLink;
@@ -22,7 +28,11 @@ @interface FlutterViewController (Testing)
2228
@property(nonatomic, assign) double targetViewInsetBottom;
2329
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;
2430

31+
@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;
32+
33+
- (void)createTouchRateCorrectionVSyncClientIfNeeded;
2534
- (void)setupKeyboardAnimationVsyncClient;
35+
- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches;
2636

2737
@end
2838

@@ -56,4 +66,111 @@ - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterV
5666
}
5767
}
5868

69+
- (void)
70+
testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
71+
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
72+
double maxFrameRate = 120;
73+
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
74+
FlutterEngine* engine = [[FlutterEngine alloc] init];
75+
[engine runWithEntrypoint:nil];
76+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
77+
nibName:nil
78+
bundle:nil];
79+
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
80+
XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
81+
}
82+
83+
- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
84+
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
85+
double maxFrameRate = 120;
86+
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
87+
88+
FlutterEngine* engine = [[FlutterEngine alloc] init];
89+
[engine runWithEntrypoint:nil];
90+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
91+
nibName:nil
92+
bundle:nil];
93+
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
94+
VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
95+
XCTAssertNotNil(clientBefore);
96+
97+
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
98+
VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
99+
XCTAssertNotNil(clientAfter);
100+
101+
XCTAssertTrue(clientBefore == clientAfter);
102+
}
103+
104+
- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
105+
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
106+
double maxFrameRate = 60;
107+
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
108+
FlutterEngine* engine = [[FlutterEngine alloc] init];
109+
[engine runWithEntrypoint:nil];
110+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
111+
nibName:nil
112+
bundle:nil];
113+
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
114+
XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
115+
}
116+
117+
- (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
118+
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
119+
double maxFrameRate = 120;
120+
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
121+
FlutterEngine* engine = [[FlutterEngine alloc] init];
122+
[engine runWithEntrypoint:nil];
123+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
124+
nibName:nil
125+
bundle:nil];
126+
[viewController loadView];
127+
[viewController viewDidLoad];
128+
129+
VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
130+
CADisplayLink* link = [client getDisplayLink];
131+
132+
UITouch* fakeTouchBegan = [[UITouch alloc] init];
133+
fakeTouchBegan.phase = UITouchPhaseBegan;
134+
135+
UITouch* fakeTouchMove = [[UITouch alloc] init];
136+
fakeTouchMove.phase = UITouchPhaseMoved;
137+
138+
UITouch* fakeTouchEnd = [[UITouch alloc] init];
139+
fakeTouchEnd.phase = UITouchPhaseEnded;
140+
141+
UITouch* fakeTouchCancelled = [[UITouch alloc] init];
142+
fakeTouchCancelled.phase = UITouchPhaseCancelled;
143+
144+
[viewController
145+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
146+
XCTAssertFalse(link.isPaused);
147+
148+
[viewController
149+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
150+
XCTAssertTrue(link.isPaused);
151+
152+
[viewController
153+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
154+
XCTAssertFalse(link.isPaused);
155+
156+
[viewController
157+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
158+
XCTAssertTrue(link.isPaused);
159+
160+
[viewController
161+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
162+
initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
163+
XCTAssertFalse(link.isPaused);
164+
165+
[viewController
166+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
167+
fakeTouchCancelled, nil]];
168+
XCTAssertTrue(link.isPaused);
169+
170+
[viewController
171+
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
172+
initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
173+
XCTAssertFalse(link.isPaused);
174+
}
175+
59176
@end

shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,23 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
116116
[vsyncClient release];
117117
}
118118

119+
- (void)testAwaitAndPauseWillWorkCorrectly {
120+
auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest");
121+
VSyncClient* vsyncClient = [[[VSyncClient alloc]
122+
initWithTaskRunner:thread_task_runner
123+
callback:[](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {}]
124+
autorelease];
125+
126+
CADisplayLink* link = [vsyncClient getDisplayLink];
127+
XCTAssertTrue(link.isPaused);
128+
129+
[vsyncClient await];
130+
XCTAssertFalse(link.isPaused);
131+
132+
[vsyncClient pause];
133+
XCTAssertTrue(link.isPaused);
134+
135+
[vsyncClient release];
136+
}
137+
119138
@end

shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
- (void)await;
4747

48+
- (void)pause;
49+
4850
- (void)invalidate;
4951

5052
- (double)getRefreshRate;

shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ - (void)await {
9797
display_link_.get().paused = NO;
9898
}
9999

100+
- (void)pause {
101+
display_link_.get().paused = YES;
102+
}
103+
100104
- (void)onDisplayLink:(CADisplayLink*)link {
101105
TRACE_EVENT0("flutter", "VSYNC");
102106

0 commit comments

Comments
 (0)