@@ -61,6 +61,11 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
61
61
@property (nonatomic , assign ) BOOL isHomeIndicatorHidden;
62
62
@property (nonatomic , assign ) BOOL isPresentingViewControllerAnimating;
63
63
64
+ /* *
65
+ * Whether we should ignore viewport metrics updates during rotation transition.
66
+ */
67
+ @property (nonatomic , assign ) BOOL shouldIgnoreViewportMetricsUpdatesDuringRotation;
68
+
64
69
/* *
65
70
* Keyboard animation properties
66
71
*/
@@ -837,6 +842,35 @@ - (void)viewDidDisappear:(BOOL)animated {
837
842
[super viewDidDisappear: animated];
838
843
}
839
844
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
+
840
874
- (void )flushOngoingTouches {
841
875
if (_engine && _ongoingTouches.get ().count > 0 ) {
842
876
auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get ().count );
@@ -1226,7 +1260,10 @@ - (void)invalidateTouchRateCorrectionVSyncClient {
1226
1260
1227
1261
#pragma mark - Handle view resizing
1228
1262
1229
- - (void )updateViewportMetrics {
1263
+ - (void )updateViewportMetricsIfNeeded {
1264
+ if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1265
+ return ;
1266
+ }
1230
1267
if ([_engine.get () viewController ] == self) {
1231
1268
[_engine.get () updateViewportMetrics: _viewportMetrics];
1232
1269
}
@@ -1243,11 +1280,9 @@ - (void)viewDidLayoutSubviews {
1243
1280
// First time since creation that the dimensions of its view is known.
1244
1281
bool firstViewBoundsUpdate = !_viewportMetrics.physical_width ;
1245
1282
_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 ];
1251
1286
1252
1287
// There is no guarantee that UIKit will layout subviews when the application is active. Creating
1253
1288
// the surface when inactive will cause GPU accesses from the background. Only wait for the first
@@ -1276,15 +1311,27 @@ - (void)viewDidLayoutSubviews {
1276
1311
}
1277
1312
1278
1313
- (void )viewSafeAreaInsetsDidChange {
1279
- [self updateViewportPadding ];
1280
- [self updateViewportMetrics ];
1314
+ [self setViewportMetricsPaddings ];
1315
+ [self updateViewportMetricsIfNeeded ];
1281
1316
[super viewSafeAreaInsetsDidChange ];
1282
1317
}
1283
1318
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.
1285
1332
//
1286
- // Viewport padding represents the iOS safe area insets.
1287
- - (void )updateViewportPadding {
1333
+ // Viewport paddings represent the iOS safe area insets.
1334
+ - (void )setViewportMetricsPaddings {
1288
1335
CGFloat scale = [UIScreen mainScreen ].scale ;
1289
1336
_viewportMetrics.physical_padding_top = self.view .safeAreaInsets .top * scale;
1290
1337
_viewportMetrics.physical_padding_left = self.view .safeAreaInsets .left * scale;
@@ -1609,15 +1656,15 @@ - (void)setupKeyboardAnimationVsyncClient {
1609
1656
flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
1610
1657
flutterViewController.get ()
1611
1658
.keyboardAnimationView .layer .presentationLayer .frame .origin .y ;
1612
- [flutterViewController updateViewportMetrics ];
1659
+ [flutterViewController updateViewportMetricsIfNeeded ];
1613
1660
}
1614
1661
} else {
1615
1662
fml::TimeDelta timeElapsed = recorder.get ()->GetVsyncTargetTime () -
1616
1663
flutterViewController.get ().keyboardAnimationStartTime ;
1617
1664
1618
1665
flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
1619
1666
[[flutterViewController keyboardSpringAnimation ] curveFunction: timeElapsed.ToSecondsF ()];
1620
- [flutterViewController updateViewportMetrics ];
1667
+ [flutterViewController updateViewportMetricsIfNeeded ];
1621
1668
}
1622
1669
};
1623
1670
flutter::Shell& shell = [_engine.get () shell ];
@@ -1646,7 +1693,7 @@ - (void)ensureViewportMetricsIsCorrect {
1646
1693
if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom ) {
1647
1694
// Make sure the `physical_view_inset_bottom` is the target value.
1648
1695
_viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom ;
1649
- [self updateViewportMetrics ];
1696
+ [self updateViewportMetricsIfNeeded ];
1650
1697
}
1651
1698
}
1652
1699
0 commit comments