Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b7b91d2

Browse files
committed
[ios_rotation_distortion]use linear interpolation to send different sizes during rotation to fix a problem with aspect ratio distortion
1 parent 6597218 commit b7b91d2

File tree

3 files changed

+301
-11
lines changed

3 files changed

+301
-11
lines changed

shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,38 @@
4444
NSNotificationName 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.
4880
typedef 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

Comments
 (0)