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

Add KeyboardAvoidingView #4339

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from

Conversation

terrysahaidak
Copy link
Contributor

Summary

This PR implements React Native KeyboardAvoidingView using useAnimatedKeyboard hook.

The goal of this implementation is to be always perfectly in sync with useAnimatedKeyboard so we can animate things based on the keyboard state in the same frame.
Another benefit of this implementation is that it doesn't rely on Layout Animation which sometimes makes things really hard.

Using Reanimated's KeyboardAvoidingView
2023-04-06.16.40.02.mov
Using React Native's KeyboardAvoidingView
2023-04-06.16.41.43.mov

Another benefit of this implementation is that we can actually introduce more properties and mechanics that are not as confusing as the current behavior and keyboardVerticalOffset props no one knows how to properly pair.

As part of this implementation, I'm adding an example I'm implementing right now for Expensify (Expensify/App#16356) but you can see it in other apps such as Slack and Messenger.
In chat applications, KeyboardAvoidingView usually pushes the scroll view to the top alongside input. But if you want to show a modal instead of the keyboard, for example, simulating InputAccessoryView, the keyboard hides without animation, and on the next frame the modal is rendered. So KeyboardAvoidingView no longer pushes the content since the keyboard is gone by now and all the content is now covered by our modal.
What we can do in this particular case is to translateY the content of the scroll view by the last keyboard height when the modal is visible.

2023-04-06.15.31.41.mov

There is a bug left with useAnimatedKeyboard which results into a few weird frames in this example, since it reports at some point:

{
  state: 4, // closed
  height: 318
}

Test plan

Run KeyboardAvoidingView example.

@terrysahaidak
Copy link
Contributor Author

This is the flicker we have right now. It returns height > 0 when the state is closed, this code can help to fix the bug - i left console.log statements for that.

image

@terrysahaidak
Copy link
Contributor Author

I created this issue for tracking the bug I mentioned. I'll continue working on this PR after it's fixed
#4351

cc @tomekzaw

@efstathiosntonas
Copy link
Contributor

@terrysahaidak it seems this PR fixes the height issue, just linking it here for reference


<Modal
style={styles.sheetContainer}
visible={isModalVisible}
Copy link
Member

@tomekzaw tomekzaw Apr 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @terrysahaidak, I've tested your example with the PR #4360.

If you look closely, there's still some flickering when opening the modal and it happens both with Reanimated's and React Native's KeyboardAvoidingView:

KeyboardAvoidingView from Reanimated
reanimated.mp4
KeyboardAvoidingView from React Native
react.native.mp4

I believe the root cause of this flicker is not the implementation of useAnimatedKeyboard hook but rather the fact that the visibility of <Modal> component is controlled using React state whereas the other part of the logic uses a shared value. In particular, there are two sources of truth and it takes 1-2 frames to render the changes from the JS thread when the React state is updated:

const [isModalVisible, setIsModalVisible] = useState(false);
const modalVisible = useSharedValue(false);

Instead, you should keep the state only in shared value and control the visibility of the modal via animated props on the UI thread:

import Animated, { useAnimatedProps } from 'react-native-reanimated';

const AnimatedModal = Animated.createAnimatedComponent(Modal);

const animatedProps = useAnimatedProps(() => {
  return { visible: isModalVisible.value };
});

return <AnimatedModal {/* props */} animatedProps={animatedProps} />;

Edit: looks like visible prop is available only on iOS, on Android it's implemented on JS side

kmagiera added a commit that referenced this pull request Apr 18, 2023
## Summary

This PR refactors the logic in REAKeyboardEventObserver to address some
number of different issues:
1) no change notification when keyboard size changes due to change in
language or showing suggestions bar
2) issue with keyboard height notification when opening modal (see
#4339)
3) issue with non-zero height after keyboard hides completely.

Here is the list of the main thing that this refactor changes:
1) We no longer use willShow/willHide/didShow/didHide notifications for
keyboard and only use willChangeFrame which is the most reliable one and
triggers also when the keyboard updates
2) We no longer use a technique where the keyboard size is determined
based on the frame of the keyboard view. This was problematic as that
frame would sometimes reflect a one-frame delayed size which resulted in
the animation not coming down to 0 or not updating at all when
immediately hidden or appear (by a modal). Instead, we use UIView
animation to animate a complementary view that is placed outside of the
screen visible area. We use UIView animation to animate height of that
invisible view and then use display link to access presentation layer in
order to read up-to-date height.

There are few gatchas related to the new technique used:
1) Because we only look at frame change notification we need to properly
determine the state of the keyboard. We do it based on the target
height. If it is 0 then we consider the state to be either closing or
closed – we select the final state based on whether there is animation
running (closed state when no animation, or closing when we are
animating). This prevents us from changing state to closing when
keyboard is only becoming shorter (e.g. when text suggestion bar is
closed). We handle opening/open state in similar way.
2) Due to the UIView animation technique we need to check
presentationLayer.animationKeys to determine if the layer runs the
animation. If it doesn't then we extract height from the view frame and
not from the presentationLayer frame, as the presentationLayer may not
have an up-to-date information (it will be delayed by one frame).

