From 863c5a06bcc6c6d7aad077ddeda9888fb0fc3a99 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Tue, 14 Mar 2023 16:01:49 -0700 Subject: [PATCH 1/5] [ios_rotation_distortion]use linear interpolation to send different sizes during rotation to fix a problem with aspect ratio distortion --- .../framework/Source/FlutterViewController.mm | 109 +++++++++- .../Source/FlutterViewControllerTest.mm | 193 +++++++++++++++++- .../Source/FlutterViewController_Internal.h | 10 + 3 files changed, 301 insertions(+), 11 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 77742b0114c9e..a1848af8d78b8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -44,6 +44,38 @@ NSNotificationName const FlutterViewControllerShowHomeIndicator = @"FlutterViewControllerShowHomeIndicator"; +/** + * Compute the interpolated value under linear interpolation. + */ +CGFloat FLTLinearInterpolatedValue(double progress, CGFloat from, CGFloat to, CGFloat scale) { + NSCAssert(progress >= 0 && progress <= 1, @"progress must be between 0 and 1"); + return (from * (1 - progress) + to * progress) * scale; +} + +/** + * Interpolate the viewport metrics for smoother rotation transition. + */ +void FLTInterpolateViewportMetrics(flutter::ViewportMetrics& viewportMetrics, + double rotationProgress, + CGSize fromSize, + UIEdgeInsets fromPadding, + CGSize toSize, + UIEdgeInsets toPadding) { + CGFloat scale = [UIScreen mainScreen].scale; + viewportMetrics.physical_width = + FLTLinearInterpolatedValue(rotationProgress, fromSize.width, toSize.width, scale); + viewportMetrics.physical_height = + FLTLinearInterpolatedValue(rotationProgress, fromSize.height, toSize.height, scale); + viewportMetrics.physical_padding_top = + FLTLinearInterpolatedValue(rotationProgress, fromPadding.top, toPadding.top, scale); + viewportMetrics.physical_padding_left = + FLTLinearInterpolatedValue(rotationProgress, fromPadding.left, toPadding.left, scale); + viewportMetrics.physical_padding_bottom = + FLTLinearInterpolatedValue(rotationProgress, fromPadding.bottom, toPadding.bottom, scale); + viewportMetrics.physical_padding_right = + FLTLinearInterpolatedValue(rotationProgress, fromPadding.right, toPadding.right, scale); +} + // 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. @@ -63,6 +95,11 @@ @interface FlutterViewController () )coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + + // We interpolate the viewport metrics (size and paddings) during rotation transition, to address + // a bug with distorted aspect ratio. + // See: https://github.com/flutter/flutter/issues/16322 + // + // For every `kRotationViewportMetricsUpdateInterval`, we send the metrics which is interpolated + // between the old metrics before the rotation transition, to the new metrics after the rotation + // transition. + // + // Currently it is using linear interpolation. Using non-linear ease-in/out interpolation may + // achieve better results. It may also help to send only rotation info (such as rotation duration) + // and perform the interpolation on the framework side, to reduce engine/framework communication. + // However, since flutter's drawing happens on the ui thread, which is not iOS main thread, + // there is no guarantee that the viewport metrics change is immediately taken effect, resulting + // in some amount of unavoidable distortion. + + NSTimeInterval transitionDuration = coordinator.transitionDuration; + // Do not interpolate if zero transition duration. + if (transitionDuration == 0) { + return; + } + + _isDuringRotationTransition = YES; + + CGSize oldSize = self.view.bounds.size; + UIEdgeInsets oldPadding = self.view.safeAreaInsets; + + __block double rotationProgress = 0; + // Timer is retained by the run loop, and will be released after invalidated. + [NSTimer + scheduledTimerWithTimeInterval:kRotationViewportMetricsUpdateInterval + repeats:YES + block:^(NSTimer* timer) { + double progressDelta = + kRotationViewportMetricsUpdateInterval / transitionDuration; + rotationProgress = fmin(1, rotationProgress + progressDelta); + + CGSize newSize = self.view.bounds.size; + UIEdgeInsets newPadding = self.view.safeAreaInsets; + + FLTInterpolateViewportMetrics(_viewportMetrics, rotationProgress, + oldSize, oldPadding, newSize, + newPadding); + [self updateViewportMetricsIfNeeded:YES]; + + // End of rotation. Invalidate the timer. + if (rotationProgress == 1) { + _isDuringRotationTransition = NO; + [timer invalidate]; + } + }]; +} + - (void)flushOngoingTouches { if (_engine && _ongoingTouches.get().count > 0) { auto packet = std::make_unique(_ongoingTouches.get().count); @@ -1278,7 +1371,11 @@ - (void)pencilInteractionDidTap:(UIPencilInteraction*)interaction API_AVAILABLE( #pragma mark - Handle view resizing -- (void)updateViewportMetrics { +- (void)updateViewportMetricsIfNeeded:(BOOL)forRotation { + // update viewport metrics only if `_isDuringRotationTransition` matches `forRotation`. + if (_isDuringRotationTransition != forRotation) { + return; + } if ([_engine.get() viewController] == self) { [_engine.get() updateViewportMetrics:_viewportMetrics]; } @@ -1299,7 +1396,7 @@ - (void)viewDidLayoutSubviews { _viewportMetrics.physical_height = viewBounds.size.height * scale; [self updateViewportPadding]; - [self updateViewportMetrics]; + [self updateViewportMetricsIfNeeded:NO]; // There is no guarantee that UIKit will layout subviews when the application is active. Creating // the surface when inactive will cause GPU accesses from the background. Only wait for the first @@ -1329,7 +1426,7 @@ - (void)viewDidLayoutSubviews { - (void)viewSafeAreaInsetsDidChange { [self updateViewportPadding]; - [self updateViewportMetrics]; + [self updateViewportMetricsIfNeeded:NO]; [super viewSafeAreaInsetsDidChange]; } @@ -1661,7 +1758,7 @@ - (void)setupKeyboardAnimationVsyncClient { flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = flutterViewController.get() .keyboardAnimationView.layer.presentationLayer.frame.origin.y; - [flutterViewController updateViewportMetrics]; + [flutterViewController updateViewportMetricsIfNeeded:NO]; } } else { fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() - @@ -1669,7 +1766,7 @@ - (void)setupKeyboardAnimationVsyncClient { flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = [[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()]; - [flutterViewController updateViewportMetrics]; + [flutterViewController updateViewportMetricsIfNeeded:NO]; } }; flutter::Shell& shell = [_engine.get() shell]; @@ -1698,7 +1795,7 @@ - (void)ensureViewportMetricsIsCorrect { if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom) { // Make sure the `physical_view_inset_bottom` is the target value. _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom; - [self updateViewportMetrics]; + [self updateViewportMetricsIfNeeded:NO]; } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 575215c2e77cb..e45346fbe4930 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -126,7 +126,7 @@ - (void)handlePressEvent:(FlutterUIPressProxy*)press nextAction:(void (^)())next API_AVAILABLE(ios(13.4)); - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer; - (flutter::PointerData)createAuxillaryStylusActionData; -- (void)updateViewportMetrics; +- (void)updateViewportMetricsIfNeeded:(BOOL)forRotation; - (void)onUserSettingsChanged:(NSNotification*)notification; - (void)applicationWillTerminate:(NSNotification*)notification; - (void)goToApplicationLifecycle:(nonnull NSString*)state; @@ -836,7 +836,7 @@ - (void)testViewDidDisappearDoesPauseEngineWhenIsTheViewController { OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]); } -- (void)testUpdateViewportMetricsDoesntInvokeEngineWhenNotTheViewController { +- (void)testUpdateViewportMetricsIfNeeded_DoesNotInvokeEngineWhenNotTheViewController { FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine @@ -847,12 +847,12 @@ - (void)testUpdateViewportMetricsDoesntInvokeEngineWhenNotTheViewController { nibName:nil bundle:nil]; mockEngine.viewController = viewControllerB; - [viewControllerA updateViewportMetrics]; + [viewControllerA updateViewportMetricsIfNeeded:NO]; flutter::ViewportMetrics viewportMetrics; OCMVerify(never(), [mockEngine updateViewportMetrics:viewportMetrics]); } -- (void)testUpdateViewportMetricsDoesInvokeEngineWhenIsTheViewController { +- (void)testUpdateViewportMetricsIfNeeded_DoesInvokeEngineWhenIsTheViewController { FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine @@ -861,7 +861,190 @@ - (void)testUpdateViewportMetricsDoesInvokeEngineWhenIsTheViewController { mockEngine.viewController = viewController; flutter::ViewportMetrics viewportMetrics; OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs(); - [viewController updateViewportMetrics]; + [viewController updateViewportMetricsIfNeeded:NO]; + OCMVerifyAll(mockEngine); +} + +- (void)testUpdateViewportMetricsIfNeeded_DoesInvokeEngineForRotationWhenRotatingDevice { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + mockEngine.viewController = viewController; + flutter::ViewportMetrics viewportMetrics; + OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs(); + + id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); + OCMStub([mockCoordinator transitionDuration]).andReturn(0.5); + + // Mimic the device rotation. + [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; + // Should trigger the engine call when passing YES to `forRotation`. + [viewController updateViewportMetricsIfNeeded:YES]; + + OCMVerifyAll(mockEngine); +} + +- (void)testUpdateViewportMetricsIfNeeded_DoesInvokeEngineNotForRotationWhenNotRotatingDevice { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + mockEngine.viewController = viewController; + flutter::ViewportMetrics viewportMetrics; + OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs(); + + id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); + OCMStub([mockCoordinator transitionDuration]).andReturn(0.5); + + // Should trigger the engine call when passing NO to `forRotation`. + [viewController updateViewportMetricsIfNeeded:NO]; + + OCMVerifyAll(mockEngine); +} + +- (void)testUpdateViewportMetricsIfNeeded_DoesNotInvokeEngineForRotationWhenNotRotatingDevice { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + mockEngine.viewController = viewController; + + id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); + OCMStub([mockCoordinator transitionDuration]).andReturn(0.5); + + // Should not trigger the engine call when passing YES to `forRotation`. + [viewController updateViewportMetricsIfNeeded:YES]; + OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]); +} + +- (void)testUpdateViewportMetricsIfNeeded_DoesNotInvokeEngineNotForRotationWhenRotatingDevice { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + mockEngine.viewController = viewController; + + id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); + OCMStub([mockCoordinator transitionDuration]).andReturn(0.5); + + // Mimic the device rotation. + [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; + // Should not trigger the engine call when passing NO to `forRotation`. + [viewController updateViewportMetricsIfNeeded:NO]; + + OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]); +} + +- (void)testFLTInterpolateViewportMetrics_UsesLinearInterpolation { + // Verify linear interpolation by checking 0%, 25%, 50% and 100% of progresses. + + // 0% of rotation progress + CGSize fromSize = CGSizeMake(1000, 500); + CGSize toSize = CGSizeMake(500, 1000); + UIEdgeInsets fromPadding = UIEdgeInsetsMake(/*top=*/10, /*left=*/20, /*bottom=*/30, /*right=*/40); + UIEdgeInsets toPadding = UIEdgeInsetsMake(/*top=*/50, /*left=*/60, /*bottom=*/70, /*right=*/80); + + flutter::ViewportMetrics viewportMetrics; + FLTInterpolateViewportMetrics(viewportMetrics, + /*rotationProgress=*/0, fromSize, fromPadding, toSize, toPadding); + + XCTAssertEqual(viewportMetrics.physical_width, 1000 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_height, 500 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 10 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 20 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 30 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 40 * UIScreen.mainScreen.scale); + + // 25% of rotation progress + FLTInterpolateViewportMetrics(viewportMetrics, + /*rotationProgress=*/0.25, fromSize, fromPadding, toSize, + toPadding); + + XCTAssertEqual(viewportMetrics.physical_width, 875 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_height, 625 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 20 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 30 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 40 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 50 * UIScreen.mainScreen.scale); + + // 50% of rotation progress + FLTInterpolateViewportMetrics(viewportMetrics, + /*rotationProgress=*/0.5, fromSize, fromPadding, toSize, toPadding); + + XCTAssertEqual(viewportMetrics.physical_width, 750 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_height, 750 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 30 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 40 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 50 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 60 * UIScreen.mainScreen.scale); + + // 100% of rotation progress + FLTInterpolateViewportMetrics(viewportMetrics, + /*rotationProgress=*/1, fromSize, fromPadding, toSize, toPadding); + + XCTAssertEqual(viewportMetrics.physical_width, 500 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_height, 1000 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 50 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 60 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 70 * UIScreen.mainScreen.scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 80 * UIScreen.mainScreen.scale); +} + +- (void)testViewWillTransitionToSize_DoesInterpolateViewportMetricsIfNonZeroDuration { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + mockEngine.viewController = viewController; + + id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); + NSTimeInterval transitionDuration = 0.5; + OCMStub([mockCoordinator transitionDuration]).andReturn(transitionDuration); + + flutter::ViewportMetrics viewportMetrics; + + XCTestExpectation* expectation = + [self expectationWithDescription:@"update viewport with interpolated metrics"]; + __block int frameCount = 0; + OCMStub([mockEngine updateViewportMetrics:viewportMetrics]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation* invocation) { + frameCount += 1; + // Since we actually accumulate the progress delta (from 0%-100%), rather than directly + // dividing the total transition duration by the frame interval, there can easily be + // a rounding error. So we take the floor for simplicity. + // We cannot use `expectedFulfillmentCount` since it could over fulfill once. + double estimatedCount = transitionDuration / kRotationViewportMetricsUpdateInterval; + if (frameCount == floor(estimatedCount)) { + [expectation fulfill]; + } + }); + [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testViewWillTransitionToSize_DoesNotInterpolateViewportMetricsIfZeroDuration { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + mockEngine.viewController = viewController; + + id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); + OCMStub([mockCoordinator transitionDuration]).andReturn(0); + + [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; + + OCMExpect([mockEngine updateViewportMetrics:flutter::ViewportMetrics()]).ignoringNonObjectArgs(); + // Should directly update the view port metrics (when passing NO to `forRotation`). + [viewController updateViewportMetricsIfNeeded:NO]; OCMVerifyAll(mockEngine); } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 664b4c282b9f8..cbe7254ba68d2 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -6,6 +6,7 @@ #define FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERVIEWCONTROLLER_INTERNAL_H_ #include "flutter/fml/memory/weak_ptr.h" +#include "flutter/lib/ui/window/viewport_metrics.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeySecondaryResponder.h" @@ -27,12 +28,21 @@ extern NSNotificationName const FlutterViewControllerHideHomeIndicator; FLUTTER_DARWIN_EXPORT extern NSNotificationName const FlutterViewControllerShowHomeIndicator; +constexpr NSTimeInterval kRotationViewportMetricsUpdateInterval = 1.0 / 60; + typedef NS_ENUM(NSInteger, FlutterKeyboardMode) { FlutterKeyboardModeHidden = 0, FlutterKeyboardModeDocked = 1, FlutterKeyboardModeFloating = 2, }; +void FLTInterpolateViewportMetrics(flutter::ViewportMetrics& viewportMetrics, + double rotationProgress, + CGSize fromSize, + UIEdgeInsets fromPadding, + CGSize toSize, + UIEdgeInsets toPadding); + @interface FlutterViewController () @property(class, nonatomic, readonly) BOOL accessibilityIsOnOffSwitchLabelsEnabled; From e17cb4f130d673c68f96de3a85046322d22a2b88 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Wed, 22 Mar 2023 10:15:46 -0700 Subject: [PATCH 2/5] added TODO with non-linear interpolation with github ticket --- .../darwin/ios/framework/Source/FlutterViewController.mm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index a1848af8d78b8..0225caf141b27 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -48,6 +48,8 @@ * Compute the interpolated value under linear interpolation. */ CGFloat FLTLinearInterpolatedValue(double progress, CGFloat from, CGFloat to, CGFloat scale) { + // TODO(hellohuanlin): consider non-linear interpolation to further reduce rotation distortion. + // See: https://github.com/flutter/flutter/issues/123248 NSCAssert(progress >= 0 && progress <= 1, @"progress must be between 0 and 1"); return (from * (1 - progress) + to * progress) * scale; } From 38d20347bebae644e13a2925b4d5e63384968d59 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Wed, 22 Mar 2023 12:24:41 -0700 Subject: [PATCH 3/5] fix race condition by invalidating previous timer --- .../framework/Source/FlutterViewController.mm | 29 ++++- .../Source/FlutterViewControllerTest.mm | 108 ++++++++++++------ .../Source/FlutterViewController_Internal.h | 3 +- 3 files changed, 101 insertions(+), 39 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 0225caf141b27..a8c89eb6d1df9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -62,8 +62,8 @@ void FLTInterpolateViewportMetrics(flutter::ViewportMetrics& viewportMetrics, CGSize fromSize, UIEdgeInsets fromPadding, CGSize toSize, - UIEdgeInsets toPadding) { - CGFloat scale = [UIScreen mainScreen].scale; + UIEdgeInsets toPadding, + CGFloat scale) { viewportMetrics.physical_width = FLTLinearInterpolatedValue(rotationProgress, fromSize.width, toSize.width, scale); viewportMetrics.physical_height = @@ -102,6 +102,11 @@ @interface FlutterViewController () From 580b9d23dcbe36250f11677af1aa151a78b4379c Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Wed, 22 Mar 2023 15:00:32 -0700 Subject: [PATCH 4/5] fix naming --- .../framework/Source/FlutterViewController.mm | 22 ++++++++++--------- .../Source/FlutterViewControllerTest.mm | 19 +++++++++++----- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index a8c89eb6d1df9..8b47cc22fbb83 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -98,9 +98,9 @@ @interface FlutterViewController () Date: Wed, 22 Mar 2023 15:14:54 -0700 Subject: [PATCH 5/5] typo --- .../darwin/ios/framework/Source/FlutterViewController.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 8b47cc22fbb83..5c86ab699b377 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -927,7 +927,7 @@ - (void)viewWillTransitionToSize:(CGSize)size // Invalidate the timer to avoid race condition when a new rotation starts before the previous // rotation's timer ends. The `viewWillTransitionToSize` itself is guaranteed to be called after // the previous rotation is complete. However, there can still be race condition because: - // 1. the transition duration may not be divisible by `kRotationViewportMetricsUpdateInterval`, \ + // 1. the transition duration may not be divisible by `kRotationViewportMetricsUpdateInterval`, // resulting in 1 additional frame. // 2. there can still be rounding errors when accumulating the progress which is normalized. // 3. NSTimer is backed by the run loop, which is not accurate timing.