diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 9838c341cf9c2..7a30a1fb16d03 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -187,6 +187,7 @@ 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", ] @@ -301,6 +302,7 @@ 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 d9ffc2c26d5bd..ced158a9eda07 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -30,6 +30,7 @@ #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; @@ -65,6 +66,9 @@ @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; @@ -594,6 +599,10 @@ - (UIView*)keyboardAnimationView { return _keyboardAnimationView.get(); } +- (SpringAnimation*)keyboardSpringAnimation { + return _keyboardSpringAnimation.get(); +} + - (UIScreen*)mainScreenIfViewLoaded { if (@available(iOS 13.0, *)) { if (self.viewIfLoaded == nil) { @@ -1314,13 +1323,14 @@ - (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]; @@ -1332,7 +1342,24 @@ - (void)handleKeyboardNotification:(NSNotification*)notification { self.targetViewInsetBottom = calculatedInset; NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - [self startKeyBoardAnimation:duration]; + + // 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; + } } - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification { @@ -1494,12 +1521,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 call this method first time, + // When this method is called for the first time, // initialize the keyboardAnimationView to get animation interpolation during animation. if ([self keyboardAnimationView] == nil) { UIView* keyboardAnimationView = [[UIView alloc] init]; @@ -1514,9 +1541,11 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { // Remove running animation when start another animation. [[self keyboardAnimationView].layer removeAllAnimations]; - // Set animation begin value. + // Set animation begin value and DisplayLink tracking values. [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]; @@ -1527,6 +1556,11 @@ - (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) { @@ -1540,6 +1574,24 @@ - (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) { @@ -1556,10 +1608,20 @@ - (void)setupKeyboardAnimationVsyncClient { // Ensure the keyboardAnimationView is in view hierarchy when animation running. [flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]]; } - if ([flutterViewController keyboardAnimationView].layer.presentationLayer) { - CGFloat value = - [flutterViewController keyboardAnimationView].layer.presentationLayer.frame.origin.y; - flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = value; + + 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()]; [flutterViewController updateViewportMetrics]; } }; @@ -1913,8 +1975,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 5d3c1c948df18..1461ca0640993 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -17,6 +17,7 @@ #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 @@ -114,6 +115,7 @@ @interface FlutterViewController (Tests) @property(nonatomic, assign) double targetViewInsetBottom; @property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground; +@property(nonatomic, assign) BOOL keyboardAnimationIsShowing; - (void)createTouchRateCorrectionVSyncClientIfNeeded; - (void)surfaceUpdated:(BOOL)appeared; @@ -133,6 +135,9 @@ - (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; @@ -166,6 +171,19 @@ - (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]; @@ -190,6 +208,147 @@ - (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]; @@ -197,7 +356,8 @@ - (void)testShouldIgnoreKeyboardNotification { nibName:nil bundle:nil]; FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - OCMStub([viewControllerMock mainScreenIfViewLoaded]).andReturn(UIScreen.mainScreen); + 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; @@ -215,6 +375,7 @@ - (void)testShouldIgnoreKeyboardNotification { @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) }]; + BOOL shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; XCTAssertTrue(shouldIgnore == NO); @@ -297,12 +458,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 = @@ -421,7 +582,6 @@ - (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; @@ -429,17 +589,14 @@ - (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 = OCMClassMock([UIView class]); - OCMStub([mockView frame]).andReturn(viewOrigFrame); - OCMStub([mockView convertRect:viewOrigFrame toCoordinateSpace:[OCMArg any]]) - .andReturn(convertedViewFrame); + id mockView = [self setupMockMainScreenAndView:viewControllerMock + viewFrame:viewOrigFrame + convertedFrame: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]; @@ -461,11 +618,9 @@ - (void)testCalculateKeyboardInset { CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40); CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300); - id mockView = OCMClassMock([UIView class]); - OCMStub([mockView frame]).andReturn(viewOrigFrame); - OCMStub([mockView convertRect:viewOrigFrame toCoordinateSpace:[OCMArg any]]) - .andReturn(convertedViewFrame); - OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView); + [self setupMockMainScreenAndView:viewControllerMock + viewFrame:viewOrigFrame + convertedFrame:convertedViewFrame]; CGFloat inset = [viewControllerMock calculateKeyboardInset:keyboardFrame keyboardMode:FlutterKeyboardModeDocked]; @@ -493,11 +648,7 @@ - (void)testHandleKeyboardNotification { @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) }]; FlutterViewController* viewControllerMock = OCMPartialMock(viewController); - 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); + [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; 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 5e73ab02f4fdd..110594093d222 100644 --- a/third_party/spring_animation/SpringAnimationTest.mm +++ b/third_party/spring_animation/SpringAnimationTest.mm @@ -38,3 +38,28 @@ 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 2ebcc62e518dc..9d200af90e8a1 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, readonly) double fromValue; -@property(nonatomic, assign, readonly) double toValue; +@property(nonatomic, assign) double fromValue; +@property(nonatomic, assign) double toValue; @end diff --git a/third_party/spring_animation/spring_animation.mm b/third_party/spring_animation/spring_animation.mm index 6232afff162b3..60016293a9f5d 100644 --- a/third_party/spring_animation/spring_animation.mm +++ b/third_party/spring_animation/spring_animation.mm @@ -18,7 +18,6 @@ @interface SpringAnimation () @property(nonatomic, assign) double omega0; @property(nonatomic, assign) double omega1; @property(nonatomic, assign) double v0; -@property(nonatomic, assign) double x0; @end @@ -44,22 +43,23 @@ - (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;