Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
@property(nonatomic, assign) double targetViewInsetBottom;
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;

/// VSyncClient for touch callback's rate correction.
Copy link
Contributor

Choose a reason for hiding this comment

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

nits:

Suggested change
/// VSyncClient for touch callback's rate correction.
/// VSyncClient for touch callback's frame rate correction.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe also add a short explanation about why this is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;
Copy link
Member

Choose a reason for hiding this comment

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

Is VSyncClient necessary here? Since this is just for increasing resolution of touch events and doesn't directly affect rendering, should we just use CADisplayLink directly?

Copy link
Contributor

Choose a reason for hiding this comment

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

Using VSyncClient ensures the preferred frame rate that used is the same as the one used on UIThread.


/*
* Mouse and trackpad gesture recognizers
*/
Expand Down Expand Up @@ -671,6 +674,9 @@ - (void)viewDidLoad {
// Register internal plugins.
[self addInternalPlugins];

// Create a vsync client to correct touch rate if needed.
Copy link
Contributor

Choose a reason for hiding this comment

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

nits:
how about: Create a vsync client to correct frame rate during touch events if needed.

Copy link
Contributor Author

@luckysmg luckysmg Aug 27, 2022

Choose a reason for hiding this comment

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

I have changed it to delivery frame rate of touch events, which seems to be more accurate.😄

[self createTouchRateCorrectionVSyncClientIfNeeded];

if (@available(iOS 13.4, *)) {
_hoverGestureRecognizer =
[[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
Expand Down Expand Up @@ -833,6 +839,7 @@ - (void)dealloc {
[self deregisterNotifications];

[self invalidateKeyboardAnimationVSyncClient];
[self invalidateTouchRateCorrectionVSyncClient];
_scrollView.get().delegate = nil;
_hoverGestureRecognizer.delegate = nil;
[_hoverGestureRecognizer release];
Expand Down Expand Up @@ -966,6 +973,9 @@ - (void)dispatchTouches:(NSSet*)touches
}
}

// Activate or pause touch rate correction according to the touches when user is interacting.
Copy link
Contributor

Choose a reason for hiding this comment

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

"touch rate" is a little ambiguous. maybe "frame rate during touch events"?

Copy link
Contributor Author

@luckysmg luckysmg Aug 27, 2022

Choose a reason for hiding this comment

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

I have changed it to delivery frame rate of touch events, which seems to be more accurate.😄

[self triggerTouchRateCorrectionIfNeeded:touches];

const CGFloat scale = [UIScreen mainScreen].scale;
auto packet =
std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
Expand Down Expand Up @@ -1111,6 +1121,63 @@ - (void)forceTouchesCancelled:(NSSet*)touches {
[self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
}

#pragma mark - Touch events rate correction

- (void)createTouchRateCorrectionVSyncClientIfNeeded {
if (_touchRateCorrectionVSyncClient != nil) {
return;
}

double displayRefreshRate = [DisplayLinkManager displayRefreshRate];
const double epsilon = 0.1;
if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0

// If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events
// is the same with render vsync rate. So we don't need to create
Copy link
Member

Choose a reason for hiding this comment

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

Do not use we in comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done^_^

// _touchRateCorrectionVSyncClient to correct touch callback's rate.
return;
}

flutter::Shell& shell = [_engine.get() shell];
auto callback = [](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
// Do nothing in this block. Just trigger system to callback touch events with correct rate.
};
_touchRateCorrectionVSyncClient =
[[VSyncClient alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner()
callback:callback];
_touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
}

- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
if (_touchRateCorrectionVSyncClient == nil) {
// If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't
// need to correct the touch rate. So just return.
return;
}

// As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved,
// we should activate the correction. Otherwise we will pause the correction.
Copy link
Member

Choose a reason for hiding this comment

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

We in comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done^_^

BOOL isUserInteracting = NO;
for (UITouch* touch in touches) {
if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
isUserInteracting = YES;
break;
}
}

if (isUserInteracting && [_engine.get() viewController] == self) {
[_touchRateCorrectionVSyncClient await];
} else {
[_touchRateCorrectionVSyncClient pause];
}
}

- (void)invalidateTouchRateCorrectionVSyncClient {
[_touchRateCorrectionVSyncClient invalidate];
[_touchRateCorrectionVSyncClient release];
_touchRateCorrectionVSyncClient = nil;
}

#pragma mark - Handle view resizing

- (void)updateViewportMetrics {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ @interface FlutterViewController (Tests)

@property(nonatomic, assign) double targetViewInsetBottom;

- (void)createTouchRateCorrectionVSyncClientIfNeeded;
- (void)surfaceUpdated:(BOOL)appeared;
- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
- (void)handlePressEvent:(FlutterUIPressProxy*)press
Expand Down Expand Up @@ -160,6 +161,18 @@ - (void)tearDown {
self.messageSent = nil;
}

- (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient {
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
[viewControllerMock loadView];
[viewControllerMock viewDidLoad];
OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]);
}

- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardAnimationVsyncClient {
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

FLUTTER_ASSERT_NOT_ARC

@interface UITouch ()

@property(nonatomic, readwrite) UITouchPhase phase;

@end

@interface VSyncClient (Testing)

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

@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;

- (void)createTouchRateCorrectionVSyncClientIfNeeded;
- (void)setupKeyboardAnimationVsyncClient;
- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches;

@end

Expand Down Expand Up @@ -56,4 +66,111 @@ - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterV
}
}

- (void)
testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
}

- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];

FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
XCTAssertNotNil(clientBefore);

[viewController createTouchRateCorrectionVSyncClientIfNeeded];
VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
XCTAssertNotNil(clientAfter);

XCTAssertTrue(clientBefore == clientAfter);
}

- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 60;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController createTouchRateCorrectionVSyncClientIfNeeded];
XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
}

- (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
double maxFrameRate = 120;
[[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
FlutterEngine* engine = [[FlutterEngine alloc] init];
[engine runWithEntrypoint:nil];
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
nibName:nil
bundle:nil];
[viewController loadView];
[viewController viewDidLoad];

VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
CADisplayLink* link = [client getDisplayLink];

UITouch* fakeTouchBegan = [[UITouch alloc] init];
fakeTouchBegan.phase = UITouchPhaseBegan;

UITouch* fakeTouchMove = [[UITouch alloc] init];
fakeTouchMove.phase = UITouchPhaseMoved;

UITouch* fakeTouchEnd = [[UITouch alloc] init];
fakeTouchEnd.phase = UITouchPhaseEnded;

UITouch* fakeTouchCancelled = [[UITouch alloc] init];
fakeTouchCancelled.phase = UITouchPhaseCancelled;

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
XCTAssertFalse(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
XCTAssertTrue(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
XCTAssertFalse(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
XCTAssertTrue(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
XCTAssertFalse(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
fakeTouchCancelled, nil]];
XCTAssertTrue(link.isPaused);

[viewController
triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
XCTAssertFalse(link.isPaused);
}

@end
19 changes: 19 additions & 0 deletions shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,23 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs
[vsyncClient release];
}

- (void)testAwaitAndPauseWillWorkCorrectly {
auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest");
VSyncClient* vsyncClient = [[[VSyncClient alloc]
initWithTaskRunner:thread_task_runner
callback:[](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {}]
autorelease];

CADisplayLink* link = [vsyncClient getDisplayLink];
XCTAssertTrue(link.isPaused);

[vsyncClient await];
XCTAssertFalse(link.isPaused);

[vsyncClient pause];
XCTAssertTrue(link.isPaused);

[vsyncClient release];
}

@end
2 changes: 2 additions & 0 deletions shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@

- (void)await;

- (void)pause;

- (void)invalidate;

- (double)getRefreshRate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ - (void)await {
display_link_.get().paused = NO;
}

- (void)pause {
display_link_.get().paused = YES;
}

- (void)onDisplayLink:(CADisplayLink*)link {
TRACE_EVENT0("flutter", "VSYNC");

Expand Down