44
44
NSNotificationName const FlutterViewControllerShowHomeIndicator =
45
45
@" FlutterViewControllerShowHomeIndicator" ;
46
46
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
+
47
79
// Struct holding data to help adapt system mouse/trackpad events to embedder events.
48
80
typedef struct MouseState {
49
81
// Current coordinate of the mouse cursor in physical device pixels.
@@ -63,6 +95,11 @@ @interface FlutterViewController () <FlutterBinaryMessenger,
63
95
@property (nonatomic , assign ) BOOL isHomeIndicatorHidden;
64
96
@property (nonatomic , assign ) BOOL isPresentingViewControllerAnimating;
65
97
98
+ /* *
99
+ * Whether the device is rotating.
100
+ */
101
+ @property (nonatomic , assign ) BOOL isDuringRotationTransition;
102
+
66
103
/* *
67
104
* Keyboard animation properties
68
105
*/
@@ -843,6 +880,62 @@ - (void)viewDidDisappear:(BOOL)animated {
843
880
[super viewDidDisappear: animated];
844
881
}
845
882
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
+
846
939
- (void )flushOngoingTouches {
847
940
if (_engine && _ongoingTouches.get ().count > 0 ) {
848
941
auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get ().count );
@@ -1278,7 +1371,11 @@ - (void)pencilInteractionDidTap:(UIPencilInteraction*)interaction API_AVAILABLE(
1278
1371
1279
1372
#pragma mark - Handle view resizing
1280
1373
1281
- - (void )updateViewportMetrics {
1374
+ - (void )updateViewportMetricsIfNeeded : (BOOL )forRotation {
1375
+ // update viewport metrics only if `_isDuringRotationTransition` matches `forRotation`.
1376
+ if (_isDuringRotationTransition != forRotation) {
1377
+ return ;
1378
+ }
1282
1379
if ([_engine.get () viewController ] == self) {
1283
1380
[_engine.get () updateViewportMetrics: _viewportMetrics];
1284
1381
}
@@ -1299,7 +1396,7 @@ - (void)viewDidLayoutSubviews {
1299
1396
_viewportMetrics.physical_height = viewBounds.size .height * scale;
1300
1397
1301
1398
[self updateViewportPadding ];
1302
- [self updateViewportMetrics ];
1399
+ [self updateViewportMetricsIfNeeded: NO ];
1303
1400
1304
1401
// There is no guarantee that UIKit will layout subviews when the application is active. Creating
1305
1402
// the surface when inactive will cause GPU accesses from the background. Only wait for the first
@@ -1329,7 +1426,7 @@ - (void)viewDidLayoutSubviews {
1329
1426
1330
1427
- (void )viewSafeAreaInsetsDidChange {
1331
1428
[self updateViewportPadding ];
1332
- [self updateViewportMetrics ];
1429
+ [self updateViewportMetricsIfNeeded: NO ];
1333
1430
[super viewSafeAreaInsetsDidChange ];
1334
1431
}
1335
1432
@@ -1661,15 +1758,15 @@ - (void)setupKeyboardAnimationVsyncClient {
1661
1758
flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
1662
1759
flutterViewController.get ()
1663
1760
.keyboardAnimationView .layer .presentationLayer .frame .origin .y ;
1664
- [flutterViewController updateViewportMetrics ];
1761
+ [flutterViewController updateViewportMetricsIfNeeded: NO ];
1665
1762
}
1666
1763
} else {
1667
1764
fml::TimeDelta timeElapsed = recorder.get ()->GetVsyncTargetTime () -
1668
1765
flutterViewController.get ().keyboardAnimationStartTime ;
1669
1766
1670
1767
flutterViewController.get ()->_viewportMetrics .physical_view_inset_bottom =
1671
1768
[[flutterViewController keyboardSpringAnimation ] curveFunction: timeElapsed.ToSecondsF ()];
1672
- [flutterViewController updateViewportMetrics ];
1769
+ [flutterViewController updateViewportMetricsIfNeeded: NO ];
1673
1770
}
1674
1771
};
1675
1772
flutter::Shell& shell = [_engine.get () shell ];
@@ -1698,7 +1795,7 @@ - (void)ensureViewportMetricsIsCorrect {
1698
1795
if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom ) {
1699
1796
// Make sure the `physical_view_inset_bottom` is the target value.
1700
1797
_viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom ;
1701
- [self updateViewportMetrics ];
1798
+ [self updateViewportMetricsIfNeeded: NO ];
1702
1799
}
1703
1800
}
1704
1801
0 commit comments