@@ -61,6 +61,11 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
6161@property (nonatomic , assign ) BOOL isHomeIndicatorHidden;
6262@property (nonatomic , assign ) BOOL isPresentingViewControllerAnimating;
6363
64+ /* *
65+ * Whether we should ignore viewport metrics updates during rotation transition.
66+ */
67+ @property (nonatomic , assign ) BOOL shouldIgnoreViewportMetricsUpdatesDuringRotation;
68+
6469/* *
6570 * Keyboard animation properties
6671 */
@@ -837,6 +842,35 @@ - (void)viewDidDisappear:(BOOL)animated {
837842 [super viewDidDisappear: animated];
838843}
839844
845+ - (void )viewWillTransitionToSize : (CGSize)size
846+ withTransitionCoordinator : (id <UIViewControllerTransitionCoordinator>)coordinator {
847+ [super viewWillTransitionToSize: size withTransitionCoordinator: coordinator];
848+
849+ // We delay the viewport metrics update for half of rotation transition duration, to address
850+ // a bug with distorted aspect ratio.
851+ // See: https://github.com/flutter/flutter/issues/16322
852+ //
853+ // This approach does not fully resolve all distortion problem. But instead, it reduces the
854+ // rotation distortion roughly from 4x to 2x. The most distorted frames occur in the middle
855+ // of the transition when it is rotating the fastest, making it hard to notice.
856+
857+ NSTimeInterval transitionDuration = coordinator.transitionDuration ;
858+ // Do not delay viewport metrics update if zero transition duration.
859+ if (transitionDuration == 0 ) {
860+ return ;
861+ }
862+
863+ _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES ;
864+ dispatch_after (dispatch_time (DISPATCH_TIME_NOW,
865+ static_cast <int64_t >(transitionDuration / 2.0 * NSEC_PER_SEC)),
866+ dispatch_get_main_queue (), ^{
867+ // `viewWillTransitionToSize` is only called after the previous rotation is
868+ // complete. So there won't be race condition for this flag.
869+ _shouldIgnoreViewportMetricsUpdatesDuringRotation = NO ;
870+ [self updateViewportMetricsIfNeeded ];
871+ });
872+ }
873+
840874- (void )flushOngoingTouches {
841875 if (_engine && _ongoingTouches.get ().count > 0 ) {
842876 auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get ().count );
@@ -1226,7 +1260,10 @@ - (void)invalidateTouchRateCorrectionVSyncClient {
12261260
12271261#pragma mark - Handle view resizing
12281262
1229- - (void )updateViewportMetrics {
1263+ - (void )updateViewportMetricsIfNeeded {
1264+ if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1265+ return ;
1266+ }
12301267 if ([_engine.get () viewController ] == self) {
12311268 [_engine.get () updateViewportMetrics: _viewportMetrics];
12321269 }
@@ -1243,11 +1280,9 @@ - (void)viewDidLayoutSubviews {
12431280 // First time since creation that the dimensions of its view is known.
12441281 bool firstViewBoundsUpdate = !_viewportMetrics.physical_width ;
12451282 _viewportMetrics.device_pixel_ratio = scale;
1246- _viewportMetrics.physical_width = viewBounds.size .width * scale;
1247- _viewportMetrics.physical_height = viewBounds.size .height * scale;
1248-
1249- [self updateViewportPadding ];
1250- [self updateViewportMetrics ];
1283+ [self setViewportMetricsSize ];
1284+ [self setViewportMetricsPaddings ];
1285+ [self updateViewportMetricsIfNeeded ];
12511286
12521287 // There is no guarantee that UIKit will layout subviews when the application is active. Creating
12531288 // the surface when inactive will cause GPU accesses from the background. Only wait for the first
@@ -1276,15 +1311,27 @@ - (void)viewDidLayoutSubviews {
12761311}
12771312
12781313- (void )viewSafeAreaInsetsDidChange {
1279- [self updateViewportPadding ];
1280- [self updateViewportMetrics ];
1314+ [self setViewportMetricsPaddings ];
1315+ [self updateViewportMetricsIfNeeded ];
12811316 [super viewSafeAreaInsetsDidChange ];
12821317}
12831318
1284- // Updates _viewportMetrics physical padding.
1319+ // Set _viewportMetrics physical size.
1320+ - (void )setViewportMetricsSize {
1321+ // TODO(hellohuanlin): Use [self mainScreenIfViewLoaded] instead of [UIScreen mainScreen].
1322+ // This requires adding the view to window during unit tests, which calls multiple engine calls
1323+ // that is hard to mock since they take/return structs. An alternative approach is to partial mock
1324+ // the FlutterViewController to make view controller life cycle methods no-op, and insert
1325+ // this mock into the responder chain.
1326+ CGFloat scale = [UIScreen mainScreen ].scale ;
1327+ _viewportMetrics.physical_width = self.view .bounds .size .width * scale;
1328+ _viewportMetrics.physical_height = self.view .bounds .size .height * scale;
1329+ }
1330+
1331+ // Set _viewportMetrics physical paddings.
12851332//
1286- // Viewport padding represents the iOS safe area insets.
1287- - (void )updateViewportPadding {
1333+ // Viewport paddings represent the iOS safe area insets.
1334+ - (void )setViewportMetricsPaddings {
12881335 CGFloat scale = [UIScreen mainScreen ].scale ;
12891336 _viewportMetrics.physical_padding_top = self.view .safeAreaInsets .top * scale;
12901337 _viewportMetrics.physical_padding_left = self.view .safeAreaInsets .left * scale;
@@ -1609,15 +1656,15 @@ - (void)setupKeyboardAnimationVsyncClient {
16091656 flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
16101657 flutterViewController.get ()
16111658 .keyboardAnimationView .layer .presentationLayer .frame .origin .y ;
1612- [flutterViewController updateViewportMetrics ];
1659+ [flutterViewController updateViewportMetricsIfNeeded ];
16131660 }
16141661 } else {
16151662 fml::TimeDelta timeElapsed = recorder.get ()->GetVsyncTargetTime () -
16161663 flutterViewController.get ().keyboardAnimationStartTime ;
16171664
16181665 flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
16191666 [[flutterViewController keyboardSpringAnimation ] curveFunction: timeElapsed.ToSecondsF ()];
1620- [flutterViewController updateViewportMetrics ];
1667+ [flutterViewController updateViewportMetricsIfNeeded ];
16211668 }
16221669 };
16231670 flutter::Shell& shell = [_engine.get () shell ];
@@ -1646,7 +1693,7 @@ - (void)ensureViewportMetricsIsCorrect {
16461693 if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom ) {
16471694 // Make sure the `physical_view_inset_bottom` is the target value.
16481695 _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom ;
1649- [self updateViewportMetrics ];
1696+ [self updateViewportMetricsIfNeeded ];
16501697 }
16511698}
16521699
0 commit comments