iOS: scroll screen when keyboard opens

This change will let QIOSInputContext scroll the root
view when the virtual keyboard is open, so that the input cursor
is not obscured.

Change-Id: If0758f4bf04c2b8e554e0196451154def7e3cb86
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@digia.com>
This commit is contained in:
Richard Moe Gustavsen 2013-11-14 09:58:25 +01:00 committed by The Qt Project
parent 344a7c540b
commit 953d85e049
3 changed files with 131 additions and 14 deletions

View File

@ -62,6 +62,7 @@ public:
bool isInputPanelVisible() const;
void focusWindowChanged(QWindow *focusWindow);
void scrollRootView();
private:
QIOSKeyboardListener *m_keyboardListener;

View File

@ -48,7 +48,12 @@
@public
QIOSInputContext *m_context;
BOOL m_keyboardVisible;
BOOL m_keyboardVisibleAndDocked;
QRectF m_keyboardRect;
QRectF m_keyboardEndRect;
NSTimeInterval m_duration;
UIViewAnimationCurve m_curve;
UIViewController *m_viewController;
}
@end
@ -60,8 +65,30 @@
if (self) {
m_context = context;
m_keyboardVisible = NO;
// After the keyboard became undockable (iOS5), UIKeyboardWillShow/UIKeyboardWillHide
// no longer works for all cases. So listen to keyboard frame changes instead:
m_keyboardVisibleAndDocked = NO;
m_duration = 0;
m_curve = UIViewAnimationCurveEaseOut;
m_viewController = 0;
if (isQtApplication()) {
// Get the root view controller that is on the same screen as the keyboard:
for (UIWindow *uiWindow in [[UIApplication sharedApplication] windows]) {
if (uiWindow.screen == [UIScreen mainScreen]) {
m_viewController = [uiWindow.rootViewController retain];
break;
}
}
Q_ASSERT(m_viewController);
}
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillShow:)
name:@"UIKeyboardWillShowNotification" object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillHide:)
name:@"UIKeyboardWillHideNotification" object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardDidChangeFrame:)
@ -72,25 +99,68 @@
- (void) dealloc
{
[m_viewController release];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:@"UIKeyboardWillShowNotification" object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:@"UIKeyboardWillHideNotification" object:nil];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:@"UIKeyboardDidChangeFrameNotification" object:nil];
[super dealloc];
}
- (QRectF) getKeyboardRect:(NSNotification *)notification
{
// For Qt applications we rotate the keyboard rect to align with the screen
// orientation (which is the interface orientation of the root view controller).
// For hybrid apps we follow native behavior, and return the rect unmodified:
CGRect keyboardFrame = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];
if (isQtApplication()) {
UIView *view = m_viewController.view;
return fromCGRect(CGRectOffset([view convertRect:keyboardFrame fromView:view.window], 0, -view.bounds.origin.y));
} else {
return fromCGRect(keyboardFrame);
}
}
- (void) keyboardDidChangeFrame:(NSNotification *)notification
{
CGRect frame;
[[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&frame];
m_keyboardRect = fromPortraitToPrimary(fromCGRect(frame), QGuiApplication::primaryScreen()->handle());
m_keyboardRect = [self getKeyboardRect:notification];
m_context->emitKeyboardRectChanged();
BOOL visible = CGRectIntersectsRect(frame, [UIScreen mainScreen].bounds);
BOOL visible = m_keyboardRect.intersects(fromCGRect([UIScreen mainScreen].bounds));
if (m_keyboardVisible != visible) {
m_keyboardVisible = visible;
m_context->emitInputPanelVisibleChanged();
}
// If the keyboard was visible and docked from before, this is just a geometry
// change (normally caused by an orientation change). In that case, update scroll:
if (m_keyboardVisibleAndDocked)
m_context->scrollRootView();
}
- (void) keyboardWillShow:(NSNotification *)notification
{
// Note that UIKeyboardWillShowNotification is only sendt when the keyboard is docked.
m_keyboardVisibleAndDocked = YES;
m_keyboardEndRect = [self getKeyboardRect:notification];
if (!m_duration) {
m_duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
m_curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16;
}
m_context->scrollRootView();
}
- (void) keyboardWillHide:(NSNotification *)notification
{
// Note that UIKeyboardWillHideNotification is also sendt when the keyboard is undocked.
m_keyboardVisibleAndDocked = NO;
m_keyboardEndRect = [self getKeyboardRect:notification];
m_context->scrollRootView();
}
@end
@ -101,6 +171,8 @@ QIOSInputContext::QIOSInputContext()
, m_focusView(0)
, m_hasPendingHideRequest(false)
{
if (isQtApplication())
connect(qGuiApp->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::scrollRootView);
connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged);
}
@ -151,3 +223,40 @@ void QIOSInputContext::focusWindowChanged(QWindow *focusWindow)
[m_focusView release];
m_focusView = [view retain];
}
void QIOSInputContext::scrollRootView()
{
// Scroll the root view (screen) if:
// - our backend controls the root view controller on the main screen (no hybrid app)
// - the focus object is on the same screen as the keyboard.
// - the first responder is a QUIView, and not some other foreign UIView.
// - the keyboard is docked. Otherwise the user can move the keyboard instead.
if (!isQtApplication() || !m_focusView)
return;
UIView *view = m_keyboardListener->m_viewController.view;
qreal scrollTo = 0;
if (m_focusView.isFirstResponder
&& m_keyboardListener->m_keyboardVisibleAndDocked
&& m_focusView.window == view.window) {
QRectF cursorRect = qGuiApp->inputMethod()->cursorRectangle();
cursorRect.translate(qGuiApp->focusWindow()->geometry().topLeft());
qreal keyboardY = m_keyboardListener->m_keyboardEndRect.y();
int statusBarY = qGuiApp->primaryScreen()->availableGeometry().y();
const int margin = 20;
if (cursorRect.bottomLeft().y() > keyboardY - margin)
scrollTo = qMin(view.bounds.size.height - keyboardY, cursorRect.y() - statusBarY - margin);
}
if (scrollTo != view.bounds.origin.y) {
// Scroll the view the same way a UIScrollView works: by changing bounds.origin:
CGRect newBounds = view.bounds;
newBounds.origin.y = scrollTo;
[UIView animateWithDuration:m_keyboardListener->m_duration delay:0
options:m_keyboardListener->m_curve
animations:^{ view.bounds = newBounds; }
completion:0];
}
}

View File

@ -181,11 +181,14 @@
QRect actualGeometry;
if (m_qioswindow->window()->isTopLevel()) {
UIWindow *uiWindow = self.window;
UIView *rootView = uiWindow.rootViewController.view;
CGRect rootViewPositionInRelationToRootViewController =
[uiWindow.rootViewController.view convertRect:uiWindow.bounds fromView:uiWindow];
[rootView convertRect:uiWindow.bounds fromView:uiWindow];
actualGeometry = fromCGRect(CGRectOffset([self.superview convertRect:self.frame toView:uiWindow.rootViewController.view],
-rootViewPositionInRelationToRootViewController.origin.x, -rootViewPositionInRelationToRootViewController.origin.y));
actualGeometry = fromCGRect(CGRectOffset([self.superview convertRect:self.frame toView:rootView],
-rootViewPositionInRelationToRootViewController.origin.x,
-rootViewPositionInRelationToRootViewController.origin.y
+ rootView.bounds.origin.y));
} else {
actualGeometry = fromCGRect(self.frame);
}
@ -515,13 +518,17 @@ void QIOSWindow::applyGeometry(const QRect &rect)
if (window()->isTopLevel()) {
// The QWindow is in QScreen coordinates, which maps to a possibly rotated root-view-controller.
// Since the root-view-controller might be translated in relation to the UIWindow, we need to
// check specifically for that and compensate.
// check specifically for that and compensate. Also check if the root view has been scrolled
// as a result of the keyboard being open.
UIWindow *uiWindow = m_view.window;
UIView *rootView = uiWindow.rootViewController.view;
CGRect rootViewPositionInRelationToRootViewController =
[uiWindow.rootViewController.view convertRect:uiWindow.bounds fromView:uiWindow];
[rootView convertRect:uiWindow.bounds fromView:uiWindow];
m_view.frame = CGRectOffset([m_view.superview convertRect:toCGRect(rect) fromView:m_view.window.rootViewController.view],
rootViewPositionInRelationToRootViewController.origin.x, rootViewPositionInRelationToRootViewController.origin.y);
m_view.frame = CGRectOffset([m_view.superview convertRect:toCGRect(rect) fromView:rootView],
rootViewPositionInRelationToRootViewController.origin.x,
rootViewPositionInRelationToRootViewController.origin.y
+ rootView.bounds.origin.y);
} else {
// Easy, in parent's coordinates
m_view.frame = toCGRect(rect);