diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 77742b0114c9e..5c86ab699b377 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -44,6 +44,40 @@ NSNotificationName const FlutterViewControllerShowHomeIndicator = @"FlutterViewControllerShowHomeIndicator"; +/** + * 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; +} + +/** + * 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) { + 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 +97,16 @@ @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; + } + + // TODO(hellohuanlin): Use [self mainScreenIfViewLoaded] instead of [UIScreen mainScreen]. + // This requires adding the view to window during unit tests, which calls multiple engine calls + // that is hard to mock since they take/return structs. An alternative approach is to partial mock + // the FlutterViewController to make view controller life cycle methods no-op, and insert + // this mock into the responder chain. + CGFloat scale = [UIScreen mainScreen].scale; + _isDuringViewportMetricsInterpolationForRotation = YES; + + CGSize oldSize = self.view.bounds.size; + UIEdgeInsets oldPadding = self.view.safeAreaInsets; + + __block double rotationProgress = 0; + // 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`, + // 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. + if ([_rotationTimer isValid]) { + [_rotationTimer invalidate]; + } + self.rotationTimer = [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, scale); + [self updateViewportMetricsIfNeeded:YES]; + + // End of rotation. Invalidate the timer. + if (rotationProgress == 1) { + _isDuringViewportMetricsInterpolationForRotation = NO; + [timer invalidate]; + } + }]; +} + - (void)flushOngoingTouches { if (_engine && _ongoingTouches.get().count > 0) { auto packet = std::make_unique(_ongoingTouches.get().count); @@ -903,6 +1018,7 @@ - (void)dealloc { [_rotationGestureRecognizer release]; _pencilInteraction.delegate = nil; [_pencilInteraction release]; + [_rotationTimer release]; [super dealloc]; } @@ -1278,7 +1394,11 @@ - (void)pencilInteractionDidTap:(UIPencilInteraction*)interaction API_AVAILABLE( #pragma mark - Handle view resizing -- (void)updateViewportMetrics { +- (void)updateViewportMetricsIfNeeded:(BOOL)forRotation { + // update only if `_isDuringViewportMetricsInterpolationForRotation` matches `forRotation`. + if (_isDuringViewportMetricsInterpolationForRotation != forRotation) { + return; + } if ([_engine.get() viewController] == self) { [_engine.get() updateViewportMetrics:_viewportMetrics]; } @@ -1299,7 +1419,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 +1449,7 @@ - (void)viewDidLayoutSubviews { - (void)viewSafeAreaInsetsDidChange { [self updateViewportPadding]; - [self updateViewportMetrics]; + [self updateViewportMetricsIfNeeded:NO]; [super viewSafeAreaInsetsDidChange]; } @@ -1661,7 +1781,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 +1789,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 +1818,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..46bc73ab54b49 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,10 +861,242 @@ - (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); + CGFloat scale = 2; + + flutter::ViewportMetrics viewportMetrics; + FLTInterpolateViewportMetrics(viewportMetrics, /*rotationProgress=*/0, fromSize, fromPadding, + toSize, toPadding, scale); + + XCTAssertEqual(viewportMetrics.physical_width, 1000 * scale); + XCTAssertEqual(viewportMetrics.physical_height, 500 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 10 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 20 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 30 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 40 * scale); + + // 25% of rotation progress + FLTInterpolateViewportMetrics(viewportMetrics, /*rotationProgress=*/0.25, fromSize, fromPadding, + toSize, toPadding, scale); + + XCTAssertEqual(viewportMetrics.physical_width, 875 * scale); + XCTAssertEqual(viewportMetrics.physical_height, 625 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 20 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 30 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 40 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 50 * scale); + + // 50% of rotation progress + FLTInterpolateViewportMetrics(viewportMetrics, /*rotationProgress=*/0.5, fromSize, fromPadding, + toSize, toPadding, scale); + + XCTAssertEqual(viewportMetrics.physical_width, 750 * scale); + XCTAssertEqual(viewportMetrics.physical_height, 750 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 30 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 40 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 50 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 60 * scale); + + // 100% of rotation progress + FLTInterpolateViewportMetrics(viewportMetrics, /*rotationProgress=*/1, fromSize, fromPadding, + toSize, toPadding, scale); + + XCTAssertEqual(viewportMetrics.physical_width, 500 * scale); + XCTAssertEqual(viewportMetrics.physical_height, 1000 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_top, 50 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_left, 60 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_bottom, 70 * scale); + XCTAssertEqual(viewportMetrics.physical_padding_right, 80 * 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); +} + +- (void)testViewWillTransitionToSize_RotationTimerRaceCondition { + // This is to verify that when a new rotation happens, the previous rotation timer must be + // invalidated. Since NSTimer is toll-free bridge type, which is not supported by OCMock, this + // test is not as straight forward - we perform the second rotation in the middle of the first + // rotation, and then check the frame count to determine that the interpolation of the previous + // rotation is not happening after the mid point. + 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:@"completed expected frame count"]; + int estimatedCount = floor(transitionDuration / kRotationViewportMetricsUpdateInterval); + __block int frameCount = 0; + OCMStub([mockEngine updateViewportMetrics:viewportMetrics]) + .ignoringNonObjectArgs() + .andDo(^(NSInvocation* invocation) { + frameCount += 1; + if (frameCount == estimatedCount / 2) { + [viewController viewWillTransitionToSize:CGSizeZero + withTransitionCoordinator:mockCoordinator]; + } + if (frameCount == estimatedCount * 3 / 2) { + [expectation fulfill]; + } + }); + [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + // Wait for additional transitionDuration to allow updateViewportMetrics calls if any. + XCTWaiterResult result = [XCTWaiter + waitForExpectations:@[ [self expectationWithDescription:@"Waiting for rotation duration"] ] + timeout:transitionDuration]; + XCTAssertEqual(result, XCTWaiterResultTimedOut); + // 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. We cannot use `expectedFulfillmentCount` since it could over-fulfill once. + // So we check the absolute difference of at most 1 frame. + XCTAssert(abs(frameCount - estimatedCount * 3 / 2) <= 1); +} + - (void)testViewDidLoadDoesntInvokeEngineWhenNotTheViewController { FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 664b4c282b9f8..9f97cb0072ac4 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,22 @@ 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, + CGFloat scale); + @interface FlutterViewController () @property(class, nonatomic, readonly) BOOL accessibilityIsOnOffSwitchLabelsEnabled;