kirillzyusko / react-native-keyboard-controller

Keyboard manager which works in identical way on both iOS and Android
https://kirillzyusko.github.io/react-native-keyboard-controller/
MIT License
1.54k stars 61 forks source link

feat: not animated keyboard resize transitions #376

Closed kirillzyusko closed 5 months ago

kirillzyusko commented 5 months ago

📜 Description

Removed animated keyboard resize transitions on Android. Now it matches iOS and is also instant.

💡 Motivation and Context

Problem description

First of all I'd like to give a little bit context:

Initially for me it looked like a correct option. But devil is in details and this approach brought new issues:

1️⃣ Incorrect progress value when keyboard gets resized

The progress value should be only between 0 (closed) and 1 (opened). If we do an animated keyboard resize, then we need to change progress value accordingly. It would be strange to see progress as 1.3 if keyboard gets bigger, so we always keep it in range of [0, 1].

[!WARNING] The only one exception is when keyboard gets smaller, in this case for first frame we increased instantly progress to >1 value (let's say 1.2) and then changed a progress to 1 (finish of animation), thus when keyboard is not animated it'll always have a progress as 0 or 1.

In https://github.com/kirillzyusko/react-native-keyboard-controller/pull/316 I fixed the problem by introducing additional hook, but solution wasn't perfect, because:

So animated transition for resize is really headache 🤯

2️⃣ Strange animation with KeyboardStickyView + switching between different TextInput fields with different input types (i. e. text to numeric)

The undesired animated resize effect comes into play when we have various simultaneous animations. For example using KeyboardToolbar when you press next button and keyboard changes it size from text to numeric (becomes smaller). If everything wrapped in KeyboardAwareScrollView, then we have several animations:

And it looks very strange. You can see it in https://github.com/kirillzyusko/react-native-keyboard-controller/issues/374

Solution

1️⃣ Overview

The solution is quite simple - react on keyboard resize instantly (i. e. it shouldn't be an animated transition). In this case we can assure that we match iOS behavior and we reduce API fragmentation across platforms.

So in this PR I reworked code and made resize transition instant one:

https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/9c311ed7-6196-4aed-957a-bd04e3c73ebf

If we take only latest value - we'll e always one frame behind. It's not perfect, but still better than race conditions:

https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/add753e7-42f9-41bb-a019-3659e07ecfca

That's the purpose behind animationsToSkip HashSet.

2️⃣ new onResize handler

Initially I though to add additional onResize handler, but after some research I thought that it could make API of library more complex without explicit benefits. So I decided to use existing methods. However it's a bit challenging, because by default on Android we listen only to onMove events. Theoretically we can add a listener and update a value on onEnd - I did it and on one of my projects it resulted to jumpy behavior.

So for sake of backward compatibility I'm dispatching 3 events on Android (onStart/onMove/onEnd) and two events on iOS (onStart/onEnd).

3️⃣ One frame lag

After implementation I discovered that there is a one frame dealy between actual keyboard size changes and the emitted event. After digging into native code I found that it suffers from the same problem:

1st frame 2nd frame 3rd frame 4th frame
image image image image

And backward transition:

1st frame 2nd frame 3rd frame
image image image

4️⃣ An ability to have an animated transitions

If for some reasons you still prefer to have an animated transition, then you still can achieve it using useKeyboardHandler hook:

const useKeyboardAnimation = () => {
  const height = useSharedValue(0);
  const shouldUpdateFromLifecycle = useSharedValue(true);

  useKeyboardHandler({
    onStart: (e) => {
      "worklet";

      // keyboard gets resized
      if (height.value !== 0 && e.height !== 0) {
        shouldUpdateFromLifecycle.value = false;
        height.value = withTiming(e.height, { duration: 250 }, () => {
          shouldUpdateFromLifecycle.value = true;
        });
      }
    },
    onMove: (e) => {
      "worklet";

      if (shouldUpdateFromLifecycle.value) {
        height.value = e.height;
      }
    },
  }, []);

  return { height };
}

Closes https://github.com/kirillzyusko/react-native-keyboard-controller/issues/374

📢 Changelog

JS

Android

🤔 How Has This Been Tested?

Tested manually on:

📸 Screenshots (if appropriate):

Keyboard toolbar

Before After

Keyboard Avoiding View

Before After

📝 Checklist

github-actions[bot] commented 5 months ago

📊 Package size report

Current size Target Size Difference
130329 bytes 134769 bytes -4440 bytes 📉