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

KeyboardAwareScrollView scroll to the top on android with multiline TextInput #708

Closed
NyoriK opened this issue Nov 27, 2024 · 21 comments · Fixed by #744
Closed

KeyboardAwareScrollView scroll to the top on android with multiline TextInput #708

NyoriK opened this issue Nov 27, 2024 · 21 comments · Fixed by #744
Assignees
Labels
🤖 android Android specific 🐛 bug Something isn't working KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component repro provided Issue contains reproduction repository/code

Comments

@NyoriK
Copy link

NyoriK commented Nov 27, 2024

Describe the bug
I am using KeyboardAwareScrollView for my form, and all the TextInputs have multiline enabled.

When the caret position reach near the keyboard and at the caret position at the end of the line, by typing a character, it scrolls to the top, and again after typing the next character, it scrolls back to the current caret position view.

This happens even on the first TextInput if the caret position reaches near the keyboard.

Code snippet

Setting up the project.

  1. Install expo project with blank typescript template.
  2. Install reanimated
  3. Install react-native-keyboard-controller
  4. Install expo router

project/app/_layout.tsx:

import { Stack } from 'expo-router';
import { KeyboardProvider } from 'react-native-keyboard-controller';
import TestForm from '../components/TestForm';

const RootLayout = () => {
    return (
        <KeyboardProvider
            statusBarTranslucent
            navigationBarTranslucent
        >
            <Stack>
                <Stack.Screen name='index' />
            </Stack>
        </KeyboardProvider>
    )
}

export default RootLayout

project/app/index.tsx:

import { View, StyleSheet } from 'react-native';
import TestForm from '../components/TestForm';

export default function Page() {
    return (
        <View style={styles.container}>
            <TestForm />
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: 'yellow',
    },
});

project/components/TestForm.tsx:

import { useState } from 'react'
import { StyleSheet, Text, View, TextInput } from 'react-native'
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'

const TestForm = () => {
    const [input1, setInput1] = useState('')
    const [input2, setInput2] = useState('')
    const [input3, setInput3] = useState('')
    const [input4, setInput4] = useState('Lorem ipsum dolor sit amet. Et aspernatur quibusdam 33 ducimus veniam vel cupiditate enim ut repudiandae dolor. Ut minima molestias eum impedit consequuntur qui odio ullam qui possimus similique qui tenetur voluptas eos assumenda eveniet aut odit omnis. Et dicta distinctio qui autem consequatur sit iste temporibus. Vel molestiae eligendi qui vitae eligendi in totam doloribus vel Quis dolorem ad consequuntur vitae est recusandae molestiae nam voluptatibus distinctio.')
    const [input5, setInput5] = useState('')

    return (
        <KeyboardAwareScrollView
            style={styles.container}
            keyboardDismissMode='on-drag'
        >
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 1</Text>
                <TextInput
                    style={styles.input}
                    value={input1}
                    onChangeText={setInput1}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 2</Text>
                <TextInput
                    style={styles.input}
                    value={input2}
                    onChangeText={setInput2}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 3</Text>
                <TextInput
                    style={styles.input}
                    value={input3}
                    onChangeText={setInput3}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 4</Text>
                <TextInput
                    style={styles.input}
                    value={input4}
                    onChangeText={setInput4}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 5</Text>
                <TextInput
                    style={styles.input}
                    value={input5}
                    onChangeText={setInput5}
                    placeholder='Enter input'
                    multiline
                />
            </View>
        </KeyboardAwareScrollView>
    )
}
export default TestForm

const styles = StyleSheet.create({
    container: {
        backgroundColor: 'lightgreen',
    },
    wrapper: {
        backgroundColor: 'lightgray',
        // padding: 20,
    },
    label: {
        fontSize: 30,
        fontWeight: 600,
        color: 'gray'
    },
    input: {
        fontSize: 40
    }
})

To Reproduce
Steps to reproduce the behavior:

  1. Go to Input 5

  2. Write till the caret position reaches the end of line, and type a single character

  3. It will scroll up to the top

  4. Then type again,

  5. It will scroll down to the current caret position view.

  6. Go to input 1

  7. Write till the caret position reach near the keyboard, and and type a character

  8. It will scroll up to the top

  9. Then type again, and it will scroll down to the current caret position view.

Expected behavior
It should not scroll to the top.

Screenshots

VID_20241128_035422.mp4
  • Desktop OS: MacOs Sonoma

  • Device: Redmi Note 10 Pro and

  • Emulator: Pixel 8

  • react: 18.3.1

  • react-native: 0.76.3

  • expo: ~52.0.11

  • react-native-reanimated: ~3.16.1

  • expo-router: ~4.0.9

  • react-native-keyboard-controller: ^1.14.5

@kirillzyusko kirillzyusko added 🐛 bug Something isn't working 🤖 android Android specific KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component labels Nov 27, 2024
@kirillzyusko
Copy link
Owner

Hi @NyoriK

Thank you for the issue - I'm currently busy at my work, but I'll look into this problem. Thanks for sharing a minimal reproducible demo ❤️

@Jul1enF
Copy link

Jul1enF commented Nov 28, 2024

@NyoriK Have you tried to use a bottomOffset for the KeyboardAwareScrollView equal or superior to the height of a line to see if it changes that behaviour ?

Try with a large one to start just to see if it fixes it.

@NyoriK
Copy link
Author

NyoriK commented Nov 28, 2024

use a bottomOffset for the KeyboardAwareScrollView

@Jul1enF Does not fix it, still has the issue.

@kirillzyusko Thank you.

@kirillzyusko kirillzyusko added the repro provided Issue contains reproduction repository/code label Dec 2, 2024
@kirillzyusko
Copy link
Owner

Hey @NyoriK

I've tried to reproduce the problem on Pixel 8 Pro (API 35, emulator) and Pixel 7 Pro (API 35, real device) and couldn't reproduce it:

aware-scroll-view-works-as-expected.mov

Any ideas what is the difference between our setups? 🤔 I'm testing the package from main version, but I don't think I merged any fixes that would fix this problem 🤔

@NyoriK
Copy link
Author

NyoriK commented Dec 4, 2024

Hi @kirillzyusko

After your comment, I tried again (just now), and this time with default create expo app template. But I still have the same issue.

Below are the steps:

1. npx create-expo-app@latest keyboard-controller-test-project

Creating an Expo project using the default template.

2. cd keyboard-controller-test-project

3. npx expo install react-native-keyboard-controller

4. npm run reset-project

5. npx expo run:android --device

✔ Created native directory
✔ Updated package.json
✔ Finished prebuild
✔ Select a device/emulator › 🔌 M2101K6P (device)
› Using --device M2101K6P
› Building app...

6. Add project/components/TestForm.tsx:

import { useState } from 'react'
import { StyleSheet, Text, View, TextInput } from 'react-native'
import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'

const TestForm = () => {
    const [input1, setInput1] = useState('')
    const [input2, setInput2] = useState('')
    const [input3, setInput3] = useState('')
    const [input4, setInput4] = useState('Lorem ipsum dolor sit amet. Et aspernatur quibusdam 33 ducimus veniam vel cupiditate enim ut repudiandae dolor. Ut minima molestias eum impedit consequuntur qui odio ullam qui possimus similique qui tenetur voluptas eos assumenda eveniet aut odit omnis. Et dicta distinctio qui autem consequatur sit iste temporibus. Vel molestiae eligendi qui vitae eligendi in totam doloribus vel Quis dolorem ad consequuntur vitae est recusandae molestiae nam voluptatibus distinctio.')
    const [input5, setInput5] = useState('')

    return (
        <KeyboardAwareScrollView
            style={styles.container}
            keyboardDismissMode='on-drag'
        >
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 1</Text>
                <TextInput
                    style={styles.input}
                    value={input1}
                    onChangeText={setInput1}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 2</Text>
                <TextInput
                    style={styles.input}
                    value={input2}
                    onChangeText={setInput2}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 3</Text>
                <TextInput
                    style={styles.input}
                    value={input3}
                    onChangeText={setInput3}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 4</Text>
                <TextInput
                    style={styles.input}
                    value={input4}
                    onChangeText={setInput4}
                    placeholder='Enter input'
                    multiline
                />
            </View>
            <View style={styles.wrapper}>
                <Text style={styles.label}>Input 5</Text>
                <TextInput
                    style={styles.input}
                    value={input5}
                    onChangeText={setInput5}
                    placeholder='Enter input'
                    multiline
                />
            </View>
        </KeyboardAwareScrollView>
    )
}

