@@ -63,6 +63,11 @@ @interface FlutterViewController () <FlutterBinaryMessenger,
6363@property (nonatomic , assign ) BOOL isHomeIndicatorHidden;
6464@property (nonatomic , assign ) BOOL isPresentingViewControllerAnimating;
6565
66+ /* *
67+ * Whether we should ignore viewport metrics updates during rotation transition.
68+ */
69+ @property (nonatomic , assign ) BOOL shouldIgnoreViewportMetricsUpdatesDuringRotation;
70+
6671/* *
6772 * Keyboard animation properties
6873 */
@@ -843,6 +848,35 @@ - (void)viewDidDisappear:(BOOL)animated {
843848 [super viewDidDisappear: animated];
844849}
845850
851+ - (void )viewWillTransitionToSize : (CGSize)size
852+ withTransitionCoordinator : (id <UIViewControllerTransitionCoordinator>)coordinator {
853+ [super viewWillTransitionToSize: size withTransitionCoordinator: coordinator];
854+
855+ // We delay the viewport metrics update for half of rotation transition duration, to address
856+ // a bug with distorted aspect ratio.
857+ // See: https://github.com/flutter/flutter/issues/16322
858+ //
859+ // This approach does not fully resolve all distortion problem. But instead, it reduces the
860+ // rotation distortion roughly from 4x to 2x. The most distorted frames occur in the middle
861+ // of the transition when it is rotating the fastest, making it hard to notice.
862+
863+ NSTimeInterval transitionDuration = coordinator.transitionDuration ;
864+ // Do not delay viewport metrics update if zero transition duration.
865+ if (transitionDuration == 0 ) {
866+ return ;
867+ }
868+
869+ _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES ;
870+ dispatch_after (dispatch_time (DISPATCH_TIME_NOW,
871+ static_cast <int64_t >(transitionDuration / 2.0 * NSEC_PER_SEC)),
872+ dispatch_get_main_queue (), ^{
873+ // `viewWillTransitionToSize` is only called after the previous rotation is
874+ // complete. So there won't be race condition for this flag.
875+ _shouldIgnoreViewportMetricsUpdatesDuringRotation = NO ;
876+ [self updateViewportMetricsIfNeeded ];
877+ });
878+ }
879+
846880- (void )flushOngoingTouches {
847881 if (_engine && _ongoingTouches.get ().count > 0 ) {
848882 auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get ().count );
@@ -1278,7 +1312,10 @@ - (void)pencilInteractionDidTap:(UIPencilInteraction*)interaction API_AVAILABLE(
12781312
12791313#pragma mark - Handle view resizing
12801314
1281- - (void )updateViewportMetrics {
1315+ - (void )updateViewportMetricsIfNeeded {
1316+ if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1317+ return ;
1318+ }
12821319 if ([_engine.get () viewController ] == self) {
12831320 [_engine.get () updateViewportMetrics: _viewportMetrics];
12841321 }
@@ -1295,11 +1332,9 @@ - (void)viewDidLayoutSubviews {
12951332 // First time since creation that the dimensions of its view is known.
12961333 bool firstViewBoundsUpdate = !_viewportMetrics.physical_width ;
12971334 _viewportMetrics.device_pixel_ratio = scale;
1298- _viewportMetrics.physical_width = viewBounds.size .width * scale;
1299- _viewportMetrics.physical_height = viewBounds.size .height * scale;
1300-
1301- [self updateViewportPadding ];
1302- [self updateViewportMetrics ];
1335+ [self setViewportMetricsSize ];
1336+ [self setViewportMetricsPaddings ];
1337+ [self updateViewportMetricsIfNeeded ];
13031338
13041339 // There is no guarantee that UIKit will layout subviews when the application is active. Creating
13051340 // the surface when inactive will cause GPU accesses from the background. Only wait for the first
@@ -1328,15 +1363,27 @@ - (void)viewDidLayoutSubviews {
13281363}
13291364
13301365- (void )viewSafeAreaInsetsDidChange {
1331- [self updateViewportPadding ];
1332- [self updateViewportMetrics ];
1366+ [self setViewportMetricsPaddings ];
1367+ [self updateViewportMetricsIfNeeded ];
13331368 [super viewSafeAreaInsetsDidChange ];
13341369}
13351370
1336- // Updates _viewportMetrics physical padding.
1371+ // Set _viewportMetrics physical size.
1372+ - (void )setViewportMetricsSize {
1373+ // TODO(hellohuanlin): Use [self mainScreenIfViewLoaded] instead of [UIScreen mainScreen].
1374+ // This requires adding the view to window during unit tests, which calls multiple engine calls
1375+ // that is hard to mock since they take/return structs. An alternative approach is to partial mock
1376+ // the FlutterViewController to make view controller life cycle methods no-op, and insert
1377+ // this mock into the responder chain.
1378+ CGFloat scale = [UIScreen mainScreen ].scale ;
1379+ _viewportMetrics.physical_width = self.view .bounds .size .width * scale;
1380+ _viewportMetrics.physical_height = self.view .bounds .size .height * scale;
1381+ }
1382+
1383+ // Set _viewportMetrics physical paddings.
13371384//
1338- // Viewport padding represents the iOS safe area insets.
1339- - (void )updateViewportPadding {
1385+ // Viewport paddings represent the iOS safe area insets.
1386+ - (void )setViewportMetricsPaddings {
13401387 CGFloat scale = [UIScreen mainScreen ].scale ;
13411388 _viewportMetrics.physical_padding_top = self.view .safeAreaInsets .top * scale;
13421389 _viewportMetrics.physical_padding_left = self.view .safeAreaInsets .left * scale;
@@ -1661,15 +1708,15 @@ - (void)setupKeyboardAnimationVsyncClient {
16611708 flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
16621709 flutterViewController.get ()
16631710 .keyboardAnimationView .layer .presentationLayer .frame .origin .y ;
1664- [flutterViewController updateViewportMetrics ];
1711+ [flutterViewController updateViewportMetricsIfNeeded ];
16651712 }
16661713 } else {
16671714 fml::TimeDelta timeElapsed = recorder.get ()->GetVsyncTargetTime () -
16681715 flutterViewController.get ().keyboardAnimationStartTime ;
16691716
16701717 flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
16711718 [[flutterViewController keyboardSpringAnimation ] curveFunction: timeElapsed.ToSecondsF ()];
1672- [flutterViewController updateViewportMetrics ];
1719+ [flutterViewController updateViewportMetricsIfNeeded ];
16731720 }
16741721 };
16751722 flutter::Shell& shell = [_engine.get () shell ];
@@ -1698,7 +1745,7 @@ - (void)ensureViewportMetricsIsCorrect {
16981745 if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom ) {
16991746 // Make sure the `physical_view_inset_bottom` is the target value.
17001747 _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom ;
1701- [self updateViewportMetrics ];
1748+ [self updateViewportMetricsIfNeeded ];
17021749 }
17031750}
17041751
0 commit comments