Skip to content

Commit

Permalink
fix: slow back transitions on Android < 11 in `KeyboardAwareScrollVie…
Browse files Browse the repository at this point in the history
…w` (#342)

## 📜 Description

Improved FPS for `KeyboardAwareScrollView` when keyboard gets closed
(and scroll position is the end of the scroll view).

## 💡 Motivation and Context

The fix consist of several stages. Below they are:

### 1️⃣ Incorrect `persistentHeight` update

When we reset `persistentHeight` in `onEnd` we:
- produce potential race condition (`persistentHeight` can be set to `0`
after timing animation completion and because of that `onEnd` handler
may not be fired at all)
- calculate `progress` incorrectly (it can be `Infinity` or `NaN` in
last frames of animation

### 2️⃣ Avoid double `onMove`/`onEnd` events

If we reset `persistentHeight` to `0` in `useAnimatedReaction` (where we
dispatch `onEnd`) -> we run animation effect again, and it may produce
additional `onMove`/`onEnd` events after `onEnd` event. So I added `if
(persistentHeight.value === 0)` check.

### 3️⃣ Don't re-calculate layout in every animation frame

When we scroll to the end of ScrollView and change padding, it seems
like we do a heavy layout re-calculation. That's why I added `+1`
padding.

Closes
#335

## 📢 Changelog

### JS

-  fixed `useSmoothKeyboardHandler` for Android < 10;

## 🤔 How Has This Been Tested?

Tested manually on Xiaomi Redmi Note 5 Pro. Also I use 2500ms instead of
250ms timing to see more detailed difference.

## 📸 Screenshots (if appropriate):

|Original performance|With corrected `useSmoothKeyboardHandler`|With
`+1` fix|

|---------------------|----------------------------------------------|----------------|
|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/e7f95f23-ed88-4d8c-80cb-1fca06d4697f">|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/fe9cc67f-61e8-40f2-8d17-b3fce4bd1c10">|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/1bb7f2b6-726b-47f6-b71e-c96c2086344f">

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
  • Loading branch information
kirillzyusko committed Jan 29, 2024
1 parent 6d3a9d0 commit 2c93b82
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 8 deletions.
8 changes: 7 additions & 1 deletion src/components/KeyboardAwareScrollView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,13 @@ const KeyboardAwareScrollView: FC<KeyboardAwareScrollViewProps> = ({

const view = useAnimatedStyle(
() => ({
paddingBottom: currentKeyboardFrameHeight.value,
// animations become laggy when scrolling to the end of the `ScrollView` (when the last input is focused)
// this happens because the layout recalculates on every frame. To avoid this we slightly increase padding
// by `+1`. In this way we assure, that `scrollTo` will never scroll to the end, because it uses interpolation
// from 0 to `keyboardHeight`, and here our padding is `keyboardHeight + 1`. It allows us not to re-run layout
// re-calculation on every animation frame and it helps to achieve smooth animation.
// see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
paddingBottom: currentKeyboardFrameHeight.value + 1,
}),
[],
);
Expand Down
22 changes: 15 additions & 7 deletions src/components/KeyboardAwareScrollView/useSmoothKeyboardHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const useSmoothKeyboardHandler: typeof useKeyboardHandler = (
deps,
) => {
const target = useSharedValue(-1);
const height = useSharedValue(0);
const persistedHeight = useSharedValue(0);
const animatedKeyboardHeight = useSharedValue(0);

Expand All @@ -43,10 +44,12 @@ export const useSmoothKeyboardHandler: typeof useKeyboardHandler = (
if (IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS) {
return;
}
if (persistedHeight.value === 0) {
return;
}
const event = {
// it'll be always 250, since we're running animation via `withTiming` where
// duration in config (TELEGRAM_ANDROID_TIMING_CONFIG.duration) = 250ms
duration: 250,
// it'll be always `TELEGRAM_ANDROID_TIMING_CONFIG.duration`, since we're running animation via `withTiming`
duration: TELEGRAM_ANDROID_TIMING_CONFIG.duration,
target: target.value,
height: animatedKeyboardHeight.value,
progress: animatedKeyboardHeight.value / persistedHeight.value,
Expand All @@ -60,8 +63,9 @@ export const useSmoothKeyboardHandler: typeof useKeyboardHandler = (
handler.onMove?.(evt);

// dispatch `onEnd`
if (evt.height === persistedHeight.value) {
if (evt.height === height.value) {
handler.onEnd?.(evt);
persistedHeight.value = height.value;
}
},
[handler],
Expand All @@ -85,6 +89,7 @@ export const useSmoothKeyboardHandler: typeof useKeyboardHandler = (
}

target.value = e.target;
height.value = e.height;

if (e.height > 0) {
persistedHeight.value = e.height;
Expand All @@ -99,7 +104,12 @@ export const useSmoothKeyboardHandler: typeof useKeyboardHandler = (
);
}

handler.onStart?.(e);
handler.onStart?.({
...e,
duration: IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS
? e.duration
: TELEGRAM_ANDROID_TIMING_CONFIG.duration,
});
},
onMove: (e) => {
"worklet";
Expand All @@ -114,8 +124,6 @@ export const useSmoothKeyboardHandler: typeof useKeyboardHandler = (
if (IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS) {
handler.onEnd?.(e);
}

persistedHeight.value = e.height;
},
},
deps,
Expand Down

0 comments on commit 2c93b82

Please sign in to comment.