IOS7: Resize main frame depending on keyboard size and orientation

It's important that the main frame, displaying the OpenGL rendered
graphics, has the proper dimensions and depending on the device
orientation. It's also important that the frame is not covered by
the iOS keyboard.

This commit calculates the frame size depening on the orientation
and the keyboard status. The keyboard knows its parent view and
can resize it when the keyboard becomes visible or hidden.

There are multiple scenarios where the frame size is changed.
 - When the keyboard is hidden/shown which can be automatically
   change depending on the device orientation
 - If the system demands the keyboard to get visible or hidden
 - When rotating the device
 - When suspending/resuming the application

There can also be combination of the scenarios above, e.g. if
suspending the application in landscape mode and resume it in
portrait mode.

A lot of effort has been put into testing different scenarios to
verify that the screen size becomes correct. However there might
be some scenario which has not been covered.
This commit is contained in:
Lars Sundström 2023-06-27 22:45:45 +02:00
parent 5d5564fceb
commit d8bc349b37
3 changed files with 99 additions and 9 deletions

View file

@ -393,16 +393,72 @@
@end
@implementation SoftKeyboard
@implementation SoftKeyboard {
BOOL _keyboardVisible;
}
#if TARGET_OS_IOS
- (void)resizeParentFrame:(NSNotification*)notification keyboardDidShow:(BOOL)didShow
{
NSDictionary* userInfo = [notification userInfo];
CGRect keyboardFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
keyboardFrame = [self.superview convertRect:keyboardFrame fromView:nil];
// Base the new frame size on the current parent frame size
CGRect newFrame = self.superview.frame;
newFrame.size.height += (keyboardFrame.size.height) * (didShow ? -1 : 1);
// Resize with a fancy animation
NSNumber *rate = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey];
[UIView animateWithDuration:rate.floatValue animations:^{
self.superview.frame = newFrame;
}];
}
- (void)keyboardDidShow:(NSNotification*)notification
{
// NotificationCenter might notify multiple times
// when keyboard did show because the accessoryView
// affect the keyboard height. However since we use
// UIKeyboardFrameEndUserInfoKey to get the keyboard
// it will always have the same value. Make sure to
// only handle one notification.
if (!_keyboardVisible) {
[self resizeParentFrame:notification keyboardDidShow:YES];
_keyboardVisible = YES;
}
}
- (void)keyboardDidHide:(NSNotification*)notification
{
// NotificationCenter will only call this once
// when keyboard did hide.
[self resizeParentFrame:notification keyboardDidShow:NO];
_keyboardVisible = NO;
}
#endif
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
#if TARGET_OS_IOS
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardDidHide:)
name:UIKeyboardDidHideNotification
object:nil];
#endif
inputDelegate = nil;
inputView = [[TextInputHandler alloc] initWithKeyboard:self];
inputView.delegate = self;
inputView.clearsOnBeginEditing = YES;
[inputView layoutIfNeeded];
_keyboardVisible = NO;
return self;
}

View file

@ -48,7 +48,9 @@ uint getSizeNextPOT(uint size);
NSLock *_eventLock;
SoftKeyboard *_keyboardView;
Common::List<GameController*> _controllers;
#if TARGET_OS_IOS
UIInterfaceOrientation _currentOrientation;
#endif
UIBackgroundTaskIdentifier _backgroundSaveStateTask;
EAGLContext *_mainContext;

View file

@ -290,6 +290,9 @@ bool iOS7_fetchEvent(InternalEvent *event) {
self = [super initWithFrame: frame];
_backgroundSaveStateTask = UIBackgroundTaskInvalid;
#if TARGET_OS_IOS
_currentOrientation = UIInterfaceOrientationUnknown;
#endif
[self setupGestureRecognizers];
@ -325,7 +328,12 @@ bool iOS7_fetchEvent(InternalEvent *event) {
[_keyboardView setInputDelegate:self];
[self addSubview:[_keyboardView inputView]];
[self addSubview: _keyboardView];
[self showKeyboard];
if ([self getScreenHeight] > [self getScreenWidth]) {
// This will make sure the keyboard is shown in portrait
// mode on start of the application since the orientation
// handling is performed before the view finish its setup
[self showKeyboard];
}
}
[self adjustViewFrameForSafeArea];
@ -344,7 +352,7 @@ bool iOS7_fetchEvent(InternalEvent *event) {
#ifdef __IPHONE_11_0
if ( @available(iOS 11, tvOS 11, *) ) {
CGRect screenSize = [[UIScreen mainScreen] bounds];
CGRect newFrame = screenSize;
CGRect newFrame = self.frame;
#if TARGET_OS_IOS
UIEdgeInsets inset = [[[UIApplication sharedApplication] keyWindow] safeAreaInsets];
UIInterfaceOrientation orientation = UIInterfaceOrientationUnknown;
@ -353,14 +361,32 @@ bool iOS7_fetchEvent(InternalEvent *event) {
} else {
orientation = [[UIApplication sharedApplication] statusBarOrientation];
}
// The code below adjust the screen size according to what Apple calls
// the "safe area". It also cover the cases when the software keyboard
// is visible and has changed the frame height so the keyboard doesn't
// cover any part of the game screen.
if (orientation != _currentOrientation) {
// If the orientation is changed the keyboard will hide or show
// depending on the current orientation. The frame size must be
// "reset" again to "full" screen size dimension. The keyboard
// will then calculate the approriate height when becoming visible.
newFrame = screenSize;
_currentOrientation = orientation;
}
// Make sure the frame height (either full screen or resized due to
// visible keyboard) is within the safe area.
CGFloat safeAreaHeight = screenSize.size.height - inset.top;
CGFloat height = newFrame.size.height < safeAreaHeight ? newFrame.size.height : safeAreaHeight;
if ( orientation == UIInterfaceOrientationPortrait ) {
newFrame = CGRectMake(screenSize.origin.x, screenSize.origin.y + inset.top, screenSize.size.width, screenSize.size.height - inset.top);
newFrame = CGRectMake(screenSize.origin.x, screenSize.origin.y + inset.top, screenSize.size.width, height);
} else if ( orientation == UIInterfaceOrientationPortraitUpsideDown ) {
newFrame = CGRectMake(screenSize.origin.x, screenSize.origin.y, screenSize.size.width, screenSize.size.height - inset.top);
newFrame = CGRectMake(screenSize.origin.x, screenSize.origin.y, screenSize.size.width, height);
} else if ( orientation == UIInterfaceOrientationLandscapeLeft ) {
newFrame = CGRectMake(screenSize.origin.x, screenSize.origin.y, screenSize.size.width - inset.right, screenSize.size.height);
newFrame = CGRectMake(screenSize.origin.x, screenSize.origin.y, screenSize.size.width - inset.right, height);
} else if ( orientation == UIInterfaceOrientationLandscapeRight ) {
newFrame = CGRectMake(screenSize.origin.x + inset.left, screenSize.origin.y, screenSize.size.width - inset.left, screenSize.size.height);
newFrame = CGRectMake(screenSize.origin.x + inset.left, screenSize.origin.y, screenSize.size.width - inset.left, height);
}
#endif
self.frame = newFrame;
@ -371,6 +397,8 @@ bool iOS7_fetchEvent(InternalEvent *event) {
#ifdef __IPHONE_11_0
// This delegate method is called when the safe area of the view changes
-(void)safeAreaInsetsDidChange {
[super safeAreaInsetsDidChange];
[self adjustViewFrameForSafeArea];
}
#endif
@ -606,6 +634,10 @@ bool iOS7_fetchEvent(InternalEvent *event) {
}
- (void)applicationSuspend {
// Make sure to hide the keyboard when suspended. Else the frame
// sizing might become incorrect because the NotificationCenter
// sends keyboard notifications on resume.
[self hideKeyboard];
[self addEvent:InternalEvent(kInputApplicationSuspended, 0, 0)];
}