## Test plan

Use example from #4339 and animated keyboard screen from Example app.
Open/close text suggestion bar to see how the app reacts when keyboard
is changing height and not just opening/closing.
@tomekzaw
Copy link
Member

Hey @terrysahaidak, since the PR with useAnimatedKeyboard improvements (#4360) just got merged, we'd like to proceed to your PR. Can you please merge main and fix linter errors so that we can review and test your changes?

@terrysahaidak
Copy link
Contributor Author

hey @tomekzaw! i'm gonna be working on this tomorrow, i'll let you know when it's ready.

@efstathiosntonas
Copy link
Contributor

@terrysahaidak did you managed to work on this? Sorry for poking you, I'm eagerly waiting for this to come out since users are experiencing weird issues with KeyboardAvoidingView.

@terrysahaidak
Copy link
Contributor Author

@efstathiosntonas yes, actually rebasing it right now and debugging stuff.

// we use `enabled === true` to be 100% compatible with original implementation
const bottomHeight = enabled === true ? bottom : 0;

console.log(
Copy link
Contributor

@efstathiosntonas efstathiosntonas Apr 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@terrysahaidak this console.log should be removed, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, these logs are there for debugging, they will be removed once it's actually ready for review, right now this PR is still in draft.

Copy link
Contributor

@efstathiosntonas efstathiosntonas Apr 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@terrysahaidak just noticed the draft badge on the PR :face_palm:

);

const getBackwardCompatibleHeight = useWorkletCallback(
(keyboardHeight: number, frame: null | LayoutRectangle) => {
Copy link
Contributor

@efstathiosntonas efstathiosntonas Apr 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@terrysahaidak frame is shadowed from L54 const [frame, setFrame] = useState<LayoutRectangle | null>(null);

rename it to kFrame? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tomekzaw I believe that the Lint / check action should include shadow variables rule

@peterlazar1993
Copy link

@terrysahaidak useAnimatedKeyboard has a potentially breaking bug.
Maybe you can get some eyeballs on this issue as well

@terrysahaidak
Copy link
Contributor Author

There is still a flicker after merging #4360 : #4351 (comment)

@terrysahaidak
Copy link
Contributor Author

Finally fixed the initial render bug, so the KeyboardAvoidingView is working fine on both iOS and Android, going to fix the example

fluiddot pushed a commit to wordpress-mobile/react-native-reanimated that referenced this pull request Jun 5, 2023
## Summary

This PR refactors the logic in REAKeyboardEventObserver to address some
number of different issues:
1) no change notification when keyboard size changes due to change in
language or showing suggestions bar
2) issue with keyboard height notification when opening modal (see
software-mansion#4339)
3) issue with non-zero height after keyboard hides completely.

Here is the list of the main thing that this refactor changes:
1) We no longer use willShow/willHide/didShow/didHide notifications for
keyboard and only use willChangeFrame which is the most reliable one and
triggers also when the keyboard updates
2) We no longer use a technique where the keyboard size is determined
based on the frame of the keyboard view. This was problematic as that
frame would sometimes reflect a one-frame delayed size which resulted in
the animation not coming down to 0 or not updating at all when
immediately hidden or appear (by a modal). Instead, we use UIView
animation to animate a complementary view that is placed outside of the
screen visible area. We use UIView animation to animate height of that
invisible view and then use display link to access presentation layer in
order to read up-to-date height.

There are few gatchas related to the new technique used:
1) Because we only look at frame change notification we need to properly
determine the state of the keyboard. We do it based on the target
height. If it is 0 then we consider the state to be either closing or
closed – we select the final state based on whether there is animation
running (closed state when no animation, or closing when we are
animating). This prevents us from changing state to closing when
keyboard is only becoming shorter (e.g. when text suggestion bar is
closed). We handle opening/open state in similar way.
2) Due to the UIView animation technique we need to check
presentationLayer.animationKeys to determine if the layer runs the
animation. If it doesn't then we extract height from the view frame and
not from the presentationLayer frame, as the presentationLayer may not
have an up-to-date information (it will be delayed by one frame).

## Test plan

Use example from software-mansion#4339 and animated keyboard screen from Example app.
Open/close text suggestion bar to see how the app reacts when keyboard
is changing height and not just opening/closing.
@Latropos Latropos self-assigned this Aug 30, 2023
@Latropos
Copy link
Contributor

Latropos commented Sep 4, 2023

There are still some bugs, for example the behaviour of animated and native KeyboardAvoidingView may differ a lot.

Screen.Recording.2023-09-04.at.16.09.40.mov

This is a very important change so expect several iterations of code reviews etc. and lot of testing. Therefore we are going to copy this code and fix it on our end. Thank you very much for your amazing work! #5064

@roryabraham
Copy link

@Latropos any update on this one?

@tomekzaw
Copy link
Member

Hey @roryabraham, I've asked @szydlovsky to review the PR and compare it with existing implementations, will keep you updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants