From 098cdf40d52beba8ee72933ccd2a1ff3e27ef4af Mon Sep 17 00:00:00 2001 From: luckysmg <2539699336@qq.com> Date: Sun, 2 Apr 2023 13:28:43 +0800 Subject: [PATCH 1/4] Revert "iOS keyboard animation synchronization (#37604)" This reverts commit 165126e7034c9d8c2075c5ba110ff2ca0a83e02c. --- shell/platform/darwin/ios/BUILD.gn | 2 - .../framework/Source/FlutterViewController.mm | 84 +------- .../Source/FlutterViewControllerTest.mm | 191 ++---------------- .../spring_animation/SpringAnimationTest.mm | 25 --- .../spring_animation/spring_animation.h | 4 +- .../spring_animation/spring_animation.mm | 10 +- 6 files changed, 38 insertions(+), 278 deletions(-) diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 7a30a1fb16d03..9838c341cf9c2 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -187,7 +187,6 @@ source_set("flutter_framework_source") { "//flutter/shell/platform/darwin/common:framework_shared", "//flutter/shell/platform/embedder:embedder_as_internal_library", "//flutter/shell/profiling:profiling", - "//flutter/third_party/spring_animation", "//third_party/skia", ] @@ -302,7 +301,6 @@ shared_library("ios_test_flutter") { "//flutter/shell/platform/darwin/common:framework_shared", "//flutter/shell/platform/embedder:embedder_as_internal_library", "//flutter/shell/platform/embedder:embedder_test_utils", - "//flutter/third_party/spring_animation", "//flutter/third_party/tonic", "//flutter/third_party/txt", "//third_party/ocmock:ocmock_shared", diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 77742b0114c9e..8694277d2c426 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -30,7 +30,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" #import "flutter/shell/platform/embedder/embedder.h" -#import "flutter/third_party/spring_animation/spring_animation.h" static constexpr int kMicrosecondsPerSecond = 1000 * 1000; static constexpr CGFloat kScrollViewContentSize = 2.0; @@ -68,9 +67,6 @@ @interface FlutterViewController () _scrollView; fml::scoped_nsobject _keyboardAnimationView; - fml::scoped_nsobject _keyboardSpringAnimation; MouseState _mouseState; // Timestamp after which a scroll inertia cancel event should be inferred. NSTimeInterval _scrollInertiaEventStartline; @@ -605,10 +600,6 @@ - (UIView*)keyboardAnimationView { return _keyboardAnimationView.get(); } -- (SpringAnimation*)keyboardSpringAnimation { - return _keyboardSpringAnimation.get(); -} - - (UIScreen*)mainScreenIfViewLoaded { if (@available(iOS 13.0, *)) { if (self.viewIfLoaded == nil) { @@ -1370,14 +1361,13 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification { } - (void)handleKeyboardNotification:(NSNotification*)notification { - // See https://flutter.dev/go/ios-keyboard-calculating-inset for more details + // See https:://flutter.dev/go/ios-keyboard-calculating-inset for more details // on why notifications are used and how things are calculated. if ([self shouldIgnoreKeyboardNotification:notification]) { return; } NSDictionary* info = notification.userInfo; - CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification]; CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode]; @@ -1389,24 +1379,7 @@ - (void)handleKeyboardNotification:(NSNotification*)notification { self.targetViewInsetBottom = calculatedInset; NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - - // Flag for simultaneous compounding animation calls. - // This captures animation calls made while the keyboard animation is currently animating. If the - // new animation is in the same direction as the current animation, this flag lets the current - // animation continue with an updated targetViewInsetBottom instead of starting a new keyboard - // animation. This allows for smoother keyboard animation interpolation. - BOOL keyboardWillShow = beginKeyboardFrame.origin.y > keyboardFrame.origin.y; - BOOL keyboardAnimationIsCompounding = - self.keyboardAnimationIsShowing == keyboardWillShow && _keyboardAnimationVSyncClient != nil; - - // Mark keyboard as showing or hiding. - self.keyboardAnimationIsShowing = keyboardWillShow; - - if (!keyboardAnimationIsCompounding) { - [self startKeyBoardAnimation:duration]; - } else if ([self keyboardSpringAnimation]) { - [self keyboardSpringAnimation].toValue = self.targetViewInsetBottom; - } + [self startKeyBoardAnimation:duration]; } - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification { @@ -1568,12 +1541,12 @@ - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger) } - (void)startKeyBoardAnimation:(NSTimeInterval)duration { - // If current physical_view_inset_bottom == targetViewInsetBottom, do nothing. + // If current physical_view_inset_bottom == targetViewInsetBottom,do nothing. if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom) { return; } - // When this method is called for the first time, + // When call this method first time, // initialize the keyboardAnimationView to get animation interpolation during animation. if ([self keyboardAnimationView] == nil) { UIView* keyboardAnimationView = [[UIView alloc] init]; @@ -1588,11 +1561,9 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { // Remove running animation when start another animation. [[self keyboardAnimationView].layer removeAllAnimations]; - // Set animation begin value and DisplayLink tracking values. + // Set animation begin value. [self keyboardAnimationView].frame = CGRectMake(0, _viewportMetrics.physical_view_inset_bottom, 0, 0); - self.keyboardAnimationStartTime = fml::TimePoint().Now(); - self.originalViewInsetBottom = _viewportMetrics.physical_view_inset_bottom; // Invalidate old vsync client if old animation is not completed. [self invalidateKeyboardAnimationVSyncClient]; @@ -1603,11 +1574,6 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { animations:^{ // Set end value. [self keyboardAnimationView].frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0); - - // Setup keyboard animation interpolation. - CAAnimation* keyboardAnimation = - [[self keyboardAnimationView].layer animationForKey:@"position"]; - [self setupKeyboardSpringAnimationIfNeeded:keyboardAnimation]; } completion:^(BOOL finished) { if (_keyboardAnimationVSyncClient == currentVsyncClient) { @@ -1621,24 +1587,6 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { }]; } -- (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation { - // If keyboard animation is null or not a spring animation, fallback to DisplayLink tracking. - if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) { - _keyboardSpringAnimation.reset(); - return; - } - - // Setup keyboard spring animation details for spring curve animation calculation. - CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation; - _keyboardSpringAnimation.reset([[SpringAnimation alloc] - initWithStiffness:keyboardCASpringAnimation.stiffness - damping:keyboardCASpringAnimation.damping - mass:keyboardCASpringAnimation.mass - initialVelocity:keyboardCASpringAnimation.initialVelocity - fromValue:self.originalViewInsetBottom - toValue:self.targetViewInsetBottom]); -} - - (void)setupKeyboardAnimationVsyncClient { auto callback = [weakSelf = [self getWeakPtr]](std::unique_ptr recorder) { @@ -1655,20 +1603,10 @@ - (void)setupKeyboardAnimationVsyncClient { // Ensure the keyboardAnimationView is in view hierarchy when animation running. [flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]]; } - - if ([flutterViewController keyboardSpringAnimation] == nil) { - if (flutterViewController.get().keyboardAnimationView.layer.presentationLayer) { - flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = - flutterViewController.get() - .keyboardAnimationView.layer.presentationLayer.frame.origin.y; - [flutterViewController updateViewportMetrics]; - } - } else { - fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() - - flutterViewController.get().keyboardAnimationStartTime; - - flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = - [[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()]; + if ([flutterViewController keyboardAnimationView].layer.presentationLayer) { + CGFloat value = + [flutterViewController keyboardAnimationView].layer.presentationLayer.frame.origin.y; + flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = value; [flutterViewController updateViewportMetrics]; } }; @@ -2022,8 +1960,8 @@ - (BOOL)isAlwaysUse24HourFormat { } // The brightness mode of the platform, e.g., light or dark, expressed as a string that -// is understood by the Flutter framework. See the settings -// system channel for more information. +// is understood by the Flutter framework. See the settings system channel for more +// information. - (NSString*)brightnessMode { if (@available(iOS 13, *)) { UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 575215c2e77cb..8ae2b4db90694 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -17,7 +17,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/embedder/embedder.h" -#import "flutter/third_party/spring_animation/spring_animation.h" FLUTTER_ASSERT_ARC @@ -117,7 +116,6 @@ @interface FlutterViewController (Tests) @property(nonatomic, assign) double targetViewInsetBottom; @property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground; -@property(nonatomic, assign) BOOL keyboardAnimationIsShowing; - (void)createTouchRateCorrectionVSyncClientIfNeeded; - (void)surfaceUpdated:(BOOL)appeared; @@ -138,9 +136,6 @@ - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame; - (void)startKeyBoardAnimation:(NSTimeInterval)duration; - (void)setupKeyboardAnimationVsyncClient; -- (UIView*)keyboardAnimationView; -- (SpringAnimation*)keyboardSpringAnimation; -- (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation; - (void)ensureViewportMetricsIsCorrect; - (void)invalidateKeyboardAnimationVSyncClient; - (void)addInternalPlugins; @@ -174,19 +169,6 @@ - (void)tearDown { self.messageSent = nil; } -- (id)setupMockMainScreenAndView:(FlutterViewController*)viewControllerMock - viewFrame:(CGRect)viewFrame - convertedFrame:(CGRect)convertedFrame { - OCMStub([viewControllerMock mainScreenIfViewLoaded]).andReturn(UIScreen.mainScreen); - id mockView = OCMClassMock([UIView class]); - OCMStub([mockView frame]).andReturn(viewFrame); - OCMStub([mockView convertRect:viewFrame toCoordinateSpace:[OCMArg any]]) - .andReturn(convertedFrame); - OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView); - - return mockView; -} - - (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; @@ -211,147 +193,6 @@ - (void)testStartKeyboardAnimationWillInvokeSetupKeyboardAnimationVsyncClient { OCMVerify([viewControllerMock setupKeyboardAnimationVsyncClient]); } -- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardSpringAnimationIfNeeded { - FlutterEngine* engine = [[FlutterEngine alloc] init]; - [engine runWithEntrypoint:nil]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; - FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - viewControllerMock.targetViewInsetBottom = 100; - [viewControllerMock startKeyBoardAnimation:0.25]; - - CAAnimation* keyboardAnimation = - [[viewControllerMock keyboardAnimationView].layer animationForKey:@"position"]; - - OCMVerify([viewControllerMock setupKeyboardSpringAnimationIfNeeded:keyboardAnimation]); -} - -- (void)testSetupKeyboardSpringAnimationIfNeeded { - FlutterEngine* engine = [[FlutterEngine alloc] init]; - [engine runWithEntrypoint:nil]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; - FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - CGRect viewFrame = UIScreen.mainScreen.bounds; - [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; - - // Null check. - [viewControllerMock setupKeyboardSpringAnimationIfNeeded:nil]; - SpringAnimation* keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation]; - XCTAssertTrue(keyboardSpringAnimation == nil); - - // CAAnimation that is not a CASpringAnimation. - CABasicAnimation* nonSpringAnimation = [CABasicAnimation animation]; - nonSpringAnimation.duration = 1.0; - nonSpringAnimation.fromValue = [NSNumber numberWithFloat:0.0]; - nonSpringAnimation.toValue = [NSNumber numberWithFloat:1.0]; - nonSpringAnimation.keyPath = @"position"; - [viewControllerMock setupKeyboardSpringAnimationIfNeeded:nonSpringAnimation]; - keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation]; - - XCTAssertTrue(keyboardSpringAnimation == nil); - - // CASpringAnimation. - CASpringAnimation* springAnimation = [CASpringAnimation animation]; - springAnimation.mass = 1.0; - springAnimation.stiffness = 100.0; - springAnimation.damping = 10.0; - springAnimation.keyPath = @"position"; - springAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)]; - springAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)]; - [viewControllerMock setupKeyboardSpringAnimationIfNeeded:springAnimation]; - keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation]; - XCTAssertTrue(keyboardSpringAnimation != nil); -} - -- (void)testKeyboardAnimationIsShowingAndCompounding { - FlutterEngine* engine = [[FlutterEngine alloc] init]; - [engine runWithEntrypoint:nil]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine - nibName:nil - bundle:nil]; - FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - CGRect viewFrame = UIScreen.mainScreen.bounds; - [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; - - BOOL isLocal = YES; - CGFloat screenHeight = UIScreen.mainScreen.bounds.size.height; - CGFloat screenWidth = UIScreen.mainScreen.bounds.size.height; - - // Start show keyboard animation. - CGRect initialShowKeyboardBeginFrame = CGRectMake(0, screenHeight, screenWidth, 250); - CGRect initialShowKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500); - NSNotification* fakeNotification = [NSNotification - notificationWithName:UIKeyboardWillChangeFrameNotification - object:nil - userInfo:@{ - @"UIKeyboardFrameBeginUserInfoKey" : @(initialShowKeyboardBeginFrame), - @"UIKeyboardFrameEndUserInfoKey" : @(initialShowKeyboardEndFrame), - @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), - @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) - }]; - viewControllerMock.targetViewInsetBottom = 0; - [viewControllerMock handleKeyboardNotification:fakeNotification]; - BOOL isShowingAnimation1 = viewControllerMock.keyboardAnimationIsShowing; - XCTAssertTrue(isShowingAnimation1); - - // Start compounding show keyboard animation. - CGRect compoundingShowKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250); - CGRect compoundingShowKeyboardEndFrame = CGRectMake(0, screenHeight - 500, screenWidth, 500); - fakeNotification = [NSNotification - notificationWithName:UIKeyboardWillChangeFrameNotification - object:nil - userInfo:@{ - @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingShowKeyboardBeginFrame), - @"UIKeyboardFrameEndUserInfoKey" : @(compoundingShowKeyboardEndFrame), - @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), - @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) - }]; - - [viewControllerMock handleKeyboardNotification:fakeNotification]; - BOOL isShowingAnimation2 = viewControllerMock.keyboardAnimationIsShowing; - XCTAssertTrue(isShowingAnimation2); - XCTAssertTrue(isShowingAnimation1 == isShowingAnimation2); - - // Start hide keyboard animation. - CGRect initialHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 500, screenWidth, 250); - CGRect initialHideKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500); - fakeNotification = [NSNotification - notificationWithName:UIKeyboardWillChangeFrameNotification - object:nil - userInfo:@{ - @"UIKeyboardFrameBeginUserInfoKey" : @(initialHideKeyboardBeginFrame), - @"UIKeyboardFrameEndUserInfoKey" : @(initialHideKeyboardEndFrame), - @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), - @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) - }]; - - [viewControllerMock handleKeyboardNotification:fakeNotification]; - BOOL isShowingAnimation3 = viewControllerMock.keyboardAnimationIsShowing; - XCTAssertFalse(isShowingAnimation3); - XCTAssertTrue(isShowingAnimation2 != isShowingAnimation3); - - // Start compounding hide keyboard animation. - CGRect compoundingHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250); - CGRect compoundingHideKeyboardEndFrame = CGRectMake(0, screenHeight, screenWidth, 500); - fakeNotification = [NSNotification - notificationWithName:UIKeyboardWillChangeFrameNotification - object:nil - userInfo:@{ - @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingHideKeyboardBeginFrame), - @"UIKeyboardFrameEndUserInfoKey" : @(compoundingHideKeyboardEndFrame), - @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), - @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) - }]; - - [viewControllerMock handleKeyboardNotification:fakeNotification]; - BOOL isShowingAnimation4 = viewControllerMock.keyboardAnimationIsShowing; - XCTAssertFalse(isShowingAnimation4); - XCTAssertTrue(isShowingAnimation3 == isShowingAnimation4); -} - - (void)testShouldIgnoreKeyboardNotification { FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; @@ -359,8 +200,7 @@ - (void)testShouldIgnoreKeyboardNotification { nibName:nil bundle:nil]; FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - CGRect viewFrame = UIScreen.mainScreen.bounds; - [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; + OCMStub([viewControllerMock mainScreenIfViewLoaded]).andReturn(UIScreen.mainScreen); CGFloat screenWidth = UIScreen.mainScreen.bounds.size.width; CGFloat screenHeight = UIScreen.mainScreen.bounds.size.height; @@ -378,7 +218,6 @@ - (void)testShouldIgnoreKeyboardNotification { @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) }]; - BOOL shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; XCTAssertTrue(shouldIgnore == NO); @@ -461,12 +300,12 @@ - (void)testCalculateKeyboardAttachMode { bundle:nil]; FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - CGRect viewFrame = UIScreen.mainScreen.bounds; - [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; CGFloat screenWidth = UIScreen.mainScreen.bounds.size.width; CGFloat screenHeight = UIScreen.mainScreen.bounds.size.height; + OCMStub([viewControllerMock mainScreenIfViewLoaded]).andReturn(UIScreen.mainScreen); + // hide notification CGRect keyboardFrame = CGRectZero; NSNotification* notification = @@ -585,6 +424,7 @@ - (void)testCalculateMultitaskingAdjustment { nibName:nil bundle:nil]; FlutterViewController* viewControllerMock = OCMPartialMock(viewController); + OCMStub([viewControllerMock mainScreenIfViewLoaded]).andReturn(UIScreen.mainScreen); CGFloat screenWidth = UIScreen.mainScreen.bounds.size.width; CGFloat screenHeight = UIScreen.mainScreen.bounds.size.height; @@ -592,14 +432,17 @@ - (void)testCalculateMultitaskingAdjustment { CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40); CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40); CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300); - id mockView = [self setupMockMainScreenAndView:viewControllerMock - viewFrame:viewOrigFrame - convertedFrame:convertedViewFrame]; + + id mockView = OCMClassMock([UIView class]); + OCMStub([mockView frame]).andReturn(viewOrigFrame); + OCMStub([mockView convertRect:viewOrigFrame toCoordinateSpace:[OCMArg any]]) + .andReturn(convertedViewFrame); id mockTraitCollection = OCMClassMock([UITraitCollection class]); OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); OCMStub([mockTraitCollection horizontalSizeClass]).andReturn(UIUserInterfaceSizeClassCompact); OCMStub([mockTraitCollection verticalSizeClass]).andReturn(UIUserInterfaceSizeClassRegular); OCMStub([mockView traitCollection]).andReturn(mockTraitCollection); + OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView); CGFloat adjustment = [viewControllerMock calculateMultitaskingAdjustment:screenRect keyboardFrame:keyboardFrame]; @@ -621,9 +464,11 @@ - (void)testCalculateKeyboardInset { CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40); CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300); - [self setupMockMainScreenAndView:viewControllerMock - viewFrame:viewOrigFrame - convertedFrame:convertedViewFrame]; + id mockView = OCMClassMock([UIView class]); + OCMStub([mockView frame]).andReturn(viewOrigFrame); + OCMStub([mockView convertRect:viewOrigFrame toCoordinateSpace:[OCMArg any]]) + .andReturn(convertedViewFrame); + OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView); CGFloat inset = [viewControllerMock calculateKeyboardInset:keyboardFrame keyboardMode:FlutterKeyboardModeDocked]; @@ -651,7 +496,11 @@ - (void)testHandleKeyboardNotification { @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) }]; FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; + OCMStub([viewControllerMock mainScreenIfViewLoaded]).andReturn(UIScreen.mainScreen); + id mockView = OCMClassMock([UIView class]); + OCMStub([mockView frame]).andReturn(viewFrame); + OCMStub([mockView convertRect:viewFrame toCoordinateSpace:[OCMArg any]]).andReturn(viewFrame); + OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView); viewControllerMock.targetViewInsetBottom = 0; XCTestExpectation* expectation = [self expectationWithDescription:@"update viewport"]; OCMStub([mockEngine updateViewportMetrics:flutter::ViewportMetrics()]) diff --git a/third_party/spring_animation/SpringAnimationTest.mm b/third_party/spring_animation/SpringAnimationTest.mm index 110594093d222..5e73ab02f4fdd 100644 --- a/third_party/spring_animation/SpringAnimationTest.mm +++ b/third_party/spring_animation/SpringAnimationTest.mm @@ -38,28 +38,3 @@ const double toValue = [animation curveFunction:endTime]; ASSERT_TRUE(fabs(toValue - animation.toValue) < accuracy); } - -TEST(SpringAnimationTest, CanUpdatePositionValuesAndCalculateCorrectly) { - SpringAnimation* animation = [[SpringAnimation alloc] initWithStiffness:1000 - damping:500 - mass:3 - initialVelocity:0 - fromValue:0 - toValue:1000]; - const double startTime = 0; - const double endTime = 0.6; - - const double startValue1 = [animation curveFunction:startTime]; - const double toValue1 = [animation curveFunction:endTime]; - - animation.fromValue = 10; - animation.toValue = 800; - - ASSERT_TRUE(animation.fromValue == 10); - ASSERT_TRUE(animation.toValue == 800); - - const double startValue2 = [animation curveFunction:startTime]; - const double toValue2 = [animation curveFunction:endTime]; - ASSERT_TRUE(startValue2 > startValue1); - ASSERT_TRUE(toValue2 < toValue1); -} diff --git a/third_party/spring_animation/spring_animation.h b/third_party/spring_animation/spring_animation.h index 9d200af90e8a1..2ebcc62e518dc 100644 --- a/third_party/spring_animation/spring_animation.h +++ b/third_party/spring_animation/spring_animation.h @@ -35,8 +35,8 @@ @property(nonatomic, assign, readonly) double damping; @property(nonatomic, assign, readonly) double mass; @property(nonatomic, assign, readonly) double initialVelocity; -@property(nonatomic, assign) double fromValue; -@property(nonatomic, assign) double toValue; +@property(nonatomic, assign, readonly) double fromValue; +@property(nonatomic, assign, readonly) double toValue; @end diff --git a/third_party/spring_animation/spring_animation.mm b/third_party/spring_animation/spring_animation.mm index 60016293a9f5d..6232afff162b3 100644 --- a/third_party/spring_animation/spring_animation.mm +++ b/third_party/spring_animation/spring_animation.mm @@ -18,6 +18,7 @@ @interface SpringAnimation () @property(nonatomic, assign) double omega0; @property(nonatomic, assign) double omega1; @property(nonatomic, assign) double v0; +@property(nonatomic, assign) double x0; @end @@ -43,23 +44,22 @@ - (instancetype)initWithStiffness:(double)stiffness _omega0 = sqrt(_stiffness / _mass); // Undamped angular frequency of the oscillator. _omega1 = _omega0 * sqrt(1.0 - _zeta * _zeta); // Exponential decay. _v0 = -_initialVelocity; + _x0 = _toValue - _fromValue; } return self; } - (double)curveFunction:(double)t { - const double x0 = _toValue - _fromValue; - double y; if (_zeta < 1) { // Under damped. const double envelope = exp(-_zeta * _omega0 * t); - y = _toValue - envelope * (((_v0 + _zeta * _omega0 * x0) / _omega1) * sin(_omega1 * t) + - x0 * cos(_omega1 * t)); + y = _toValue - envelope * (((_v0 + _zeta * _omega0 * _x0) / _omega1) * sin(_omega1 * t) + + _x0 * cos(_omega1 * t)); } else { // Critically damped. const double envelope = exp(-_omega0 * t); - y = _toValue - envelope * (x0 + (_v0 + _omega0 * x0) * t); + y = _toValue - envelope * (_x0 + (_v0 + _omega0 * _x0) * t); } return y; From 90bab3e16fe0e2908775eb350cea5564060ad8b6 Mon Sep 17 00:00:00 2001 From: luckysmg <2539699336@qq.com> Date: Sun, 2 Apr 2023 14:44:34 +0800 Subject: [PATCH 2/4] Using UIViewPropertyAnimator for keyboard animation presentationLayer tracking. --- .../framework/Source/FlutterViewController.mm | 99 +++++++++++++++---- 1 file changed, 79 insertions(+), 20 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 8694277d2c426..d8e0d34f8dcb9 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -33,6 +33,7 @@ static constexpr int kMicrosecondsPerSecond = 1000 * 1000; static constexpr CGFloat kScrollViewContentSize = 2.0; +static constexpr CGFloat kMinDurationToExecuteKeyboardAnimation = 0.01; static NSString* const kFlutterRestorationStateAppData = @"FlutterRestorationStateAppData"; @@ -125,6 +126,7 @@ @implementation FlutterViewController { // https://github.com/flutter/flutter/issues/35050 fml::scoped_nsobject _scrollView; fml::scoped_nsobject _keyboardAnimationView; + fml::scoped_nsobject _keyboardAnimator; MouseState _mouseState; // Timestamp after which a scroll inertia cancel event should be inferred. NSTimeInterval _scrollInertiaEventStartline; @@ -600,6 +602,10 @@ - (UIView*)keyboardAnimationView { return _keyboardAnimationView.get(); } +- (UIViewPropertyAnimator*)keyboardAnimator { + return _keyboardAnimator.get(); +} + - (UIScreen*)mainScreenIfViewLoaded { if (@available(iOS 13.0, *)) { if (self.viewIfLoaded == nil) { @@ -1541,11 +1547,21 @@ - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger) } - (void)startKeyBoardAnimation:(NSTimeInterval)duration { - // If current physical_view_inset_bottom == targetViewInsetBottom,do nothing. + // If current physical_view_inset_bottom == targetViewInsetBottom, do nothing. if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom) { return; } + // If the duation we get from notification is near zero, we just update the + // view inset instead of start a keyboard animation. This happens when we change + // the keyboard type. + // eg: We change the keyboard type from text to emoji. + if (duration < kMinDurationToExecuteKeyboardAnimation) { + _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom; + [self updateViewportMetrics]; + return; + } + // When call this method first time, // initialize the keyboardAnimationView to get animation interpolation during animation. if ([self keyboardAnimationView] == nil) { @@ -1558,8 +1574,10 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { [self.view addSubview:[self keyboardAnimationView]]; } - // Remove running animation when start another animation. - [[self keyboardAnimationView].layer removeAllAnimations]; + // Stop previous running animation. And clear the animator to recreate it when + // next animation begins. + [[self keyboardAnimator] stopAnimation:YES]; + _keyboardAnimator.reset(); // Set animation begin value. [self keyboardAnimationView].frame = @@ -1568,23 +1586,6 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { // Invalidate old vsync client if old animation is not completed. [self invalidateKeyboardAnimationVSyncClient]; [self setupKeyboardAnimationVsyncClient]; - VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient; - - [UIView animateWithDuration:duration - animations:^{ - // Set end value. - [self keyboardAnimationView].frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0); - } - completion:^(BOOL finished) { - if (_keyboardAnimationVSyncClient == currentVsyncClient) { - // Indicates the vsync client captured by this block is the original one, which also - // indicates the animation has not been interrupted from its beginning. Moreover, - // indicates the animation is over and there is no more to execute. - [self invalidateKeyboardAnimationVSyncClient]; - [self removeKeyboardAnimationView]; - [self ensureViewportMetricsIsCorrect]; - } - }]; } - (void)setupKeyboardAnimationVsyncClient { @@ -1603,6 +1604,14 @@ - (void)setupKeyboardAnimationVsyncClient { // Ensure the keyboardAnimationView is in view hierarchy when animation running. [flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]]; } + + double currentRefreshRate = [DisplayLinkManager displayRefreshRate]; + if (flutterViewController.get().keyboardAnimationVSyncClient) { + currentRefreshRate = + [flutterViewController.get().keyboardAnimationVSyncClient getRefreshRate]; + } + [flutterViewController activateKeyboardAnimatorIfNeeded:currentRefreshRate]; + if ([flutterViewController keyboardAnimationView].layer.presentationLayer) { CGFloat value = [flutterViewController keyboardAnimationView].layer.presentationLayer.frame.origin.y; @@ -1626,6 +1635,56 @@ - (void)invalidateKeyboardAnimationVSyncClient { _keyboardAnimationVSyncClient = nil; } +- (void)activateKeyboardAnimatorIfNeeded:(double)refreshRate { + if ([self keyboardAnimator] != nil) { + return; + } + + // Change the initialVelocity according to current refresh rate. + // Using presentationLayer to tracking animation, the lower refresh rate + // (The larger frame interval time) will enlarge the gap with the + // system's keyboard we are tracking, so we should increase the + // initialVelocity when refresh rate is low to track the system's keyboard + // better. + const double maxRefreshRate = [DisplayLinkManager displayRefreshRate]; + const double baseInitialVelocity = 8.5; + const double adaptiveInitialVelocity = + baseInitialVelocity + (maxRefreshRate - refreshRate) * 4.0 / 60.0; + + VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient; + + // iOS system's keyboard animation spring configuration. + const double damping = 500; + const double mass = 3; + const double stiffness = 1000; + UISpringTimingParameters* spring = [[[UISpringTimingParameters alloc] + initWithMass:mass + stiffness:stiffness + damping:damping + initialVelocity:CGVectorMake(0, adaptiveInitialVelocity)] autorelease]; + + // The duration doesn't matter, because the spring animation will not be + // impacted by duration, so just pass 0. + UIViewPropertyAnimator* animator = [[UIViewPropertyAnimator alloc] initWithDuration:0 + timingParameters:spring]; + [animator addAnimations:^{ + [self keyboardAnimationView].frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0); + }]; + [animator addCompletion:^(UIViewAnimatingPosition finalPosition) { + if (_keyboardAnimationVSyncClient == currentVsyncClient) { + // Indicates the vsync client captured by this block is the original one, + // which also indicates the animation has not been interrupted from its + // beginning. Moreover, indicates the animation is over and there is no + // more to execute. + [self invalidateKeyboardAnimationVSyncClient]; + [self removeKeyboardAnimationView]; + [self ensureViewportMetricsIsCorrect]; + } + }]; + [animator startAnimation]; + _keyboardAnimator.reset(animator); +} + - (void)removeKeyboardAnimationView { if ([self keyboardAnimationView].superview != nil) { [[self keyboardAnimationView] removeFromSuperview]; From a8aec7f968676a66eb15e00e9fdf9c4ae091778a Mon Sep 17 00:00:00 2001 From: luckysmg <2539699336@qq.com> Date: Mon, 3 Apr 2023 11:17:06 +0800 Subject: [PATCH 3/4] Optimize Velocity computation logic. --- .../framework/Source/FlutterViewController.mm | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index d8e0d34f8dcb9..25ea616670112 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -1640,28 +1640,18 @@ - (void)activateKeyboardAnimatorIfNeeded:(double)refreshRate { return; } - // Change the initialVelocity according to current refresh rate. - // Using presentationLayer to tracking animation, the lower refresh rate - // (The larger frame interval time) will enlarge the gap with the - // system's keyboard we are tracking, so we should increase the - // initialVelocity when refresh rate is low to track the system's keyboard - // better. - const double maxRefreshRate = [DisplayLinkManager displayRefreshRate]; - const double baseInitialVelocity = 8.5; - const double adaptiveInitialVelocity = - baseInitialVelocity + (maxRefreshRate - refreshRate) * 4.0 / 60.0; - VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient; // iOS system's keyboard animation spring configuration. const double damping = 500; const double mass = 3; const double stiffness = 1000; + const double initialVelocity = [self computeSpringAnimationAdaptiveInitialVelocity:refreshRate]; UISpringTimingParameters* spring = [[[UISpringTimingParameters alloc] initWithMass:mass stiffness:stiffness damping:damping - initialVelocity:CGVectorMake(0, adaptiveInitialVelocity)] autorelease]; + initialVelocity:CGVectorMake(0, initialVelocity)] autorelease]; // The duration doesn't matter, because the spring animation will not be // impacted by duration, so just pass 0. @@ -1685,6 +1675,24 @@ - (void)activateKeyboardAnimatorIfNeeded:(double)refreshRate { _keyboardAnimator.reset(animator); } +- (double)computeSpringAnimationAdaptiveInitialVelocity:(double)refreshRate { + // Return diffrent velocity according to current refresh rate. + // Using presentationLayer to tracking animation, the lower refresh rate + // (The larger frame interval time) will enlarge the gap with the + // system's keyboard we are tracking, so we should increase the + // initialVelocity when refresh rate is low to track the system's keyboard + // better. + + const double epsilon = 0.1; + if (refreshRate >= 80.0 - epsilon) { + // For refresh rate at 80 ~ 120. + return 9.0; + } else { + // Below 80. + return 13.0; + } +} + - (void)removeKeyboardAnimationView { if ([self keyboardAnimationView].superview != nil) { [[self keyboardAnimationView] removeFromSuperview]; From 57eb2954455ea16004b1c7b83b6d00a6dd1bc452 Mon Sep 17 00:00:00 2001 From: luckysmg <2539699336@qq.com> Date: Mon, 3 Apr 2023 11:27:47 +0800 Subject: [PATCH 4/4] ++ --- .../darwin/ios/framework/Source/FlutterViewController.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 25ea616670112..3f88cf45b4c26 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -1689,7 +1689,7 @@ - (double)computeSpringAnimationAdaptiveInitialVelocity:(double)refreshRate { return 9.0; } else { // Below 80. - return 13.0; + return 12.5; } }