@@ -69,6 +69,12 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
69
69
@property (nonatomic , assign ) BOOL isHomeIndicatorHidden;
70
70
@property (nonatomic , assign ) BOOL isPresentingViewControllerAnimating;
71
71
72
+ /* *
73
+ * Keyboard animation properties
74
+ */
75
+ @property (nonatomic , assign ) double targetViewInsetBottom;
76
+ @property (nonatomic , strong ) CADisplayLink * displayLink;
77
+
72
78
/* *
73
79
* Creates and registers plugins used by this view controller.
74
80
*/
@@ -113,6 +119,7 @@ @implementation FlutterViewController {
113
119
fml::scoped_nsobject<UIScrollView> _scrollView;
114
120
fml::scoped_nsobject<UIPointerInteraction> _pointerInteraction API_AVAILABLE (ios (13.4 ));
115
121
fml::scoped_nsobject<UIPanGestureRecognizer> _panGestureRecognizer API_AVAILABLE (ios (13.4 ));
122
+ fml::scoped_nsobject<UIView> _keyboardAnimationView;
116
123
MouseState _mouseState;
117
124
}
118
125
@@ -542,6 +549,10 @@ - (UIView*)splashScreenView {
542
549
return _splashScreenView.get ();
543
550
}
544
551
552
+ - (UIView*)keyboardAnimationView {
553
+ return _keyboardAnimationView.get ();
554
+ }
555
+
545
556
- (BOOL )loadDefaultSplashScreenView {
546
557
NSString * launchscreenName =
547
558
[[[NSBundle mainBundle ] infoDictionary ] objectForKey: @" UILaunchStoryboardName" ];
@@ -719,6 +730,8 @@ - (void)viewWillDisappear:(BOOL)animated {
719
730
- (void )viewDidDisappear : (BOOL )animated {
720
731
TRACE_EVENT0 (" flutter" , " viewDidDisappear" );
721
732
if ([_engine.get () viewController ] == self) {
733
+ [self invalidateDisplayLink ];
734
+ [self ensureViewportMetricsIsCorrect ];
722
735
[self surfaceUpdated: NO ];
723
736
[[_engine.get () lifecycleChannel ] sendMessage: @" AppLifecycleState.paused" ];
724
737
[self flushOngoingTouches ];
@@ -1104,29 +1117,115 @@ - (void)keyboardWillChangeFrame:(NSNotification*)notification {
1104
1117
}
1105
1118
}
1106
1119
1120
+ // Ignore keyboard notifications if engine’s viewController is not current viewController.
1121
+ if ([_engine.get () viewController ] != self) {
1122
+ return ;
1123
+ }
1124
+
1107
1125
CGRect keyboardFrame = [[info objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue ];
1108
1126
CGRect screenRect = [[UIScreen mainScreen ] bounds ];
1109
1127
1128
+ // Get the animation duration
1129
+ NSTimeInterval duration =
1130
+ [[info objectForKey: UIKeyboardAnimationDurationUserInfoKey] doubleValue ];
1131
+
1110
1132
// Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present
1111
1133
// in the screen to see if the keyboard is visible.
1112
1134
if (CGRectIntersectsRect (keyboardFrame, screenRect)) {
1113
1135
CGFloat bottom = CGRectGetHeight (keyboardFrame);
1114
1136
CGFloat scale = [UIScreen mainScreen ].scale ;
1115
-
1116
1137
// The keyboard is treated as an inset since we want to effectively reduce the window size by
1117
1138
// the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1118
1139
// bottom padding.
1119
- _viewportMetrics. physical_view_inset_bottom = bottom * scale;
1140
+ self. targetViewInsetBottom = bottom * scale;
1120
1141
} else {
1121
- _viewportMetrics. physical_view_inset_bottom = 0 ;
1142
+ self. targetViewInsetBottom = 0 ;
1122
1143
}
1123
-
1124
- [self updateViewportMetrics ];
1144
+ [self startKeyBoardAnimation: duration];
1125
1145
}
1126
1146
1127
1147
- (void )keyboardWillBeHidden : (NSNotification *)notification {
1128
- _viewportMetrics.physical_view_inset_bottom = 0 ;
1129
- [self updateViewportMetrics ];
1148
+ // When keyboard hide, the keyboardWillChangeFrame function will be called to update viewport
1149
+ // metrics. So do not call [self updateViewportMetrics] here again.
1150
+ }
1151
+
1152
+ - (void )startKeyBoardAnimation : (NSTimeInterval )duration {
1153
+ // If current physical_view_inset_bottom == targetViewInsetBottom,do nothing.
1154
+ if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom ) {
1155
+ return ;
1156
+ }
1157
+
1158
+ // When call this method first time,
1159
+ // initialize the keyboardAnimationView to get animation interpolation during animation.
1160
+ if ([self keyboardAnimationView ] == nil ) {
1161
+ UIView* keyboardAnimationView = [[UIView alloc ] init ];
1162
+ [keyboardAnimationView setHidden: YES ];
1163
+ _keyboardAnimationView.reset (keyboardAnimationView);
1164
+ }
1165
+
1166
+ if ([self keyboardAnimationView ].superview == nil ) {
1167
+ [self .view addSubview: [self keyboardAnimationView ]];
1168
+ }
1169
+
1170
+ // Remove running animation when start another animation.
1171
+ // After calling this line,the old display link will invalidate.
1172
+ [[self keyboardAnimationView ].layer removeAllAnimations ];
1173
+
1174
+ // Set animation begin value.
1175
+ [self keyboardAnimationView ].frame =
1176
+ CGRectMake (0 , _viewportMetrics.physical_view_inset_bottom , 0 , 0 );
1177
+
1178
+ // Invalidate old display link if the old animation is not complete
1179
+ [self invalidateDisplayLink ];
1180
+
1181
+ self.displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector (onDisplayLink )];
1182
+ [self .displayLink addToRunLoop: NSRunLoop .currentRunLoop forMode: NSRunLoopCommonModes ];
1183
+ __block CADisplayLink * currentDisplayLink = self.displayLink ;
1184
+
1185
+ [UIView animateWithDuration: duration
1186
+ animations: ^{
1187
+ // Set end value.
1188
+ [self keyboardAnimationView ].frame = CGRectMake (0 , self.targetViewInsetBottom , 0 , 0 );
1189
+ }
1190
+ completion: ^(BOOL finished) {
1191
+ if (self.displayLink == currentDisplayLink) {
1192
+ [self invalidateDisplayLink ];
1193
+ }
1194
+ if (finished) {
1195
+ [self removeKeyboardAnimationView ];
1196
+ [self ensureViewportMetricsIsCorrect ];
1197
+ }
1198
+ }];
1199
+ }
1200
+
1201
+ - (void )invalidateDisplayLink {
1202
+ [self .displayLink invalidate ];
1203
+ }
1204
+
1205
+ - (void )removeKeyboardAnimationView {
1206
+ if ([self keyboardAnimationView ].superview != nil ) {
1207
+ [[self keyboardAnimationView ] removeFromSuperview ];
1208
+ }
1209
+ }
1210
+
1211
+ - (void )ensureViewportMetricsIsCorrect {
1212
+ if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom ) {
1213
+ // Make sure the `physical_view_inset_bottom` is the target value.
1214
+ _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom ;
1215
+ [self updateViewportMetrics ];
1216
+ }
1217
+ }
1218
+
1219
+ - (void )onDisplayLink {
1220
+ if ([self keyboardAnimationView ].superview == nil ) {
1221
+ // Ensure the keyboardAnimationView is in view hierarchy when animation running.
1222
+ [self .view addSubview: [self keyboardAnimationView ]];
1223
+ }
1224
+ if ([self keyboardAnimationView ].layer .presentationLayer ) {
1225
+ CGFloat value = [self keyboardAnimationView ].layer .presentationLayer .frame .origin .y ;
1226
+ _viewportMetrics.physical_view_inset_bottom = value;
1227
+ [self updateViewportMetrics ];
1228
+ }
1130
1229
}
1131
1230
1132
1231
- (void )handlePressEvent : (FlutterUIPressProxy*)press
0 commit comments