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

Commit 165126e

Browse files
CoolDude53jmagman
andauthored
iOS keyboard animation synchronization (#37604)
* first keyboard commit * first keyboard commit * cleaned up from initial testing * added keyboard animation stop calculation * added keyboard animation stop calculation * lucky updates :) * type change * final touches * final touches * final touches * removed KeyboardAnimationView class * removed KeyboardAnimationView class * dynamic keyboard spring curve implementation * broke out spring curve to own objc files * broke out spring curve to own objc files * simplified and added test * modified spring formula to use damping * added logic for compounding simultaneous animation calls * remove unnecessary code * cleanup * cleanup * update springCurveIos file * cleaning up * simple optimizations * comment update * bool update * credit spring calculation project * improved setupKeyboardAnimationCureveIfNeeded tests Improved setupKeyboardAnimationCurveIfNeeded tests * Optimized compounding animation checks and added new tests * changed viewport update logic * Update FlutterViewControllerTest.mm Updated test nits * Updated license * Updated tests * Cleanup * Migrated spring class based on React * Updated syntax * Update shell/platform/darwin/ios/framework/Source/spring_curve_ios.mm Co-authored-by: Jenn Magder <[email protected]> * Update shell/platform/darwin/ios/framework/Source/spring_curve_ios.h Co-authored-by: Jenn Magder <[email protected]> * Updated naming * Updated naming * Optimized spring model calculations * Improved interpolation accuracy * Revert "Improved interpolation accuracy" This reverts commit 6d02512. * Updated comment * Updated spring curve comments * updated to work with third_party spring animation * updated to use SpringAnimation to/from values * updated license (removed old files) * updates * allow updating springanimation position values * updated test * update * fixes --------- Co-authored-by: Jenn Magder <[email protected]>
1 parent 0315d06 commit 165126e

File tree

6 files changed

+278
-38
lines changed

6 files changed

+278
-38
lines changed

shell/platform/darwin/ios/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ source_set("flutter_framework_source") {
187187
"//flutter/shell/platform/darwin/common:framework_shared",
188188
"//flutter/shell/platform/embedder:embedder_as_internal_library",
189189
"//flutter/shell/profiling:profiling",
190+
"//flutter/third_party/spring_animation",
190191
"//third_party/skia",
191192
]
192193

@@ -301,6 +302,7 @@ shared_library("ios_test_flutter") {
301302
"//flutter/shell/platform/darwin/common:framework_shared",
302303
"//flutter/shell/platform/embedder:embedder_as_internal_library",
303304
"//flutter/shell/platform/embedder:embedder_test_utils",
305+
"//flutter/third_party/spring_animation",
304306
"//flutter/third_party/tonic",
305307
"//flutter/third_party/txt",
306308
"//third_party/ocmock:ocmock_shared",

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

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
3131
#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
3232
#import "flutter/shell/platform/embedder/embedder.h"
33+
#import "flutter/third_party/spring_animation/spring_animation.h"
3334

3435
static constexpr int kMicrosecondsPerSecond = 1000 * 1000;
3536
static constexpr CGFloat kScrollViewContentSize = 2.0;
@@ -65,6 +66,9 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
6566
*/
6667
@property(nonatomic, assign) double targetViewInsetBottom;
6768
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;
69+
@property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
70+
@property(nonatomic, assign) fml::TimePoint keyboardAnimationStartTime;
71+
@property(nonatomic, assign) CGFloat originalViewInsetBottom;
6872
@property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;
6973

7074
/// VSyncClient for touch events delivery frame rate correction.
@@ -123,6 +127,7 @@ @implementation FlutterViewController {
123127
// https://github.com/flutter/flutter/issues/35050
124128
fml::scoped_nsobject<UIScrollView> _scrollView;
125129
fml::scoped_nsobject<UIView> _keyboardAnimationView;
130+
fml::scoped_nsobject<SpringAnimation> _keyboardSpringAnimation;
126131
MouseState _mouseState;
127132
// Timestamp after which a scroll inertia cancel event should be inferred.
128133
NSTimeInterval _scrollInertiaEventStartline;
@@ -594,6 +599,10 @@ - (UIView*)keyboardAnimationView {
594599
return _keyboardAnimationView.get();
595600
}
596601

602+
- (SpringAnimation*)keyboardSpringAnimation {
603+
return _keyboardSpringAnimation.get();
604+
}
605+
597606
- (UIScreen*)mainScreenIfViewLoaded {
598607
if (@available(iOS 13.0, *)) {
599608
if (self.viewIfLoaded == nil) {
@@ -1314,13 +1323,14 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification {
13141323
}
13151324

13161325
- (void)handleKeyboardNotification:(NSNotification*)notification {
1317-
// See https:://flutter.dev/go/ios-keyboard-calculating-inset for more details
1326+
// See https://flutter.dev/go/ios-keyboard-calculating-inset for more details
13181327
// on why notifications are used and how things are calculated.
13191328
if ([self shouldIgnoreKeyboardNotification:notification]) {
13201329
return;
13211330
}
13221331

13231332
NSDictionary* info = notification.userInfo;
1333+
CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
13241334
CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
13251335
FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification];
13261336
CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];
@@ -1332,7 +1342,24 @@ - (void)handleKeyboardNotification:(NSNotification*)notification {
13321342

13331343
self.targetViewInsetBottom = calculatedInset;
13341344
NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
1335-
[self startKeyBoardAnimation:duration];
1345+
1346+
// Flag for simultaneous compounding animation calls.
1347+
// This captures animation calls made while the keyboard animation is currently animating. If the
1348+
// new animation is in the same direction as the current animation, this flag lets the current
1349+
// animation continue with an updated targetViewInsetBottom instead of starting a new keyboard
1350+
// animation. This allows for smoother keyboard animation interpolation.
1351+
BOOL keyboardWillShow = beginKeyboardFrame.origin.y > keyboardFrame.origin.y;
1352+
BOOL keyboardAnimationIsCompounding =
1353+
self.keyboardAnimationIsShowing == keyboardWillShow && _keyboardAnimationVSyncClient != nil;
1354+
1355+
// Mark keyboard as showing or hiding.
1356+
self.keyboardAnimationIsShowing = keyboardWillShow;
1357+
1358+
if (!keyboardAnimationIsCompounding) {
1359+
[self startKeyBoardAnimation:duration];
1360+
} else if ([self keyboardSpringAnimation]) {
1361+
[self keyboardSpringAnimation].toValue = self.targetViewInsetBottom;
1362+
}
13361363
}
13371364

13381365
- (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification {
@@ -1494,12 +1521,12 @@ - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger)
14941521
}
14951522

14961523
- (void)startKeyBoardAnimation:(NSTimeInterval)duration {
1497-
// If current physical_view_inset_bottom == targetViewInsetBottom,do nothing.
1524+
// If current physical_view_inset_bottom == targetViewInsetBottom, do nothing.
14981525
if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom) {
14991526
return;
15001527
}
15011528

1502-
// When call this method first time,
1529+
// When this method is called for the first time,
15031530
// initialize the keyboardAnimationView to get animation interpolation during animation.
15041531
if ([self keyboardAnimationView] == nil) {
15051532
UIView* keyboardAnimationView = [[UIView alloc] init];
@@ -1514,9 +1541,11 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
15141541
// Remove running animation when start another animation.
15151542
[[self keyboardAnimationView].layer removeAllAnimations];
15161543

1517-
// Set animation begin value.
1544+
// Set animation begin value and DisplayLink tracking values.
15181545
[self keyboardAnimationView].frame =
15191546
CGRectMake(0, _viewportMetrics.physical_view_inset_bottom, 0, 0);
1547+
self.keyboardAnimationStartTime = fml::TimePoint().Now();
1548+
self.originalViewInsetBottom = _viewportMetrics.physical_view_inset_bottom;
15201549

15211550
// Invalidate old vsync client if old animation is not completed.
15221551
[self invalidateKeyboardAnimationVSyncClient];
@@ -1527,6 +1556,11 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
15271556
animations:^{
15281557
// Set end value.
15291558
[self keyboardAnimationView].frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0);
1559+
1560+
// Setup keyboard animation interpolation.
1561+
CAAnimation* keyboardAnimation =
1562+
[[self keyboardAnimationView].layer animationForKey:@"position"];
1563+
[self setupKeyboardSpringAnimationIfNeeded:keyboardAnimation];
15301564
}
15311565
completion:^(BOOL finished) {
15321566
if (_keyboardAnimationVSyncClient == currentVsyncClient) {
@@ -1540,6 +1574,24 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
15401574
}];
15411575
}
15421576

1577+
- (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
1578+
// If keyboard animation is null or not a spring animation, fallback to DisplayLink tracking.
1579+
if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) {
1580+
_keyboardSpringAnimation.reset();
1581+
return;
1582+
}
1583+
1584+
// Setup keyboard spring animation details for spring curve animation calculation.
1585+
CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation;
1586+
_keyboardSpringAnimation.reset([[SpringAnimation alloc]
1587+
initWithStiffness:keyboardCASpringAnimation.stiffness
1588+
damping:keyboardCASpringAnimation.damping
1589+
mass:keyboardCASpringAnimation.mass
1590+
initialVelocity:keyboardCASpringAnimation.initialVelocity
1591+
fromValue:self.originalViewInsetBottom
1592+
toValue:self.targetViewInsetBottom]);
1593+
}
1594+
15431595
- (void)setupKeyboardAnimationVsyncClient {
15441596
auto callback = [weakSelf =
15451597
[self getWeakPtr]](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
@@ -1556,10 +1608,20 @@ - (void)setupKeyboardAnimationVsyncClient {
15561608
// Ensure the keyboardAnimationView is in view hierarchy when animation running.
15571609
[flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]];
15581610
}
1559-
if ([flutterViewController keyboardAnimationView].layer.presentationLayer) {
1560-
CGFloat value =
1561-
[flutterViewController keyboardAnimationView].layer.presentationLayer.frame.origin.y;
1562-
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = value;
1611+
1612+
if ([flutterViewController keyboardSpringAnimation] == nil) {
1613+
if (flutterViewController.get().keyboardAnimationView.layer.presentationLayer) {
1614+
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1615+
flutterViewController.get()
1616+
.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
1617+
[flutterViewController updateViewportMetrics];
1618+
}
1619+
} else {
1620+
fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() -
1621+
flutterViewController.get().keyboardAnimationStartTime;
1622+
1623+
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1624+
[[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()];
15631625
[flutterViewController updateViewportMetrics];
15641626
}
15651627
};
@@ -1913,8 +1975,8 @@ - (BOOL)isAlwaysUse24HourFormat {
19131975
}
19141976

19151977
// The brightness mode of the platform, e.g., light or dark, expressed as a string that
1916-
// is understood by the Flutter framework. See the settings system channel for more
1917-
// information.
1978+
// is understood by the Flutter framework. See the settings
1979+
// system channel for more information.
19181980
- (NSString*)brightnessMode {
19191981
if (@available(iOS 13, *)) {
19201982
UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;

0 commit comments

Comments
 (0)