20@property(nonatomic, weak) id<FlutterKeyboardInsetManagerDelegate> delegate;
22@property(nonatomic, assign) CGFloat originalViewInsetBottom;
24@property(nonatomic, assign)
BOOL keyboardAnimationIsShowing;
25@property(nonatomic, assign) NSTimeInterval keyboardAnimationStartTime;
26@property(nonatomic, strong) UIView* keyboardAnimationView;
33- (instancetype)initWithDelegate:(
id<FlutterKeyboardInsetManagerDelegate>)delegate {
37 _targetViewInsetBottom = 0.0;
42- (void)handleKeyboardNotification:(NSNotification*)notification {
45 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
46 if (!delegate || [
self shouldIgnoreKeyboardNotification:notification]) {
50 NSDictionary* info = notification.userInfo;
51 CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
52 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
53 FlutterKeyboardMode keyboardMode = [
self calculateKeyboardAttachMode:notification];
54 CGFloat calculatedInset = [
self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];
55 NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
62 if (keyboardMode == FlutterKeyboardModeHidden && calculatedInset == 0.0 && duration == 0.0) {
63 [
self hideKeyboardImmediately];
68 if (
self.targetViewInsetBottom == calculatedInset) {
78 BOOL keyboardWillShow = beginKeyboardFrame.origin.y > keyboardFrame.origin.y;
79 BOOL keyboardAnimationIsCompounding =
80 self.keyboardAnimationIsShowing == keyboardWillShow && _keyboardAnimationVSyncClient != nil;
83 self.keyboardAnimationIsShowing = keyboardWillShow;
85 if (!keyboardAnimationIsCompounding) {
86 [
self startKeyBoardAnimation:duration];
87 }
else if (
self.keyboardSpringAnimation) {
88 self.keyboardSpringAnimation.toValue =
self.targetViewInsetBottom;
92- (
BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification {
97 if (notification.name == UIKeyboardWillHideNotification) {
106 NSDictionary* info = notification.userInfo;
107 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
108 if (notification.name == UIKeyboardWillChangeFrameNotification &&
109 CGRectEqualToRect(keyboardFrame, CGRectZero)) {
115 if (CGRectIsEmpty(keyboardFrame)) {
120 if ([
self isKeyboardNotificationForDifferentView:notification]) {
126- (
BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification {
127 NSDictionary* info = notification.userInfo;
132 id isLocal = info[UIKeyboardIsLocalUserInfoKey];
133 if (isLocal && ![isLocal boolValue]) {
136 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
137 return (
id)delegate.engine.viewController != (
id)delegate;
140- (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification {
148 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
150 return FlutterKeyboardModeHidden;
152 NSDictionary* info = notification.userInfo;
153 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
155 if (notification.name == UIKeyboardWillHideNotification) {
156 return FlutterKeyboardModeHidden;
161 if (CGRectEqualToRect(keyboardFrame, CGRectZero)) {
162 return FlutterKeyboardModeFloating;
165 if (CGRectIsEmpty(keyboardFrame)) {
166 return FlutterKeyboardModeHidden;
169 CGRect screenRect = delegate.flutterScreenIfViewLoaded.bounds;
170 CGRect adjustedKeyboardFrame = keyboardFrame;
171 adjustedKeyboardFrame.origin.y += [
self calculateMultitaskingAdjustment:screenRect
172 keyboardFrame:keyboardFrame];
177 CGRect intersection = CGRectIntersection(adjustedKeyboardFrame, screenRect);
178 CGFloat intersectionHeight = CGRectGetHeight(intersection);
179 CGFloat intersectionWidth = CGRectGetWidth(intersection);
180 if (round(intersectionHeight) > 0 && intersectionWidth > 0) {
181 CGFloat screenHeight = CGRectGetHeight(screenRect);
182 CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame);
183 if (round(adjustedKeyboardBottom) < screenHeight) {
184 return FlutterKeyboardModeFloating;
186 return FlutterKeyboardModeDocked;
188 return FlutterKeyboardModeHidden;
198- (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame {
199 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
200 if (!delegate.isViewLoaded) {
207 UIView*
view = delegate.view;
208 if ([delegate isPadInSlideOverOrStageManagerMode]) {
209 CGFloat screenHeight = CGRectGetHeight(screenRect);
210 CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame);
214 if (screenHeight == keyboardBottom) {
217 CGRect viewRectRelativeToScreen = [delegate convertViewRectToScreen:view.bounds];
218 CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen);
219 CGFloat offset = screenHeight - viewBottom;
227- (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame
228 keyboardMode:(FlutterKeyboardMode)keyboardMode {
229 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
232 if (keyboardMode == FlutterKeyboardModeDocked) {
233 if (!delegate.isViewLoaded) {
236 UIView*
view = delegate.view;
237 CGRect viewRectRelativeToScreen = [delegate convertViewRectToScreen:view.bounds];
238 CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen);
239 CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection);
244 CGFloat scale = delegate.flutterScreenIfViewLoaded.scale;
245 return portionOfKeyboardInView * scale;
250- (void)startKeyBoardAnimation:(NSTimeInterval)duration {
252 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
253 if (!delegate.isViewLoaded) {
256 UIView*
view = delegate.view;
260 if (!
self.keyboardAnimationView) {
261 UIView* keyboardAnimationView = [[UIView alloc] init];
262 keyboardAnimationView.hidden = YES;
263 self.keyboardAnimationView = keyboardAnimationView;
266 if (
self.keyboardAnimationView.superview != view) {
267 [view addSubview:self.keyboardAnimationView];
271 [
self.keyboardAnimationView.layer removeAllAnimations];
274 CGFloat currentInset = delegate.physicalViewInsetBottom;
275 self.keyboardAnimationView.frame = CGRectMake(0, currentInset, 0, 0);
276 self.keyboardAnimationStartTime = CACurrentMediaTime();
277 self.originalViewInsetBottom = currentInset;
280 [
self invalidateKeyboardAnimationVSyncClient];
283 [
self setUpKeyboardAnimationVsyncClient:^(NSTimeInterval targetTime) {
284 [weakSelf handleKeyboardAnimationCallbackWithTargetTime:targetTime];
288 [UIView animateWithDuration:duration
296 strongSelf.keyboardAnimationView.frame =
297 CGRectMake(0, strongSelf.targetViewInsetBottom, 0, 0);
300 [strongSelf.keyboardAnimationView layoutIfNeeded];
301 CAAnimation* keyboardAnimation =
302 [strongSelf.keyboardAnimationView.layer animationForKey:@"position"];
303 [strongSelf setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation];
305 completion:^(BOOL finished) {
307 if (strongSelf && strongSelf.keyboardAnimationVSyncClient == currentVsyncClient) {
311 [strongSelf invalidateKeyboardAnimationVSyncClient];
312 [strongSelf removeKeyboardAnimationView];
313 [strongSelf ensureViewportMetricsIsCorrect];
319 [
self invalidateKeyboardAnimationVSyncClient];
320 if (
self.keyboardAnimationView) {
321 [
self.keyboardAnimationView.layer removeAllAnimations];
322 [
self removeKeyboardAnimationView];
323 self.keyboardAnimationView = nil;
325 if (
self.keyboardSpringAnimation) {
326 self.keyboardSpringAnimation = nil;
328 self.targetViewInsetBottom = 0.0;
329 [
self ensureViewportMetricsIsCorrect];
332- (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
334 if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) {
335 _keyboardSpringAnimation = nil;
340 CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation;
341 _keyboardSpringAnimation =
342 [[
SpringAnimation alloc] initWithStiffness:keyboardCASpringAnimation.stiffness
343 damping:keyboardCASpringAnimation.damping
344 mass:keyboardCASpringAnimation.mass
345 initialVelocity:keyboardCASpringAnimation.initialVelocity
346 fromValue:self.originalViewInsetBottom
347 toValue:self.targetViewInsetBottom];
350- (void)handleKeyboardAnimationCallbackWithTargetTime:(NSTimeInterval)targetTime {
351 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
354 if (!delegate.isViewLoaded) {
359 if (!
self.keyboardAnimationView) {
364 if (!
self.keyboardAnimationVSyncClient) {
368 if (
self.keyboardAnimationView.superview != delegate.view) {
370 [delegate.view addSubview:self.keyboardAnimationView];
373 CGFloat currentInset = 0;
374 if (!
self.keyboardSpringAnimation) {
375 if (
self.keyboardAnimationView.layer.presentationLayer) {
376 currentInset =
self.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
379 NSTimeInterval timeElapsed = targetTime -
self.keyboardAnimationStartTime;
380 currentInset = [
self.keyboardSpringAnimation curveFunction:timeElapsed];
383 [delegate updateViewportMetricsWithInset:currentInset];
386- (void)setUpKeyboardAnimationVsyncClient:
388 if (!keyboardAnimationCallback) {
391 NSAssert(_keyboardAnimationVSyncClient == nil,
392 @"_keyboardAnimationVSyncClient must be nil when setting up.");
397 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
398 auto vsyncCallback = ^(CFTimeInterval startTime, CFTimeInterval targetTime) {
399 CFTimeInterval frameInterval = targetTime - startTime;
400 CFTimeInterval projectedTargetTime = targetTime + frameInterval;
401 dispatch_async(dispatch_get_main_queue(), ^(
void) {
402 animationCallback(projectedTargetTime);
406 initWithTaskRunner:delegate.engine.uiTaskRunner
407 isVariableRefreshRateEnabled:FlutterDisplayLinkManager.maxRefreshRateEnabledOnIPhone
408 maxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate
409 callback:vsyncCallback];
410 _keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
411 [_keyboardAnimationVSyncClient await];
414- (void)invalidateKeyboardAnimationVSyncClient {
415 [_keyboardAnimationVSyncClient invalidate];
416 _keyboardAnimationVSyncClient = nil;
419- (void)removeKeyboardAnimationView {
420 if (
self.keyboardAnimationView.superview != nil) {
421 [
self.keyboardAnimationView removeFromSuperview];
425- (void)ensureViewportMetricsIsCorrect {
426 id<FlutterKeyboardInsetManagerDelegate> delegate =
self.delegate;
427 [delegate updateViewportMetricsWithInset:self.targetViewInsetBottom];
431 [
self invalidateKeyboardAnimationVSyncClient];
432 [
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 ...
A client that wraps a CADisplayLink to deliver synchronized vsync signals.
void(^ FlutterKeyboardAnimationCallback)(NSTimeInterval)