4444NSNotificationName const FlutterViewControllerShowHomeIndicator =
4545 @" FlutterViewControllerShowHomeIndicator" ;
4646
47+ /* *
48+ * Compute the interpolated value under linear interpolation.
49+ */
50+ CGFloat FLTLinearInterpolatedValue (double progress, CGFloat from, CGFloat to, CGFloat scale) {
51+ NSCAssert (progress >= 0 && progress <= 1 , @" progress must be between 0 and 1" );
52+ return (from * (1 - progress) + to * progress) * scale;
53+ }
54+
55+ /* *
56+ * Interpolate the viewport metrics for smoother rotation transition.
57+ */
58+ void FLTInterpolateViewportMetrics (flutter::ViewportMetrics& viewportMetrics,
59+ double rotationProgress,
60+ CGSize fromSize,
61+ UIEdgeInsets fromPadding,
62+ CGSize toSize,
63+ UIEdgeInsets toPadding) {
64+ CGFloat scale = [UIScreen mainScreen ].scale ;
65+ viewportMetrics.physical_width =
66+ FLTLinearInterpolatedValue (rotationProgress, fromSize.width , toSize.width , scale);
67+ viewportMetrics.physical_height =
68+ FLTLinearInterpolatedValue (rotationProgress, fromSize.height , toSize.height , scale);
69+ viewportMetrics.physical_padding_top =
70+ FLTLinearInterpolatedValue (rotationProgress, fromPadding.top , toPadding.top , scale);
71+ viewportMetrics.physical_padding_left =
72+ FLTLinearInterpolatedValue (rotationProgress, fromPadding.left , toPadding.left , scale);
73+ viewportMetrics.physical_padding_bottom =
74+ FLTLinearInterpolatedValue (rotationProgress, fromPadding.bottom , toPadding.bottom , scale);
75+ viewportMetrics.physical_padding_right =
76+ FLTLinearInterpolatedValue (rotationProgress, fromPadding.right , toPadding.right , scale);
77+ }
78+
4779// Struct holding data to help adapt system mouse/trackpad events to embedder events.
4880typedef struct MouseState {
4981 // Current coordinate of the mouse cursor in physical device pixels.
@@ -63,6 +95,11 @@ @interface FlutterViewController () <FlutterBinaryMessenger,
6395@property (nonatomic , assign ) BOOL isHomeIndicatorHidden;
6496@property (nonatomic , assign ) BOOL isPresentingViewControllerAnimating;
6597
98+ /* *
99+ * Whether the device is rotating.
100+ */
101+ @property (nonatomic , assign ) BOOL isDuringRotationTransition;
102+
66103/* *
67104 * Keyboard animation properties
68105 */
@@ -843,6 +880,62 @@ - (void)viewDidDisappear:(BOOL)animated {
843880 [super viewDidDisappear: animated];
844881}
845882
883+ - (void )viewWillTransitionToSize : (CGSize)size
884+ withTransitionCoordinator : (id <UIViewControllerTransitionCoordinator>)coordinator {
885+ [super viewWillTransitionToSize: size withTransitionCoordinator: coordinator];
886+
887+ // We interpolate the viewport metrics (size and paddings) during rotation transition, to address
888+ // a bug with distorted aspect ratio.
889+ // See: https://github.com/flutter/flutter/issues/16322
890+ //
891+ // For every `kRotationViewportMetricsUpdateInterval`, we send the metrics which is interpolated
892+ // between the old metrics before the rotation transition, to the new metrics after the rotation
893+ // transition.
894+ //
895+ // Currently it is using linear interpolation. Using non-linear ease-in/out interpolation may
896+ // achieve better results. It may also help to send only rotation info (such as rotation duration)
897+ // and perform the interpolation on the framework side, to reduce engine/framework communication.
898+ // However, since flutter's drawing happens on the ui thread, which is not iOS main thread,
899+ // there is no guarantee that the viewport metrics change is immediately taken effect, resulting
900+ // in some amount of unavoidable distortion.
901+
902+ NSTimeInterval transitionDuration = coordinator.transitionDuration ;
903+ // Do not interpolate if zero transition duration.
904+ if (transitionDuration == 0 ) {
905+ return ;
906+ }
907+
908+ _isDuringRotationTransition = YES ;
909+
910+ CGSize oldSize = self.view .bounds .size ;
911+ UIEdgeInsets oldPadding = self.view .safeAreaInsets ;
912+
913+ __block double rotationProgress = 0 ;
914+ // Timer is retained by the run loop, and will be released after invalidated.
915+ [NSTimer
916+ scheduledTimerWithTimeInterval: kRotationViewportMetricsUpdateInterval
917+ repeats: YES
918+ block: ^(NSTimer * timer) {
919+ double progressDelta =
920+ kRotationViewportMetricsUpdateInterval / transitionDuration;
921+ rotationProgress = fmin (1 , rotationProgress + progressDelta);
922+
923+ CGSize newSize = self.view .bounds .size ;
924+ UIEdgeInsets newPadding = self.view .safeAreaInsets ;
925+
926+ FLTInterpolateViewportMetrics (_viewportMetrics, rotationProgress,
927+ oldSize, oldPadding, newSize,
928+ newPadding);
929+ [self updateViewportMetricsIfNeeded: YES ];
930+
931+ // End of rotation. Invalidate the timer.
932+ if (rotationProgress == 1 ) {
933+ _isDuringRotationTransition = NO ;
934+ [timer invalidate ];
935+ }
936+ }];
937+ }
938+
846939- (void )flushOngoingTouches {
847940 if (_engine && _ongoingTouches.get ().count > 0 ) {
848941 auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get ().count );
@@ -1278,7 +1371,11 @@ - (void)pencilInteractionDidTap:(UIPencilInteraction*)interaction API_AVAILABLE(
12781371
12791372#pragma mark - Handle view resizing
12801373
1281- - (void )updateViewportMetrics {
1374+ - (void )updateViewportMetricsIfNeeded : (BOOL )forRotation {
1375+ // update viewport metrics only if `_isDuringRotationTransition` matches `forRotation`.
1376+ if (_isDuringRotationTransition != forRotation) {
1377+ return ;
1378+ }
12821379 if ([_engine.get () viewController ] == self) {
12831380 [_engine.get () updateViewportMetrics: _viewportMetrics];
12841381 }
@@ -1299,7 +1396,7 @@ - (void)viewDidLayoutSubviews {
12991396 _viewportMetrics.physical_height = viewBounds.size .height * scale;
13001397
13011398 [self updateViewportPadding ];
1302- [self updateViewportMetrics ];
1399+ [self updateViewportMetricsIfNeeded: NO ];
13031400
13041401 // There is no guarantee that UIKit will layout subviews when the application is active. Creating
13051402 // the surface when inactive will cause GPU accesses from the background. Only wait for the first
@@ -1329,7 +1426,7 @@ - (void)viewDidLayoutSubviews {
13291426
13301427- (void )viewSafeAreaInsetsDidChange {
13311428 [self updateViewportPadding ];
1332- [self updateViewportMetrics ];
1429+ [self updateViewportMetricsIfNeeded: NO ];
13331430 [super viewSafeAreaInsetsDidChange ];
13341431}
13351432
@@ -1661,15 +1758,15 @@ - (void)setupKeyboardAnimationVsyncClient {
16611758 flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
16621759 flutterViewController.get ()
16631760 .keyboardAnimationView .layer .presentationLayer .frame .origin .y ;
1664- [flutterViewController updateViewportMetrics ];
1761+ [flutterViewController updateViewportMetricsIfNeeded: NO ];
16651762 }
16661763 } else {
16671764 fml::TimeDelta timeElapsed = recorder.get ()->GetVsyncTargetTime () -
16681765 flutterViewController.get ().keyboardAnimationStartTime ;
16691766
16701767 flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
16711768 [[flutterViewController keyboardSpringAnimation ] curveFunction: timeElapsed.ToSecondsF ()];
1672- [flutterViewController updateViewportMetrics ];
1769+ [flutterViewController updateViewportMetricsIfNeeded: NO ];
16731770 }
16741771 };
16751772 flutter::Shell& shell = [_engine.get () shell ];
@@ -1698,7 +1795,7 @@ - (void)ensureViewportMetricsIsCorrect {
16981795 if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom ) {
16991796 // Make sure the `physical_view_inset_bottom` is the target value.
17001797 _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom ;
1701- [self updateViewportMetrics ];
1798+ [self updateViewportMetricsIfNeeded: NO ];
17021799 }
17031800}
17041801
0 commit comments