export default TestForm

const styles = StyleSheet.create({
    container: {
        backgroundColor: 'lightgreen',
    },
    wrapper: {
        backgroundColor: 'lightgray',
        // padding: 20,
    },
    label: {
        fontSize: 30,
        fontWeight: 600,
        color: 'gray'
    },
    input: {
        fontSize: 40
    }
})

7. Update project/app/index.tsx:

import TestForm from '@/components/TestForm';
import { View, StyleSheet } from 'react-native';

export default function Page() {
  return (
    <View style={styles.container}>
      <TestForm />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: 'yellow',
  },
});

8. Update project/app/_layout.tsx:

import { Stack } from 'expo-router';
import { KeyboardProvider } from 'react-native-keyboard-controller';
const RootLayout = () => {
  return (
    <KeyboardProvider
      statusBarTranslucent
      navigationBarTranslucent
    >
      <Stack>
        <Stack.Screen name='index' />
      </Stack>
    </KeyboardProvider>
  )
}

export default RootLayout

9. npx expo start -c (just to be on the safe side)

Android Xiaomi Redmi Note 10 Pro (device):

android-device-test.mov

Android Pixel_8_API_35 (emulator):

android-emulator-test.mov

project/package.json:

{
  "name": "keyboard-controller-test-project",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "test": "jest --watchAll",
    "lint": "expo lint"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.0.2",
    "@react-navigation/bottom-tabs": "^7.0.0",
    "@react-navigation/native": "^7.0.0",
    "expo": "~52.0.11",
    "expo-blur": "~14.0.1",
    "expo-constants": "~17.0.3",
    "expo-font": "~13.0.1",
    "expo-haptics": "~14.0.0",
    "expo-linking": "~7.0.3",
    "expo-router": "~4.0.9",
    "expo-splash-screen": "~0.29.13",
    "expo-status-bar": "~2.0.0",
    "expo-symbols": "~0.2.0",
    "expo-system-ui": "~4.0.4",
    "expo-web-browser": "~14.0.1",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "react-native": "0.76.3",
    "react-native-gesture-handler": "~2.20.2",
    "react-native-keyboard-controller": "^1.14.5",
    "react-native-reanimated": "~3.16.1",
    "react-native-safe-area-context": "4.12.0",
    "react-native-screens": "~4.1.0",
    "react-native-web": "~0.19.13",
    "react-native-webview": "13.12.2"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/jest": "^29.5.12",
    "@types/react": "~18.3.12",
    "@types/react-test-renderer": "^18.3.0",
    "jest": "^29.2.1",
    "jest-expo": "~52.0.2",
    "react-test-renderer": "18.3.1",
    "typescript": "^5.3.3"
  },
  "private": true
}

@kirillzyusko
Copy link
Owner

@NyoriK oki-doki, may I ask you to upload your reproduction project to the github so that I can clone it and run it locally?

@NyoriK
Copy link
Author

NyoriK commented Dec 5, 2024

@kirillzyusko
Copy link
Owner

Thanks @NyoriK

With 50/50 chances I can actually reproduce a problem. After a quick look it seems like handlers can not be attached 🤔 Trying to figuring out what's going wrong!

@kirillzyusko
Copy link
Owner

Okay @NyoriK I think it's because of react-native-screens@4 - can you try to downgrade to 3.35 and check on your end? Will it be fixed or not?

In a meantime I'll try to fix the issue in react-native-screens 🤞

@NyoriK
Copy link
Author

NyoriK commented Dec 7, 2024

@kirillzyusko Thanks for the suggestion to try react-native-screens@3.35.0. I attempted to downgrade but ran into dependency conflicts because my project uses:

  • expo-router v4, which requires @react-navigation/bottom-tabs v7
  • @react-navigation/bottom-tabs v7 requires react-native-screens v4+

When I tried to install react-native-screens@3.35.0 and @react-navigation/native@6.x.x, npm showed dependency resolution errors due to these requirements. Downgrading would require:

  1. Downgrading expo-router and the entire navigation stack
  2. Downgrading the Expo SDK itself since newer versions are built to work with these newer navigation packages

This would be a major breaking change for the project. Could we explore a fix that works with react-native-screens v4? Or perhaps there are other workarounds we could try?

@NyoriK
Copy link
Author

NyoriK commented Dec 9, 2024

hi @kirillzyusko

Update: I did some additional testing with an older setup:

  1. Created new project with npx create-expo-app@latest older-gen-app -t expo-template-default@sdk-51, installed react-native-keyboard-controller, and replaced app/ and components/ with the test project files.

Here's the package.json for reference:

{
  "name": "older-gen-app",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "test": "jest --watchAll",
    "lint": "expo lint"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.0.2",
    "@react-navigation/native": "^6.0.2",
    "expo": "~51.0.28",
    "expo-constants": "~16.0.2",
    "expo-font": "~12.0.9",
    "expo-linking": "~6.3.1",
    "expo-router": "~3.5.23",
    "expo-splash-screen": "~0.27.5",
    "expo-status-bar": "~1.12.1",
    "expo-system-ui": "~3.0.7",
    "expo-web-browser": "~13.0.3",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.74.5",
    "react-native-gesture-handler": "~2.16.1",
    "react-native-keyboard-controller": "^1.14.5",
    "react-native-reanimated": "~3.10.1",
    "react-native-safe-area-context": "4.10.5",
    "react-native-screens": "3.31.1",
    "react-native-web": "~0.19.10"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@types/jest": "^29.5.12",
    "@types/react": "~18.2.45",
    "@types/react-test-renderer": "^18.0.7",
    "jest": "^29.2.1",
    "jest-expo": "~51.0.3",
    "react-test-renderer": "18.2.0",
    "typescript": "~5.3.3"
  },
  "private": true
}
  1. Interesting findings:
  • Initially Input 5 is out of view, but while typing in Input 4, Input 5 becomes visible. I tested this behavior with a regular ScrollView as well and got the same result, so it seems to be general ScrollView behavior rather than specific to the keyboard controller.

  • On Android with this older setup:

    • The original scrolling issue doesn't occur like before
android-good-expo-older-version.mov
  • However, when typing far above the visible area, I noticed the scroll and caret positions can sometimes jump. This might be the expected result, I am not sure.
android-bad-expo-old.mov
  • On iOS when scrollEnabled={false}, I discovered similar behavior to our original Android issue - when typing far above the visible area and near the end of a line of the TextInput, it briefly scrolls to the top before returning to the caret position.
ios-scrollenabled-false-bad-expo-old.mp4

@kirillzyusko
Copy link
Owner

However, when typing far above the visible area, I noticed the scroll and caret positions can sometimes jump. This might be the expected result, I am not sure.

At the moment fields that occupy entire screen may not work correctly, because current KeyboardAwareScrollView relies on layout changes (it doesn't track caret position) and thus when layout overlap with keyboard and status bar we don't know exact direction to scroll to, so you may observe such bugs.

I'm going to re-implement this component and rely on caret position to be sure that actually caret is in visible area and not entire field.

On iOS when scrollEnabled={false}, I discovered similar behavior to our original Android issue - when typing far above the visible area and near the end of a line of the TextInput, it briefly scrolls to the top before returning to the caret position.

Did you just specify scrollEnabled={false} for a ScrollView? You don't nest your ScrollView into FlatList or any otehr scrollable component? In this case when you disable scroll for KeyboardAwareScrollView you disable the functionality of this component and I think you fallback to default iOS behavior. See #566 for more details.

Or perhaps there are other workarounds we could try?

When I mounted KeyboardProvider asynchronously (after 16ms) the issue disappear, but I'm not sure it will always work - I haven't tested it extensively.

Let me know if you have any further questions 😊

@NyoriK
Copy link
Author

NyoriK commented Dec 9, 2024

@kirillzyusko

Did you just specify scrollEnabled={false} for a ScrollView? You don't nest your ScrollView into FlatList or any otehr scrollable component?

About the iOS behavior - the scrolling issue occurs with scrollEnabled={false} on just a regular KeyboardAwareScrollView, not nested in any other scrollable component. To test without ScrollView, I only replaced KeyboardAwareScrollView with react native's ScrollView, and added the prop scrollEnabled={false}. By the way thanks for pointing me to #566, I'll check that out.

When I mounted KeyboardProvider asynchronously (after 16ms) the issue disappear, but I'm not sure it will always work - I haven't tested it extensively.

About the async KeyboardProvider mounting - that's an interesting workaround. Would you be willing to share more details about how you tested this? I'd be happy to try it out and provide feedback on different scenarios.

@kirillzyusko
Copy link
Owner

About the async KeyboardProvider mounting - that's an interesting workaround. Would you be willing to share more details about how you tested this? I'd be happy to try it out and provide feedback on different scenarios.

That was a quite primitive code, something like:

const [shouldMountKeyboardProvider, setMountKeyboardProvider] = useState(false);

useEffect(() => {
  setTimeout(() => {
    setMountKeyboardProvider(true);
  }, 16);
}, []);

return shouldMountKeyboardProvider ? <KeyboardProvider>...</KeyboardProvider> : null;

Feel free to experiment with that 🙌 Maybe it helped only in my case 😅

Alternatively you can try to remove setOnApplyWindowInsetsListener calls inside react-native-screens, but in this case there is very high chances that functionality of react-native-screens can be broken. We are discussing a better/real solution in software-mansion/react-native-screens#2554

@NyoriK
Copy link
Author

NyoriK commented Dec 11, 2024

@kirillzyusko Ahaa! Thanks for sharing the async mounting code! I tried it:

const RootLayout = () => {
  const [mountKeyboardProvider, setMountKeyboardProvider] = useState(false)

  useEffect(() => {
    setTimeout(() => {
      setMountKeyboardProvider(true);
    }, 16);
  }, []);

  return mountKeyboardProvider ? (
    <KeyboardProvider
      statusBarTranslucent
      navigationBarTranslucent
    >
      <Stack>
        <Stack.Screen name='index' />
      </Stack>
    </KeyboardProvider>
  ) : null
}

It does improve the behavior a lot - however if observing closely, I can still see the scroll does go to the top and comes back like before, but it's so fast that it's not immediately noticeable. It manifests as a quick stutter when scrolling down to the next line rather than the obvious scroll-to-top behavior we saw previously.
I'll keep an eye on the react-native-screens discussion (software-mansion/react-native-screens#2554) for updates on the more permanent solution. Thanks for your help with this!

@kirillzyusko
Copy link
Owner

kirillzyusko commented Dec 23, 2024

@NyoriK can you check if #744 fixes the problem in your case? With this fix in place I can not reproduce the problem in your reproduction example anymore 😊

@NyoriK
Copy link
Author

NyoriK commented Dec 24, 2024

@kirillzyusko I've tested PR #744 on both iOS and Android. Here are my findings:

  1. iOS: Works perfectly smooth - no scrolling/jerk issues observed when typing goes to the next line

  2. Android:

  • Normal typing: There's still a slight stutter/jerk but it's barely noticeable.
keyboard-controller-pr-744.mp4
  • However, When typing in a specific pattern (one character → enter → one character → enter), the scroll stuttering becomes more pronounced as it tries to maintain the cursor position above the keyboard
keyboard-controller-pr-744-jerk.mp4
  • Unlike iOS where it scrolls very smoothly to the next line, on Android it's a bit odd

The fix definitely improves the original behavior - the extreme scroll-to-top issue is gone. The remaining stutters are much less noticeable than the original problem, though they become more apparent with certain typing patterns.

@kirillzyusko
Copy link
Owner

Well, it's kind of explainable. I'll start with input 5.

It seems like input 5 grows with a delay. When you move a caret to a new line:

  • it simply scroll multiline input (input actually doesn't grow)
  • you type a new digit
  • the digit becomes visible
  • text input grows (newly typed letter gets hidden/obscured by keyboard)
  • KeyboardAwareScrollView receives an update, that input has changed layout and calculates how much pixels it should grow
First frame You type enter You type next digit Input grew AwareScrollView performs scroll
image image image image image

In paper example project the input grows as soon as you pressed "Enter" (can't test Fabric because suddenly input doesn't grow there at all - need to look into it):

telegram-cloud-document-2-5426846321304701558.mp4

The code example that I used can be found at: https://github.com/kirillzyusko/react-native-keyboard-controller/blob/main/example/src/screens/Examples/AwareScrollView/index.tsx

I think if you manage your input to grow immediately when you press "Enter" it will work fine (in the same way as on iOS, because I'm more than sure, that iOS stretches input immediately without a need to type a new character). Maybe it's Fabric specific bug 🤷‍♂️

Normal typing: There's still a slight stutter/jerk but it's barely noticeable.

I'll try to investigate it today 👍

@kirillzyusko
Copy link
Owner

I'll try to investigate it today 👍

I've investigated and it seems like it's the same problem with internal scroll + layout update, look at this:

First frame Second Frame Third frame Fourth frame
image image image image

Step-by-step breakdown:

  • on first frame I'm about to type a new letter
  • on second frame the letter typed - text input doesn't grow - instead it simply scrolls its own internal scroll 🤷
  • third frame - TextInput actually grows
  • fourth frame - AwareScrollView detects the grow and scrolls input into a visible area

I don't know why it happens (i. e. asynchronous grow). In my paper example I don't see such behavior.

What I'm suggesting to do - I'll merge my PR with a fix to scroll-to-top problem. It'll automatically close that issue. If you want you can create a new issue and we'll continue to discuss why the layout gets updated asynchronously (maybe you didn't specify some props on JS side). I don't want to mix different problems in a single issue as it makes the history of PRs more complicated and I prefer "one PR one issue" approach 😊

Keen to help and curious to know why this layout update happens asynchronously 👀

kirillzyusko added a commit that referenced this issue Dec 24, 2024
## 📜 Description

Execute monkey-patch applying earlier than `StatusBar` modifications.

## 💡 Motivation and Context

It happens when `KeyboardProvider` had direct child as `StatusBar` - and
such thing can happen in `expo-router`, for example:
https://github.com/expo/expo/blob/5f40a80019bb6b892eda94dd244fdc0df8880ccb/packages/expo-router/src/ExpoRoot.tsx#L57

To prevent this problem I'm executing monkey patch applying in "layout
effect" instead of plain "effect". This gives me a precious time and an
ability to apply patch earlier and thus re-direct a call to my module.

And if we dig a little bit deeper. When `KeyboardProvider` gets mounted
the `useLayoutEffect` will be fired after component mount. On contrast
`StatusBar` will try to change its properties in `componentDidMount`.
And technically `componentDidMount` will be executed first (before
`useLayoutEffect`). But `StatusBar` schedules update via `setImmediate`
and `setImmediate` will execute its callback after `useLayoutEffect`, so
this fix should work 🙂

Closes
#708
#587

## 📢 Changelog

### Android

- apply monkey patch in layout effect to be sure monkey patch can be
applied earlier than first call to `StatusBar` module (if
`KeyboardProvider` and `StatusBar` were mounted simultaneously).

## 🤔 How Has This Been Tested?

Tested manually in
https://github.com/NyoriK/keyboard-controller-test-project

## 📸 Screenshots (if appropriate):


https://github.com/user-attachments/assets/6e259ffe-de59-46a6-b9a6-26de05698b06

## 📝 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
@NyoriK
Copy link
Author

NyoriK commented Dec 25, 2024

@kirillzyusko Thanks for the detailed investigation! Your analysis of the asynchronous layout update behavior matches exactly what I'm seeing:

I agree with your suggestion to:

  1. Close this issue with the scroll-to-top fix
  2. Open a new issue to track the async layout update behavior

Would you like me to create the new issue focusing specifically on the async layout updates and stuttering? I can include the same details and videos you shared, focusing on this specific behavior.

@kirillzyusko
Copy link
Owner

Would you like me to create the new issue focusing specifically on the async layout updates and stuttering? I can include the same details and videos you shared, focusing on this specific behavior.

Yes, please do 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🤖 android Android specific 🐛 bug Something isn't working KeyboardAwareScrollView 📜 Anything related to KeyboardAwareScrollView component repro provided Issue contains reproduction repository/code
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants