Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: eliminate one frame delay in onMove handler on iOS #412

Merged
merged 38 commits into from
May 2, 2024

Conversation

kirillzyusko
Copy link
Owner

@kirillzyusko kirillzyusko commented Apr 16, 2024

📜 Description

Improved precision of onMove handler to drive animation frame-in-frame using this handler.

💡 Motivation and Context

This problem was introduced since #87 was merged.

The thing is that we are reading layer properties using CADisplayLink. The problem is that in this case we read always out-of-date values, because CADisplayLink fires a callback before the next frame - in this case we'll always read a value for a previous frame. Thus we have always one frame delay.

The idea of this PR is to have an ability to read future values and use them. For that I had to replicate how SpringAnimation calculates its values (i. e. the math behind it, because default CASpringAnimation class doesn't give us such ability).

Now we know all variables - current timestamp, beginning of the animation, next scheduled timestamp etc. so we can calculate the next frame.

However it's also tricky, because in my observation:

  • the algorithm how next frame will be rendered is different if we compare simulator vs actual device (on a real device we have a bigger delay);
  • if debugger from XCode is attached, then frames are also calculated slightly different.

So the formula like targetTimestamp - beginTime + duration may work good if XCode debugger is not attached, but will produce junky animation when debugger is attached.

I struggled a lot and found that CADisplayLink can fire its updates with monotonic intervals (i. e. 16ms) but the keyboard position can be at this point of time in a different values (it can be 12ms or 20ms or any other value comparing to the previous frame) - see the image below as an example of values captured on simulator:

image

So in the end I decided to stick to slightly different approach - when CADisplayLink callback is fired, then we read prev frame keyboard position, and instead of calculation duration value based on absolute time intervals we are doing reversing of current keyboard frame, i. e. for a give keyboard position value we are calculating the duration (of animation), and only then calculate next frame as durationFromKeyboardPosition + frameDuration. Such approach gives a decent result on a real device (debug/release, debugger attached/detached etc.).

Last but not least - this PR improves precision for plain keyboard show/hide movements. However on iOS in certain cases animation can be not only CASpringAniamtion - right now I handle it in a PR in a fallback way, i. e. if it's not a spring animation we fallback to the mechanism introduced in #87 and simply read layer properties.

Later on I'll cover this case as well and will synchronize animations for all types of animations.

Warning

I can not test this PR on ProMotion device because I don't have it at the moment. However I'll get it back after 10.05.24, so I'll test it there and will see if any code adjustments are needed.
Attached video proof, that on iPhone 14 Pro animations are running frame in frame.

📢 Changelog

Docs

  • remove a reference about not perfectly synchronized onMove handler;

E2E

  • updated assets that were using KeyboardAnimation component;

JS

  • added gray circle in KeyboardAnimation component which is driven by onMove handler;
  • in FlatList example -> use onMove handler to handle a case when layout animation is not schedule (when keyboard closed via keyboardShouldPersistTaps)

iOS

  • added core folder;
  • added SpringAnimation class;
  • read animation from layer and try to cast it to CASpringAnimation;
  • if internal SpringAnimation class is available, then calculate keyboard position using this class;
  • don't schedule global layout animations anymore since in this case onMove will not be perfectly synchronized in any way (layout animation will override our calculations);

🤔 How Has This Been Tested?

Tested manually on:

  • iPhone 11 (iOS 17.4.1)
  • iPhone 6s (iOS 15.8)
  • iPhone 14 Pro (iOS 17.4) <- I tested it during development, however I made some changes in final code and can not test it again because this device is not with me at the moment - I'll test later when device comes back to me
  • iPhone 15 Pro (iOS 17.4, simulator)

📸 Screenshots (if appropriate):

iPhone 11, iPhone 14, iPhone 6s

KeyboardAnimation

When Simulator iPhone 11 iPhone 6s iPhone 14 Pro
Before
Screen.Recording.2024-05-01.at.17.23.22.mov
RPReplay_Final1714577657.MP4
RPReplay_Final1714664989.mp4
N/A
After
keyboard-animation.mov
RPReplay_Final1714555108.MP4
RPReplay_Final1714642652.mp4
RPReplay_Final1715175619.MP4

KeyboardAvoidingView

When Simulator iPhone 11 iPhone 6s iPhone 14 Pro
Before
Screen.Recording.2024-05-01.at.17.27.24.mov
RPReplay_Final1714577704.MP4
RPReplay_Final1714665069.mp4
N/A
After
Screen.Recording.2024-05-01.at.14.22.48.mov
iphone11-kav.mp4
RPReplay_Final1714642836.mp4
RPReplay_Final1715175667.MP4

KeyboardAwareScrollView

When Simulator iPhone 11 iPhone 6s iPhone 14 Pro
Before
Screen.Recording.2024-05-01.at.17.25.39.mov
RPReplay_Final1714577676.MP4
RPReplay_Final1714665039.mp4
N/A
After
Screen.Recording.2024-05-01.at.14.24.21.mov
RPReplay_Final1714555178.MP4
RPReplay_Final1714642685.mp4
RPReplay_Final1715175642.MP4

📝 Checklist

  • CI successfully passed
  • I added new mocks and corresponding unit-tests if library API was changed

@kirillzyusko kirillzyusko added 🍎 iOS iOS specific 🚀 optimization You optimize something and it becomes working faster labels Apr 16, 2024
@kirillzyusko kirillzyusko self-assigned this Apr 16, 2024
Copy link
Contributor

github-actions bot commented Apr 16, 2024

📊 Package size report

Current size Target Size Difference
131402 bytes 129703 bytes 1699 bytes 📈

Copy link
Contributor

github-actions bot commented Apr 16, 2024

PR Preview Action v1.4.7
Preview removed because the pull request was closed.
2024-05-02 08:37 UTC

Copy link

argos-ci bot commented Apr 16, 2024

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ✅ No change detected - May 2, 2024, 8:14 AM

@kirillzyusko kirillzyusko force-pushed the experimental/mnext-frame-math-calculation branch from 4cde354 to 3218f3e Compare April 16, 2024 22:51
@kirillzyusko kirillzyusko force-pushed the experimental/mnext-frame-math-calculation branch 2 times, most recently from 96d43c8 to 18c64be Compare April 28, 2024 18:30
…ation level, handle concurrent animations, remove willChange handler (since it doesn't give any benefits), repalce .duration with ONE_FRAME, rename displaylink to link, added TODO list
…ded because when we calculated duration as `targetTimestamp - beginTime (0.0)`, we were getting a very big value and because of that first animation frame was as a final one, so we had a jump. But since now we handle concurrent animations max(beginTime, initTime) we can remove if-statement
…removed unused code, renamed animation public methods)
…DO, fix slow animations in simulator, apply correction only if SpringAnimation indeed present
@kirillzyusko kirillzyusko force-pushed the experimental/mnext-frame-math-calculation branch from 344ffb1 to 5eb5765 Compare April 30, 2024 16:49
@kirillzyusko kirillzyusko added the documentation Improvements or additions to documentation label May 1, 2024
@kirillzyusko kirillzyusko marked this pull request as ready for review May 1, 2024 12:33
@kirillzyusko kirillzyusko merged commit 27da1d0 into main May 2, 2024
19 checks passed
@kirillzyusko kirillzyusko deleted the experimental/mnext-frame-math-calculation branch May 2, 2024 08:35
kirillzyusko added a commit that referenced this pull request May 4, 2024
…n driver (#431)

## 📜 Description

Added `KeyboardAnimation` class (as common interface) which drives an
animation of the keyboard.

## 💡 Motivation and Context

Follow up for
#412

Keyboard animation can be driven not only by `CASpringAnimation` class.
What we want to achieve is to have a **single interface** that gives us
all necessary properties (value for a particular timing, etc.), but will
incapsulate calculations of these values internally.

So in this PR I added `KeyboardAnimation` abstract class that uses some
common properties for all animations (from/to value, speed, init time),
has universal method for finding a particular timing for a given value
(since we use a binary search) and defines other publicly available
functions.

And in this PR I also started to use this universal interface
(`KeyboardAnimation`) in `KeyboardMovementObserver` (`SpringAnimation`
inherits from `KeyboardAnimation`).

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### iOS

- added `KeyboardAnimation` class and `KeyboardAnimationProtocol`;
- moved all shared logic in `KeyboardAnimation` class;
- started to use `KeyboardAnimation` as a common interface for
`animation`.

## 🤔 How Has This Been Tested?

Tested on iPhone 11 (iOS 17.4), real device.

## 📸 Screenshots (if appropriate):

No visual changes.

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
@kirillzyusko kirillzyusko mentioned this pull request Jul 22, 2024
2 tasks
kirillzyusko added a commit that referenced this pull request Jul 22, 2024
## 📜 Description

A follow up PR for
#412
- follow keyboard frame-in-frame if keyboard animation is **not** spring
animation.

## 💡 Motivation and Context

On iOS keyboard animation can be not only spring animation. It can be
also `CABasicAnimation` (when keyboard goes up after interactive
dismissal, for example).

Before we were relying on `CADisplayLink` and coordinates pooling, but
such approach doesn't give a good precision. Even after implementing
custom `SpringAnimation` we still relied on this approach (we used this
approach as a fallback if the animation is not `CASpringAnimation`).

In this PR I implemented custom `TimingAnimation` class and if the
animation is `CABasicAnimation` then we also can assure frame precision.
All these math calculations were based on this [Desmos
playground](https://www.desmos.com/calculator/eynenh1aga?lang=en) - I
used concrete params of a particular animation config and concrete
values produced by iOS. And then I wrote a code 🙃

> _As you can see some of points are not perfectly aligned to the curve
- I think a similar behavior I also observed with `SpringAnimation` and
later on I'll improve that as well_

For finding `t` for particular `x` (or `y`) I used
[Newton-Raphson](https://en.wikipedia.org/wiki/Newton%27s_method)
method. It has better convergence than a binary search and typically I
can find a value with given precision within 6 iterations.

Last but not least - the animation (`CABaicAnimation`) is not available
straight in `keyboardWillShow` callback (it's `nil` there) and it's
available only in `CADisplayLink` callback (so I added an attempt of
animation initialization there as well).

## 📢 Changelog

<!-- High level overview of important changes -->
<!-- For example: fixed status bar manipulation; added new types
declarations; -->
<!-- If your changes don't affect one of platform/language below - then
remove this platform/language -->

### iOS

- added `TimingAnimation` class;
- use `TimingAnimation` in `KeyboardMovementObserver`.

## 🤔 How Has This Been Tested?

Tested manually on:
- iPhone 6s (iOS 15.8)
- iPhone 11 (iOS 17.4);
- iPhone 14 Pro (iOS 17.4);
- iPhone 15 Pro (iOS 17.5, simulator);

## 📸 Screenshots (if appropriate):

|Before|After|
|-------|-----|
|<video
src="https://github.com/user-attachments/assets/1c1bba40-c76e-468f-87aa-7e9105715ab7">|<video
src="https://github.com/user-attachments/assets/55de0c4d-9d0a-4775-a4dd-4983acc30283">|

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation 🍎 iOS iOS specific 🚀 optimization You optimize something and it becomes working faster
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant