30
30
#import " flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
31
31
#import " flutter/shell/platform/darwin/ios/platform_view_ios.h"
32
32
#import " flutter/shell/platform/embedder/embedder.h"
33
+ #import " flutter/third_party/spring_animation/spring_animation.h"
33
34
34
35
static constexpr int kMicrosecondsPerSecond = 1000 * 1000 ;
35
36
static constexpr CGFloat kScrollViewContentSize = 2.0 ;
@@ -65,6 +66,9 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
65
66
*/
66
67
@property (nonatomic , assign ) double targetViewInsetBottom;
67
68
@property (nonatomic , retain ) VSyncClient* keyboardAnimationVSyncClient;
69
+ @property (nonatomic , assign ) BOOL keyboardAnimationIsShowing;
70
+ @property (nonatomic , assign ) fml::TimePoint keyboardAnimationStartTime;
71
+ @property (nonatomic , assign ) CGFloat originalViewInsetBottom;
68
72
@property (nonatomic , assign ) BOOL isKeyboardInOrTransitioningFromBackground;
69
73
70
74
// / VSyncClient for touch events delivery frame rate correction.
@@ -123,6 +127,7 @@ @implementation FlutterViewController {
123
127
// https://github.com/flutter/flutter/issues/35050
124
128
fml::scoped_nsobject<UIScrollView> _scrollView;
125
129
fml::scoped_nsobject<UIView> _keyboardAnimationView;
130
+ fml::scoped_nsobject<SpringAnimation> _keyboardSpringAnimation;
126
131
MouseState _mouseState;
127
132
// Timestamp after which a scroll inertia cancel event should be inferred.
128
133
NSTimeInterval _scrollInertiaEventStartline;
@@ -594,6 +599,10 @@ - (UIView*)keyboardAnimationView {
594
599
return _keyboardAnimationView.get ();
595
600
}
596
601
602
+ - (SpringAnimation*)keyboardSpringAnimation {
603
+ return _keyboardSpringAnimation.get ();
604
+ }
605
+
597
606
- (UIScreen*)mainScreenIfViewLoaded {
598
607
if (@available (iOS 13.0 , *)) {
599
608
if (self.viewIfLoaded == nil ) {
@@ -1314,13 +1323,14 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification {
1314
1323
}
1315
1324
1316
1325
- (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
1318
1327
// on why notifications are used and how things are calculated.
1319
1328
if ([self shouldIgnoreKeyboardNotification: notification]) {
1320
1329
return ;
1321
1330
}
1322
1331
1323
1332
NSDictionary * info = notification.userInfo ;
1333
+ CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue ];
1324
1334
CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue ];
1325
1335
FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode: notification];
1326
1336
CGFloat calculatedInset = [self calculateKeyboardInset: keyboardFrame keyboardMode: keyboardMode];
@@ -1332,7 +1342,24 @@ - (void)handleKeyboardNotification:(NSNotification*)notification {
1332
1342
1333
1343
self.targetViewInsetBottom = calculatedInset;
1334
1344
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
+ }
1336
1363
}
1337
1364
1338
1365
- (BOOL )shouldIgnoreKeyboardNotification : (NSNotification *)notification {
@@ -1494,12 +1521,12 @@ - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger)
1494
1521
}
1495
1522
1496
1523
- (void )startKeyBoardAnimation : (NSTimeInterval )duration {
1497
- // If current physical_view_inset_bottom == targetViewInsetBottom,do nothing.
1524
+ // If current physical_view_inset_bottom == targetViewInsetBottom, do nothing.
1498
1525
if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom ) {
1499
1526
return ;
1500
1527
}
1501
1528
1502
- // When call this method first time,
1529
+ // When this method is called for the first time,
1503
1530
// initialize the keyboardAnimationView to get animation interpolation during animation.
1504
1531
if ([self keyboardAnimationView ] == nil ) {
1505
1532
UIView* keyboardAnimationView = [[UIView alloc ] init ];
@@ -1514,9 +1541,11 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
1514
1541
// Remove running animation when start another animation.
1515
1542
[[self keyboardAnimationView ].layer removeAllAnimations ];
1516
1543
1517
- // Set animation begin value.
1544
+ // Set animation begin value and DisplayLink tracking values .
1518
1545
[self keyboardAnimationView ].frame =
1519
1546
CGRectMake (0 , _viewportMetrics.physical_view_inset_bottom , 0 , 0 );
1547
+ self.keyboardAnimationStartTime = fml::TimePoint ().Now ();
1548
+ self.originalViewInsetBottom = _viewportMetrics.physical_view_inset_bottom ;
1520
1549
1521
1550
// Invalidate old vsync client if old animation is not completed.
1522
1551
[self invalidateKeyboardAnimationVSyncClient ];
@@ -1527,6 +1556,11 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
1527
1556
animations: ^{
1528
1557
// Set end value.
1529
1558
[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];
1530
1564
}
1531
1565
completion: ^(BOOL finished) {
1532
1566
if (_keyboardAnimationVSyncClient == currentVsyncClient) {
@@ -1540,6 +1574,24 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
1540
1574
}];
1541
1575
}
1542
1576
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
+
1543
1595
- (void )setupKeyboardAnimationVsyncClient {
1544
1596
auto callback = [weakSelf =
1545
1597
[self getWeakPtr ]](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
@@ -1556,10 +1608,20 @@ - (void)setupKeyboardAnimationVsyncClient {
1556
1608
// Ensure the keyboardAnimationView is in view hierarchy when animation running.
1557
1609
[flutterViewController.get ().view addSubview: [flutterViewController keyboardAnimationView ]];
1558
1610
}
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 ()];
1563
1625
[flutterViewController updateViewportMetrics ];
1564
1626
}
1565
1627
};
@@ -1913,8 +1975,8 @@ - (BOOL)isAlwaysUse24HourFormat {
1913
1975
}
1914
1976
1915
1977
// 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.
1918
1980
- (NSString *)brightnessMode {
1919
1981
if (@available (iOS 13 , *)) {
1920
1982
UIUserInterfaceStyle style = self.traitCollection .userInterfaceStyle ;
0 commit comments