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
3435static constexpr int kMicrosecondsPerSecond = 1000 * 1000 ;
3536static 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