Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]

Expand Down Expand Up @@ -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",
Expand Down
179 changes: 92 additions & 87 deletions shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
#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;
static constexpr CGFloat kMinDurationToExecuteKeyboardAnimation = 0.01;

static NSString* const kFlutterRestorationStateAppData = @"FlutterRestorationStateAppData";

Expand Down Expand Up @@ -68,9 +68,6 @@ @interface FlutterViewController () <FlutterBinaryMessenger,
*/
@property(nonatomic, assign) double targetViewInsetBottom;
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;
@property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
@property(nonatomic, assign) fml::TimePoint keyboardAnimationStartTime;
@property(nonatomic, assign) CGFloat originalViewInsetBottom;
@property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;

/// VSyncClient for touch events delivery frame rate correction.
Expand Down Expand Up @@ -129,7 +126,7 @@ @implementation FlutterViewController {
// https://github.com/flutter/flutter/issues/35050
fml::scoped_nsobject<UIScrollView> _scrollView;
fml::scoped_nsobject<UIView> _keyboardAnimationView;
fml::scoped_nsobject<SpringAnimation> _keyboardSpringAnimation;
fml::scoped_nsobject<UIViewPropertyAnimator> _keyboardAnimator;
MouseState _mouseState;
// Timestamp after which a scroll inertia cancel event should be inferred.
NSTimeInterval _scrollInertiaEventStartline;
Expand Down Expand Up @@ -605,8 +602,8 @@ - (UIView*)keyboardAnimationView {
return _keyboardAnimationView.get();
}

- (SpringAnimation*)keyboardSpringAnimation {
return _keyboardSpringAnimation.get();
- (UIViewPropertyAnimator*)keyboardAnimator {
return _keyboardAnimator.get();
}

- (UIScreen*)mainScreenIfViewLoaded {
Expand Down Expand Up @@ -1370,14 +1367,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];
Expand All @@ -1389,24 +1385,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 {
Expand Down Expand Up @@ -1573,7 +1552,17 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
return;
}

// When this method is called for the first time,
// 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) {
UIView* keyboardAnimationView = [[UIView alloc] init];
Expand All @@ -1585,58 +1574,18 @@ - (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 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];
[self setupKeyboardAnimationVsyncClient];
VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;

[UIView animateWithDuration: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) {
// 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)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 {
Expand All @@ -1656,19 +1605,17 @@ - (void)setupKeyboardAnimationVsyncClient {
[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;
double currentRefreshRate = [DisplayLinkManager displayRefreshRate];
if (flutterViewController.get().keyboardAnimationVSyncClient) {
currentRefreshRate =
[flutterViewController.get().keyboardAnimationVSyncClient getRefreshRate];
}
[flutterViewController activateKeyboardAnimatorIfNeeded:currentRefreshRate];

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];
}
};
Expand All @@ -1688,6 +1635,64 @@ - (void)invalidateKeyboardAnimationVSyncClient {
_keyboardAnimationVSyncClient = nil;
}

- (void)activateKeyboardAnimatorIfNeeded:(double)refreshRate {
if ([self keyboardAnimator] != nil) {
return;
}

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, initialVelocity)] 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);
}

- (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 12.5;
}
}

- (void)removeKeyboardAnimationView {
if ([self keyboardAnimationView].superview != nil) {
[[self keyboardAnimationView] removeFromSuperview];
Expand Down Expand Up @@ -2022,8 +2027,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;
Expand Down
Loading