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:
parent
344a7c540b
commit
953d85e049
@ -62,6 +62,7 @@ public:
|
||||
bool isInputPanelVisible() const;
|
||||
|
||||
void focusWindowChanged(QWindow *focusWindow);
|
||||
void scrollRootView();
|
||||
|
||||
private:
|
||||
QIOSKeyboardListener *m_keyboardListener;
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user