19@property(nonatomic, weak) id<FlutterKeyboardInsetManagerDelegate> delegate;
21@property(nonatomic, assign) CGFloat originalViewInsetBottom;
22@property(nonatomic, strong) FlutterVSyncClient* keyboardAnimationVSyncClient;
23@property(nonatomic, assign)
BOOL keyboardAnimationIsShowing;
24@property(nonatomic, assign) NSTimeInterval keyboardAnimationStartTime;
25@property(nonatomic, strong) UIView* keyboardAnimationView;
32- (instancetype)initWithDelegate:(
id<FlutterKeyboardInsetManagerDelegate>)delegate {
36 _targetViewInsetBottom = 0.0;
41- (void)handleKeyboardNotification:(NSNotification*)notification {
44 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
45 if (!delegate || [
self shouldIgnoreKeyboardNotification:notification]) {
49 NSDictionary* info = notification.userInfo;
50 CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
51 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
52 FlutterKeyboardMode keyboardMode = [
self calculateKeyboardAttachMode:notification];
53 CGFloat calculatedInset = [
self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];
54 NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
61 if (keyboardMode == FlutterKeyboardModeHidden && calculatedInset == 0.0 && duration == 0.0) {
62 [
self hideKeyboardImmediately];
67 if (
self.targetViewInsetBottom == calculatedInset) {
77 BOOL keyboardWillShow = beginKeyboardFrame.origin.y > keyboardFrame.origin.y;
78 BOOL keyboardAnimationIsCompounding =
79 self.keyboardAnimationIsShowing == keyboardWillShow && _keyboardAnimationVSyncClient != nil;
82 self.keyboardAnimationIsShowing = keyboardWillShow;
84 if (!keyboardAnimationIsCompounding) {
85 [
self startKeyBoardAnimation:duration];
86 }
else if (
self.keyboardSpringAnimation) {
87 self.keyboardSpringAnimation.toValue =
self.targetViewInsetBottom;
91- (
BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification {
96 if (notification.name == UIKeyboardWillHideNotification) {
105 NSDictionary* info = notification.userInfo;
106 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
107 if (notification.name == UIKeyboardWillChangeFrameNotification &&
108 CGRectEqualToRect(keyboardFrame, CGRectZero)) {
114 if (CGRectIsEmpty(keyboardFrame)) {
119 if ([
self isKeyboardNotificationForDifferentView:notification]) {
125- (
BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification {
126 NSDictionary* info = notification.userInfo;
131 id isLocal = info[UIKeyboardIsLocalUserInfoKey];
132 if (isLocal && ![isLocal boolValue]) {
135 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
136 return (
id)delegate.engine.viewController != (
id)delegate;
139- (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification {
147 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
149 return FlutterKeyboardModeHidden;
151 NSDictionary* info = notification.userInfo;
152 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
154 if (notification.name == UIKeyboardWillHideNotification) {
155 return FlutterKeyboardModeHidden;
160 if (CGRectEqualToRect(keyboardFrame, CGRectZero)) {
161 return FlutterKeyboardModeFloating;
164 if (CGRectIsEmpty(keyboardFrame)) {
165 return FlutterKeyboardModeHidden;
168 CGRect screenRect = delegate.flutterScreenIfViewLoaded.bounds;
169 CGRect adjustedKeyboardFrame = keyboardFrame;
170 adjustedKeyboardFrame.origin.y += [
self calculateMultitaskingAdjustment:screenRect
171 keyboardFrame:keyboardFrame];
176 CGRect intersection = CGRectIntersection(adjustedKeyboardFrame, screenRect);
177 CGFloat intersectionHeight = CGRectGetHeight(intersection);
178 CGFloat intersectionWidth = CGRectGetWidth(intersection);
179 if (round(intersectionHeight) > 0 && intersectionWidth > 0) {
180 CGFloat screenHeight = CGRectGetHeight(screenRect);
181 CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame);
182 if (round(adjustedKeyboardBottom) < screenHeight) {
183 return FlutterKeyboardModeFloating;
185 return FlutterKeyboardModeDocked;
187 return FlutterKeyboardModeHidden;
197- (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame {
198 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
199 if (!delegate.isViewLoaded) {
206 UIView*
view = delegate.view;
207 if ([delegate isPadInSlideOverOrStageManagerMode]) {
208 CGFloat screenHeight = CGRectGetHeight(screenRect);
209 CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame);
213 if (screenHeight == keyboardBottom) {
216 CGRect viewRectRelativeToScreen = [delegate convertViewRectToScreen:view.bounds];
217 CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen);
218 CGFloat offset = screenHeight - viewBottom;
226- (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame
227 keyboardMode:(FlutterKeyboardMode)keyboardMode {
228 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
231 if (keyboardMode == FlutterKeyboardModeDocked) {
232 if (!delegate.isViewLoaded) {
235 UIView*
view = delegate.view;
236 CGRect viewRectRelativeToScreen = [delegate convertViewRectToScreen:view.bounds];
237 CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen);
238 CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection);
243 CGFloat scale = delegate.flutterScreenIfViewLoaded.scale;
244 return portionOfKeyboardInView * scale;
249- (void)startKeyBoardAnimation:(NSTimeInterval)duration {
251 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
252 if (!delegate.isViewLoaded) {
255 UIView*
view = delegate.view;
259 if (!
self.keyboardAnimationView) {
260 UIView* keyboardAnimationView = [[UIView alloc] init];
261 keyboardAnimationView.hidden = YES;
262 self.keyboardAnimationView = keyboardAnimationView;
265 if (
self.keyboardAnimationView.superview != view) {
266 [view addSubview:self.keyboardAnimationView];
270 [
self.keyboardAnimationView.layer removeAllAnimations];
273 CGFloat currentInset = delegate.physicalViewInsetBottom;
274 self.keyboardAnimationView.frame = CGRectMake(0, currentInset, 0, 0);
275 self.keyboardAnimationStartTime = CACurrentMediaTime();
276 self.originalViewInsetBottom = currentInset;
279 [
self invalidateKeyboardAnimationVSyncClient];
282 [
self setUpKeyboardAnimationVsyncClient:^(NSTimeInterval targetTime) {
283 [weakSelf handleKeyboardAnimationCallbackWithTargetTime:targetTime];
285 FlutterVSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;
287 [UIView animateWithDuration:duration
295 strongSelf.keyboardAnimationView.frame =
296 CGRectMake(0, strongSelf.targetViewInsetBottom, 0, 0);
299 [strongSelf.keyboardAnimationView layoutIfNeeded];
300 CAAnimation* keyboardAnimation =
301 [strongSelf.keyboardAnimationView.layer animationForKey:@"position"];
302 [strongSelf setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation];
304 completion:^(BOOL finished) {
306 if (strongSelf && strongSelf.keyboardAnimationVSyncClient == currentVsyncClient) {
310 [strongSelf invalidateKeyboardAnimationVSyncClient];
311 [strongSelf removeKeyboardAnimationView];
312 [strongSelf ensureViewportMetricsIsCorrect];
318 [
self invalidateKeyboardAnimationVSyncClient];
319 if (
self.keyboardAnimationView) {
320 [
self.keyboardAnimationView.layer removeAllAnimations];
321 [
self removeKeyboardAnimationView];
322 self.keyboardAnimationView = nil;
324 if (
self.keyboardSpringAnimation) {
325 self.keyboardSpringAnimation = nil;
328 [
self ensureViewportMetricsIsCorrect];
331- (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
333 if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) {
334 _keyboardSpringAnimation = nil;
339 CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation;
340 _keyboardSpringAnimation =
341 [[
SpringAnimation alloc] initWithStiffness:keyboardCASpringAnimation.stiffness
342 damping:keyboardCASpringAnimation.damping
343 mass:keyboardCASpringAnimation.mass
344 initialVelocity:keyboardCASpringAnimation.initialVelocity
345 fromValue:self.originalViewInsetBottom
346 toValue:self.targetViewInsetBottom];
349- (void)handleKeyboardAnimationCallbackWithTargetTime:(NSTimeInterval)targetTime {
350 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
353 if (!delegate.isViewLoaded) {
358 if (!
self.keyboardAnimationView) {
363 if (!
self.keyboardAnimationVSyncClient) {
367 if (
self.keyboardAnimationView.superview != delegate.view) {
369 [delegate.view addSubview:self.keyboardAnimationView];
372 CGFloat currentInset = 0;
373 if (!
self.keyboardSpringAnimation) {
374 if (
self.keyboardAnimationView.layer.presentationLayer) {
375 currentInset =
self.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
378 NSTimeInterval timeElapsed = targetTime -
self.keyboardAnimationStartTime;
379 currentInset = [
self.keyboardSpringAnimation curveFunction:timeElapsed];
382 [delegate updateViewportMetricsWithInset:currentInset];
385- (void)setUpKeyboardAnimationVsyncClient:
387 if (!keyboardAnimationCallback) {
390 NSAssert(_keyboardAnimationVSyncClient == nil,
391 @"_keyboardAnimationVSyncClient must be nil when setting up.");
396 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
397 auto vsyncCallback = ^(CFTimeInterval startTime, CFTimeInterval targetTime) {
398 CFTimeInterval frameInterval = targetTime - startTime;
399 CFTimeInterval projectedTargetTime = targetTime + frameInterval;
400 dispatch_async(dispatch_get_main_queue(), ^(
void) {
401 animationCallback(projectedTargetTime);
404 _keyboardAnimationVSyncClient = [[FlutterVSyncClient alloc]
405 initWithTaskRunner:delegate.engine.uiTaskRunner
406 isVariableRefreshRateEnabled:FlutterDisplayLinkManager.maxRefreshRateEnabledOnIPhone
407 maxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate
408 callback:vsyncCallback];
409 _keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
410 [_keyboardAnimationVSyncClient await];
413- (void)invalidateKeyboardAnimationVSyncClient {
414 [_keyboardAnimationVSyncClient invalidate];
415 _keyboardAnimationVSyncClient = nil;
418- (void)removeKeyboardAnimationView {
419 if (
self.keyboardAnimationView.superview != nil) {
420 [
self.keyboardAnimationView removeFromSuperview];
424- (void)ensureViewportMetricsIsCorrect {
425 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
426 [delegate updateViewportMetricsWithInset:self.targetViewInsetBottom];
430 [
self invalidateKeyboardAnimationVSyncClient];
431 [
self removeKeyboardAnimationView];
Coordinates the animation of the bottom viewport inset in response to system keyboard visibility chan...
void invalidate()
Terminates any active animations and releases internal resources.
CGFloat targetViewInsetBottom
The physical pixel value of the bottom inset once the current animation reaches its final state.
void hideKeyboardImmediately()
Immediately stops any active keyboard animations and synchronizes the engine's viewport metrics with ...
void(^ FlutterKeyboardAnimationCallback)(NSTimeInterval)