blob: 1c8486f7852e5905dc6fecfb4a802ae04ee9d8d4 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/util/keyboard_observer_helper.h"
#include "base/check.h"
#include "base/check_op.h"
#import "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#include "ui/base/device_form_factor.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface KeyboardObserverHelper ()
// Flag that indicates if the keyboard is on screen.
@property(nonatomic, readwrite, getter=isKeyboardVisible) BOOL keyboardVisible;
// Current keyboard state.
@property(nonatomic, readwrite, getter=getKeyboardState)
KeyboardState keyboardState;
// The last known keyboard view. If this changes, it probably means that the
// application lost focus in multiwindow mode.
@property(nonatomic, weak) UIView* keyboardView;
// Mutable array storing weak pointers to consumers.
@property(nonatomic, strong) NSPointerArray* consumers;
@end
@implementation KeyboardObserverHelper
#pragma mark - Public class methods
+ (instancetype)sharedKeyboardObserver {
static KeyboardObserverHelper* sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[KeyboardObserverHelper alloc] init];
});
return sharedInstance;
}
#pragma mark - Public instance methods
- (void)addConsumer:(id<KeyboardObserverHelperConsumer>)consumer {
[self.consumers addPointer:(__bridge void*)consumer];
[self.consumers compact];
}
- (CGFloat)visibleKeyboardHeight {
if (self.keyboardState.isVisible && !self.keyboardState.isHardware &&
!self.keyboardState.isUndocked) {
// Software keyboard is visible and covers the full width of the screen
// (docked). Returns the keyboard + accessory height.
return CGRectGetHeight(self.keyboardView.frame);
} else if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_PHONE &&
self.keyboardState.isVisible && !self.keyboardState.isUndocked) {
// Keyboard is visible but hardware, only the accessory covers the full
// width of the display, the keyboard is hidden below the display. Returns
// the accessory's height.
return CurrentScreenHeight() - self.keyboardView.frame.origin.y;
} else {
return 0;
}
}
#pragma mark - Private instance methods
- (instancetype)init {
self = [super init];
if (self) {
[[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(keyboardWillDidChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(keyboardWillDidChangeFrame:)
name:UIKeyboardDidChangeFrameNotification
object:nil];
_consumers =
[[NSPointerArray alloc] initWithOptions:NSPointerFunctionsWeakMemory];
}
return self;
}
#pragma mark - Private class methods
+ (UIView*)keyboardView {
NSArray* windows = [UIApplication sharedApplication].windows;
NSUInteger expectedMinWindows =
(ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) ? 2 : 3;
if (windows.count < expectedMinWindows)
return nil;
UIWindow* window = windows.lastObject;
for (UIView* subview in window.subviews) {
if ([NSStringFromClass([subview class]) rangeOfString:@"PeripheralHost"]
.location != NSNotFound) {
return subview;
}
if ([NSStringFromClass([subview class]) rangeOfString:@"SetContainer"]
.location != NSNotFound) {
for (UIView* subsubview in subview.subviews) {
if ([NSStringFromClass([subsubview class]) rangeOfString:@"SetHost"]
.location != NSNotFound) {
return subsubview;
}
}
}
}
return nil;
}
+ (UIScreen*)keyboardScreen {
UIView* keyboardView = [self keyboardView];
return keyboardView.window.screen;
}
#pragma mark - Keyboard Notifications
- (void)keyboardWillShow:(NSNotification*)notification {
self.keyboardVisible = YES;
}
- (void)keyboardWillHide:(NSNotification*)notification {
self.keyboardVisible = NO;
}
- (void)keyboardWillDidChangeFrame:(NSNotification*)notification {
[self updateKeyboardState];
}
#pragma mark Keyboard State Detection
// Update keyboard state by looking at keyboard frame.
- (void)updateKeyboardState {
UIView* keyboardView = KeyboardObserverHelper.keyboardView;
CGFloat windowHeight = [UIScreen mainScreen].bounds.size.height;
CGRect keyboardFrame = keyboardView.frame;
BOOL isVisible = CGRectGetMinY(keyboardFrame) < windowHeight;
BOOL isUndocked = CGRectGetMaxY(keyboardFrame) < windowHeight;
BOOL isHardware = isVisible && CGRectGetMaxY(keyboardFrame) > windowHeight;
// Only notify if a change is detected.
if (isVisible != self.keyboardState.isVisible ||
isUndocked != self.keyboardState.isUndocked ||
isHardware != self.keyboardState.isHardware ||
keyboardView != self.keyboardView) {
self.keyboardState = {isVisible, isUndocked, isHardware};
self.keyboardView = keyboardView;
// Notify on the next cycle.
dispatch_async(dispatch_get_main_queue(), ^{
for (id<KeyboardObserverHelperConsumer> consumer in self.consumers) {
[consumer keyboardWillChangeToState:self.keyboardState];
}
});
}
}
@end