@@ -65,6 +65,7 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
65
65
*/
66
66
@property (nonatomic , assign ) double targetViewInsetBottom;
67
67
@property (nonatomic , retain ) VSyncClient* keyboardAnimationVSyncClient;
68
+ @property (nonatomic , assign ) BOOL isKeyboardInOrTransitioningFromBackground;
68
69
69
70
// / VSyncClient for touch events delivery frame rate correction.
70
71
// /
@@ -315,6 +316,11 @@ - (void)setupNotificationCenterObservers {
315
316
name: UIKeyboardWillChangeFrameNotification
316
317
object: nil ];
317
318
319
+ [center addObserver: self
320
+ selector: @selector (keyboardWillShowNotification: )
321
+ name: UIKeyboardWillShowNotification
322
+ object: nil ];
323
+
318
324
[center addObserver: self
319
325
selector: @selector (keyboardWillBeHidden: )
320
326
name: UIKeyboardWillHideNotification
@@ -588,6 +594,16 @@ - (UIView*)keyboardAnimationView {
588
594
return _keyboardAnimationView.get ();
589
595
}
590
596
597
+ - (UIScreen*)mainScreenIfViewLoaded {
598
+ if (@available (iOS 13.0 , *)) {
599
+ if (self.viewIfLoaded == nil ) {
600
+ FML_LOG (WARNING) << " Trying to access the view before it is loaded." ;
601
+ }
602
+ return self.viewIfLoaded .window .windowScene .screen ;
603
+ }
604
+ return UIScreen.mainScreen ;
605
+ }
606
+
591
607
- (BOOL )loadDefaultSplashScreenView {
592
608
NSString * launchscreenName =
593
609
[[[NSBundle mainBundle ] infoDictionary ] objectForKey: @" UILaunchStoryboardName" ];
@@ -873,6 +889,7 @@ - (void)dealloc {
873
889
874
890
- (void )applicationBecameActive : (NSNotification *)notification {
875
891
TRACE_EVENT0 (" flutter" , " applicationBecameActive" );
892
+ self.isKeyboardInOrTransitioningFromBackground = NO ;
876
893
if (_viewportMetrics.physical_width ) {
877
894
[self surfaceUpdated: YES ];
878
895
}
@@ -891,6 +908,7 @@ - (void)applicationWillTerminate:(NSNotification*)notification {
891
908
892
909
- (void )applicationDidEnterBackground : (NSNotification *)notification {
893
910
TRACE_EVENT0 (" flutter" , " applicationDidEnterBackground" );
911
+ self.isKeyboardInOrTransitioningFromBackground = YES ;
894
912
[self surfaceUpdated: NO ];
895
913
[self goToApplicationLifecycle: @" AppLifecycleState.paused" ];
896
914
}
@@ -1272,65 +1290,207 @@ - (void)updateViewportPadding {
1272
1290
1273
1291
#pragma mark - Keyboard events
1274
1292
1293
+ - (void )keyboardWillShowNotification : (NSNotification *)notification {
1294
+ // Immediately prior to a docked keyboard being shown or when a keyboard goes from
1295
+ // undocked/floating to docked, this notification is triggered. This notification also happens
1296
+ // when Minimized/Expanded Shortcuts bar is dropped after dragging (the keyboard's end frame will
1297
+ // be CGRectZero).
1298
+ [self handleKeyboardNotification: notification];
1299
+ }
1300
+
1275
1301
- (void )keyboardWillChangeFrame : (NSNotification *)notification {
1276
- NSDictionary * info = [notification userInfo ];
1302
+ // Immediately prior to a change in keyboard frame, this notification is triggered.
1303
+ // Sometimes when the keyboard is being hidden or undocked, this notification's keyboard's end
1304
+ // frame is not yet entirely out of screen, which is why we also use
1305
+ // UIKeyboardWillHideNotification.
1306
+ [self handleKeyboardNotification: notification];
1307
+ }
1277
1308
1278
- // Ignore keyboard notifications related to other apps.
1279
- id isLocal = info[UIKeyboardIsLocalUserInfoKey];
1280
- if (isLocal && ![isLocal boolValue ]) {
1309
+ - (void )keyboardWillBeHidden : (NSNotification *)notification {
1310
+ // When keyboard is hidden or undocked, this notification will be triggered.
1311
+ // This notification might not occur when the keyboard is changed from docked to floating, which
1312
+ // is why we also use UIKeyboardWillChangeFrameNotification.
1313
+ [self handleKeyboardNotification: notification];
1314
+ }
1315
+
1316
+ - (void )handleKeyboardNotification : (NSNotification *)notification {
1317
+ // See https:://flutter.dev/go/ios-keyboard-calculating-inset for more details
1318
+ // on why notifications are used and how things are calculated.
1319
+ if ([self shouldIgnoreKeyboardNotification: notification]) {
1281
1320
return ;
1282
1321
}
1283
1322
1284
- // Ignore keyboard notifications if engine’s viewController is not current viewController.
1285
- if ([_engine.get () viewController ] != self) {
1323
+ NSDictionary * info = notification.userInfo ;
1324
+ CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue ];
1325
+ FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode: notification];
1326
+ CGFloat calculatedInset = [self calculateKeyboardInset: keyboardFrame keyboardMode: keyboardMode];
1327
+
1328
+ // Avoid double triggering startKeyBoardAnimation.
1329
+ if (self.targetViewInsetBottom == calculatedInset) {
1286
1330
return ;
1287
1331
}
1288
1332
1289
- CGRect keyboardFrame = [[info objectForKey: UIKeyboardFrameEndUserInfoKey] CGRectValue ];
1290
- CGRect screenRect = [[UIScreen mainScreen ] bounds ];
1333
+ self.targetViewInsetBottom = calculatedInset;
1334
+ NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue ];
1335
+ [self startKeyBoardAnimation: duration];
1336
+ }
1337
+
1338
+ - (BOOL )shouldIgnoreKeyboardNotification : (NSNotification *)notification {
1339
+ // Don't ignore UIKeyboardWillHideNotification notifications.
1340
+ // Even if the notification is triggered in the background or by a different app/view controller,
1341
+ // we want to always handle this notification to avoid inaccurate inset when in a mulitasking mode
1342
+ // or when switching between apps.
1343
+ if (notification.name == UIKeyboardWillHideNotification) {
1344
+ return NO ;
1345
+ }
1291
1346
1292
- // Get the animation duration
1293
- NSTimeInterval duration =
1294
- [[info objectForKey: UIKeyboardAnimationDurationUserInfoKey] doubleValue ];
1347
+ // Ignore notification when keyboard's dimensions and position are all zeroes for
1348
+ // UIKeyboardWillChangeFrameNotification. This happens when keyboard is dragged. Do not ignore if
1349
+ // the notification is UIKeyboardWillShowNotification, as CGRectZero for that notfication only
1350
+ // occurs when Minimized/Expanded Shortcuts Bar is dropped after dragging, which we later use to
1351
+ // categorize it as floating.
1352
+ NSDictionary * info = notification.userInfo ;
1353
+ CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue ];
1354
+ if (notification.name == UIKeyboardWillChangeFrameNotification &&
1355
+ CGRectEqualToRect (keyboardFrame, CGRectZero )) {
1356
+ return YES ;
1357
+ }
1295
1358
1296
- // Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present
1297
- // in the screen to see if the keyboard is visible.
1298
- if (CGRectIntersectsRect (keyboardFrame, screenRect)) {
1299
- CGFloat bottom = CGRectGetHeight (keyboardFrame);
1300
- CGFloat scale = [UIScreen mainScreen ].scale ;
1301
- // The keyboard is treated as an inset since we want to effectively reduce the window size by
1302
- // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1303
- // bottom padding.
1304
- self.targetViewInsetBottom = bottom * scale;
1359
+ // When keyboard's height or width is set to 0, don't ignore. This does not happen
1360
+ // often but can happen sometimes when switching between multitasking modes.
1361
+ if (CGRectIsEmpty (keyboardFrame)) {
1362
+ return NO ;
1363
+ }
1364
+
1365
+ // Ignore keyboard notifications related to other apps or view controllers.
1366
+ if ([self isKeyboardNotificationForDifferentView: notification]) {
1367
+ return YES ;
1368
+ }
1369
+
1370
+ if (@available (iOS 13.0 , *)) {
1371
+ // noop
1305
1372
} else {
1306
- self.targetViewInsetBottom = 0 ;
1373
+ // If OS version is less than 13, ignore notification if the app is in the background
1374
+ // or is transitioning from the background. In older versions, when switching between
1375
+ // apps with the keyboard open in the secondary app, notifications are sent when
1376
+ // the app is in the background/transitioning from background as if they belong
1377
+ // to the app and as if the keyboard is showing even though it is not.
1378
+ if (self.isKeyboardInOrTransitioningFromBackground ) {
1379
+ return YES ;
1380
+ }
1307
1381
}
1308
- [self startKeyBoardAnimation: duration];
1309
- }
1310
1382
1311
- - ( void ) keyboardWillBeHidden : ( NSNotification *) notification {
1312
- NSDictionary * info = [notification userInfo ];
1383
+ return NO ;
1384
+ }
1313
1385
1314
- // Ignore keyboard notifications related to other apps.
1386
+ - (BOOL )isKeyboardNotificationForDifferentView : (NSNotification *)notification {
1387
+ NSDictionary * info = notification.userInfo ;
1388
+ // Keyboard notifications related to other apps.
1389
+ // If the UIKeyboardIsLocalUserInfoKey key doesn't exist (this should not happen after iOS 8),
1390
+ // proceed as if it was local so that the notification is not ignored.
1315
1391
id isLocal = info[UIKeyboardIsLocalUserInfoKey];
1316
1392
if (isLocal && ![isLocal boolValue ]) {
1317
- return ;
1393
+ return YES ;
1318
1394
}
1319
-
1320
- // Ignore keyboard notifications if engine’s viewController is not current viewController.
1395
+ // Engine’s viewController is not current viewController.
1321
1396
if ([_engine.get () viewController ] != self) {
1322
- return ;
1397
+ return YES ;
1323
1398
}
1399
+ return NO ;
1400
+ }
1324
1401
1325
- if (self.targetViewInsetBottom != 0 ) {
1326
- // Ensure the keyboard will be dismissed. Just like the keyboardWillChangeFrame,
1327
- // keyboardWillBeHidden is also in an animation block in iOS sdk, so we don't need to set the
1328
- // animation curve. Related issue: https://github.com/flutter/flutter/issues/99951
1329
- self.targetViewInsetBottom = 0 ;
1330
- NSTimeInterval duration =
1331
- [[info objectForKey: UIKeyboardAnimationDurationUserInfoKey] doubleValue ];
1332
- [self startKeyBoardAnimation: duration];
1402
+ - (FlutterKeyboardMode)calculateKeyboardAttachMode : (NSNotification *)notification {
1403
+ // There are multiple types of keyboard: docked, undocked, split, split docked,
1404
+ // floating, expanded shortcuts bar, minimized shortcuts bar. This function will categorize
1405
+ // the keyboard as one of the following modes: docked, floating, or hidden.
1406
+ // Docked mode includes docked, split docked, expanded shortcuts bar (when opening via click),
1407
+ // and minimized shortcuts bar (when opened via click).
1408
+ // Floating includes undocked, split, floating, expanded shortcuts bar (when dragged and dropped),
1409
+ // and minimized shortcuts bar (when dragged and dropped).
1410
+ NSDictionary * info = notification.userInfo ;
1411
+ CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue ];
1412
+
1413
+ if (notification.name == UIKeyboardWillHideNotification) {
1414
+ return FlutterKeyboardModeHidden;
1415
+ }
1416
+
1417
+ // If keyboard's dimensions and position are all zeroes, that means it's a Minimized/Expanded
1418
+ // Shortcuts Bar that has been dropped after dragging, which we categorize as floating.
1419
+ if (CGRectEqualToRect (keyboardFrame, CGRectZero )) {
1420
+ return FlutterKeyboardModeFloating;
1421
+ }
1422
+ // If keyboard's width or height are 0, it's hidden.
1423
+ if (CGRectIsEmpty (keyboardFrame)) {
1424
+ return FlutterKeyboardModeHidden;
1425
+ }
1426
+
1427
+ CGRect screenRect = [self mainScreenIfViewLoaded ].bounds ;
1428
+ CGRect adjustedKeyboardFrame = keyboardFrame;
1429
+ adjustedKeyboardFrame.origin .y += [self calculateMultitaskingAdjustment: screenRect
1430
+ keyboardFrame: keyboardFrame];
1431
+
1432
+ // If the keyboard is partially or fully showing within the screen, it's either docked or
1433
+ // floating. Sometimes with custom keyboard extensions, the keyboard's position may be off by a
1434
+ // small decimal amount (which is why CGRectIntersectRect can't be used). Round to compare.
1435
+ CGRect intersection = CGRectIntersection (adjustedKeyboardFrame, screenRect);
1436
+ CGFloat intersectionHeight = CGRectGetHeight (intersection);
1437
+ CGFloat intersectionWidth = CGRectGetWidth (intersection);
1438
+ if (round (intersectionHeight) > 0 && intersectionWidth > 0 ) {
1439
+ // If the keyboard is above the bottom of the screen, it's floating.
1440
+ CGFloat screenHeight = CGRectGetHeight (screenRect);
1441
+ CGFloat adjustedKeyboardBottom = CGRectGetMaxY (adjustedKeyboardFrame);
1442
+ if (round (adjustedKeyboardBottom) < screenHeight) {
1443
+ return FlutterKeyboardModeFloating;
1444
+ }
1445
+ return FlutterKeyboardModeDocked;
1446
+ }
1447
+ return FlutterKeyboardModeHidden;
1448
+ }
1449
+
1450
+ - (CGFloat )calculateMultitaskingAdjustment : (CGRect )screenRect keyboardFrame : (CGRect )keyboardFrame {
1451
+ // In Slide Over mode, the keyboard's frame does not include the space
1452
+ // below the app, even though the keyboard may be at the bottom of the screen.
1453
+ // To handle, shift the Y origin by the amount of space below the app.
1454
+ if (self.viewIfLoaded .traitCollection .userInterfaceIdiom == UIUserInterfaceIdiomPad &&
1455
+ self.viewIfLoaded .traitCollection .horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
1456
+ self.viewIfLoaded .traitCollection .verticalSizeClass == UIUserInterfaceSizeClassRegular) {
1457
+ CGFloat screenHeight = CGRectGetHeight (screenRect);
1458
+ CGFloat keyboardBottom = CGRectGetMaxY (keyboardFrame);
1459
+
1460
+ // Stage Manager mode will also meet the above parameters, but it does not handle
1461
+ // the keyboard positioning the same way, so skip if keyboard is at bottom of page.
1462
+ if (screenHeight == keyboardBottom) {
1463
+ return 0 ;
1464
+ }
1465
+ CGRect viewRectRelativeToScreen =
1466
+ [self .viewIfLoaded convertRect: self .viewIfLoaded.frame
1467
+ toCoordinateSpace: [self mainScreenIfViewLoaded ].coordinateSpace];
1468
+ CGFloat viewBottom = CGRectGetMaxY (viewRectRelativeToScreen);
1469
+ CGFloat offset = screenHeight - viewBottom;
1470
+ if (offset > 0 ) {
1471
+ return offset;
1472
+ }
1473
+ }
1474
+ return 0 ;
1475
+ }
1476
+
1477
+ - (CGFloat )calculateKeyboardInset : (CGRect )keyboardFrame keyboardMode : (NSInteger )keyboardMode {
1478
+ // Only docked keyboards will have an inset.
1479
+ if (keyboardMode == FlutterKeyboardModeDocked) {
1480
+ // Calculate how much of the keyboard intersects with the view.
1481
+ CGRect viewRectRelativeToScreen =
1482
+ [self .viewIfLoaded convertRect: self .viewIfLoaded.frame
1483
+ toCoordinateSpace: [self mainScreenIfViewLoaded ].coordinateSpace];
1484
+ CGRect intersection = CGRectIntersection (keyboardFrame, viewRectRelativeToScreen);
1485
+ CGFloat portionOfKeyboardInView = CGRectGetHeight (intersection);
1486
+
1487
+ // The keyboard is treated as an inset since we want to effectively reduce the window size by
1488
+ // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1489
+ // bottom padding.
1490
+ CGFloat scale = [self mainScreenIfViewLoaded ].scale ;
1491
+ return portionOfKeyboardInView * scale;
1333
1492
}
1493
+ return 0 ;
1334
1494
}
1335
1495
1336
1496
- (void )startKeyBoardAnimation : (NSTimeInterval )duration {
0 commit comments