diff --git a/FabricExample/src/components/AwareScrollView/index.ts b/FabricExample/src/components/AwareScrollView/index.ts deleted file mode 100644 index 5903f0ef7..000000000 --- a/FabricExample/src/components/AwareScrollView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./KeyboardAwareScrollView"; diff --git a/FabricExample/src/components/AwareScrollView/useSmoothKeyboardHandler.ts b/FabricExample/src/components/AwareScrollView/useSmoothKeyboardHandler.ts deleted file mode 100644 index a30ce6e85..000000000 --- a/FabricExample/src/components/AwareScrollView/useSmoothKeyboardHandler.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Platform } from 'react-native'; -import { - Easing, - useAnimatedReaction, - useSharedValue, - withTiming, -} from 'react-native-reanimated'; -import { useKeyboardHandler } from 'react-native-keyboard-controller'; - -const IS_ANDROID_ELEVEN_OR_HIGHER = - Platform.OS === 'android' && Platform.Version >= 30; -// on these platforms keyboard transitions will be smooth -const IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS = - IS_ANDROID_ELEVEN_OR_HIGHER || Platform.OS === 'ios'; -// on Android Telegram is not using androidx.core values and uses custom interpolation -// duration is taken from here: https://github.com/DrKLO/Telegram/blob/e9a35cea54c06277c69d41b8e25d94b5d7ede065/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/AdjustPanLayoutHelper.java#L39 -// and bezier is taken from: https://github.com/DrKLO/Telegram/blob/e9a35cea54c06277c69d41b8e25d94b5d7ede065/TMessagesProj/src/main/java/androidx/recyclerview/widget/ChatListItemAnimator.java#L40 -const TELEGRAM_ANDROID_TIMING_CONFIG = { - duration: 250, - easing: Easing.bezier( - 0.19919472913616398, - 0.010644531250000006, - 0.27920937042459737, - 0.91025390625 - ), -}; - -/** - * Hook that uses default transitions for iOS and Android > 11, and uses - * custom interpolation on Android < 11 to achieve more smooth animation - */ -export const useSmoothKeyboardHandler: typeof useKeyboardHandler = ( - handler, - deps -) => { - const target = useSharedValue(-1); - const persistedHeight = useSharedValue(0); - const animatedKeyboardHeight = useSharedValue(0); - - useAnimatedReaction( - () => { - if (IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS) { - 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, - target: target.value, - height: animatedKeyboardHeight.value, - progress: animatedKeyboardHeight.value / persistedHeight.value, - }; - return event; - }, - (evt) => { - if (!evt) { - return; - } - handler.onMove?.(evt); - - // dispatch `onEnd` - if (evt.height === persistedHeight.value) { - handler.onEnd?.(evt); - } - }, - [handler] - ); - - useKeyboardHandler( - { - onStart: (e) => { - 'worklet'; - - // immediately dispatch onStart/onEnd events if onStart dispatched with the same height - // and don't wait for animation 250ms - if ( - !IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS && - e.height === persistedHeight.value - ) { - handler.onStart?.(e); - handler.onEnd?.(e); - - return; - } - - target.value = e.target; - - if (e.height > 0) { - persistedHeight.value = e.height; - } - // if we are running on Android < 9, then we are using custom interpolation - // to achieve smoother animation and use `animatedKeyboardHeight` as animation - // driver - if (!IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS) { - animatedKeyboardHeight.value = withTiming( - e.height, - TELEGRAM_ANDROID_TIMING_CONFIG - ); - } - - handler.onStart?.(e); - }, - onMove: (e) => { - 'worklet'; - - if (IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS) { - handler.onMove?.(e); - } - }, - onEnd: (e) => { - 'worklet'; - - if (IS_ANDROID_ELEVEN_OR_HIGHER_OR_IOS) { - handler.onEnd?.(e); - } - - persistedHeight.value = e.height; - }, - }, - deps - ); -}; diff --git a/FabricExample/src/components/AwareScrollView/utils.ts b/FabricExample/src/components/AwareScrollView/utils.ts deleted file mode 100644 index 0fd416496..000000000 --- a/FabricExample/src/components/AwareScrollView/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const debounce = ) => ReturnType>(worklet: F, wait = 0) => { - 'worklet'; - - const value = { - time: 0, - }; - - return (...args: Parameters): ReturnType | void => { - 'worklet'; - - const t = Date.now(); - const now = t - value.time; - - if (now < wait) { - value.time = t; - return; - } - - value.time = t; - - return worklet(...args); - } -}; \ No newline at end of file diff --git a/FabricExample/src/screens/Examples/AwareScrollView/index.tsx b/FabricExample/src/screens/Examples/AwareScrollView/index.tsx index 5376b41ec..8754cf59d 100644 --- a/FabricExample/src/screens/Examples/AwareScrollView/index.tsx +++ b/FabricExample/src/screens/Examples/AwareScrollView/index.tsx @@ -1,13 +1,10 @@ import React from 'react'; -import { useResizeMode } from 'react-native-keyboard-controller'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; -import KeyboardAwareScrollView from '../../../components/AwareScrollView'; import TextInput from '../../../components/TextInput'; import { styles } from './styles'; export default function AwareScrollView() { - useResizeMode(); - return ( {new Array(10).fill(0).map((_, i) => ( diff --git a/FabricExample/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx b/FabricExample/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx index ea488067c..95ffe3e7d 100644 --- a/FabricExample/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx +++ b/FabricExample/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { LayoutChangeEvent, View, Text, Button } from 'react-native'; import { StackScreenProps } from '@react-navigation/stack'; -import { useResizeMode, KeyboardStickyView } from 'react-native-keyboard-controller'; +import { KeyboardAwareScrollView, KeyboardStickyView } from 'react-native-keyboard-controller'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ExamplesStackParamList } from '../../../navigation/ExamplesStack'; -import KeyboardAwareScrollView from '../../../components/AwareScrollView'; import TextInput from '../../../components/TextInput'; import { styles } from './styles'; @@ -15,8 +14,6 @@ const variants = ['v1', 'v2', 'v3'] as const; type Variant = typeof variants[number]; export default function AwareScrollViewStickyFooter({ navigation }: Props) { - useResizeMode(); - const { bottom } = useSafeAreaInsets(); const [footerHeight, setFooterHeight] = useState(0); const [variant, setVariant] = useState("v1"); diff --git a/README.md b/README.md index fa2a455c3..a3a3cc132 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Keyboard manager which works in identical way on both iOS and Android. - module for changing soft input mode on Android 🤔 - reanimated support 🚀 - interactive keyboard dismissing 👆📱 -- prebuilt components (`KeyboardStickyView`, re-worked `KeyboardAvoidingView`) 📚 +- prebuilt components (`KeyboardStickyView`, `KeyboardAwareScrollView`, re-worked `KeyboardAvoidingView`) 📚 - easy focused input information retrieval 📝 🔮 - works with any navigation library 🧭 - and more is coming... Stay tuned! 😊 diff --git a/docs/docs/api/components/keyboard-aware-scroll-view.mdx b/docs/docs/api/components/keyboard-aware-scroll-view.mdx new file mode 100644 index 000000000..e9ffdb0b1 --- /dev/null +++ b/docs/docs/api/components/keyboard-aware-scroll-view.mdx @@ -0,0 +1,110 @@ +--- +keywords: [react-native-keyboard-controller, KeyboardAwareScrollView, keyboard aware view, aware scroll view] +--- + +# KeyboardAwareScrollView + +import Lottie from 'lottie-react'; +import lottie from '../../../src/components/HomepageFeatures/text-inputs.lottie.json'; + +
+ +
+ +`ScrollView` that effortlessly handles keyboard appearance, automatically scrolls to focused `TextInput` and provides a native-like performance. + +## Comparison + +Current `react-native` ecosystem has a plenty of solutions that solves the problem of focused inputs being covered by keyboard. Each of them has its own advantages and disadvantages. + +Below is a table with the most important functions and their support in various implementations: + +||[react-native-avoid-soft-input](https://mateusz1913.github.io/react-native-avoid-softinput/)|[react-native-keyboard-aware-scroll-view](https://github.com/APSL/react-native-keyboard-aware-scroll-view)|[react-native-keyboard-manager](https://github.com/douglasjunior/react-native-keyboard-manager)|[react-native-keyboard-controller](./keyboard-aware-scroll-view.mdx)| +|-----|-----|-----|-----|-----| +|Respects keyboard animation|🟠 1|❌|✅|✅| +|JS implementation|❌|✅|❌|🟠 2| +|Reacts on focused input layout changes|❌|❌|🟠 3|✅| +|Reacts on focus changes|✅|✅|✅|✅| +|Auto-scroll when user is typing and input in non visible area|❌|❌|🟠 3|✅| +|Android support|✅|✅|❌|✅| +|Maintained|✅|❌|✅|✅| +|Support Fabric (new) architecture|✅|🟠 4|❌|✅| + +> 1 only on iOS + +> 2 KeyboardAwareScrollView is implemented in JS, but some hooks (useKeyboardHandler/useReanimatedFocusedInput/useFocusedInputHandler) exposed from native code + +> 3 achievable with KeyboardManager.reloadLayoutIfNeeded() usage in appropriate TextInput callbacks (onLayout/onChangeText) + +> 4 since it's JS based solution it supports new architecture, but it uses deprecated API. + +## Props + +### ScrollView Props + +Inherits [ScrollView Props](https://reactnative.dev/docs/scrollview#props). + +### `bottomOffset` + +The distance between keyboard and focused `TextInput` when keyboard is shown. + +## Example + +```tsx +import React from 'react'; +import { StyleSheet, TextInputProps, TextInput as TextInputRN } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; + +const TextInput = (props: TextInputProps) => { + return ( + + ); +}; + +export default function AwareScrollView() { + return ( + + {new Array(10).fill(0).map((_, i) => ( + + ))} + + ); +} + +const styles = StyleSheet.create({ + container: { + paddingHorizontal: 16, + }, + content: { + paddingTop: 50, + }, + textInput: { + width: '100%', + minHeight: 50, + maxHeight: 200, + marginBottom: 50, + borderColor: 'black', + borderWidth: 2, + marginRight: 160, + borderRadius: 10, + color: 'black', + paddingHorizontal: 12, + }, +}); +``` \ No newline at end of file diff --git a/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx b/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx deleted file mode 100644 index 9bd8032a4..000000000 --- a/example/src/components/AwareScrollView/KeyboardAwareScrollView.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import React, { FC, useCallback, useMemo } from 'react'; -import { ScrollViewProps, useWindowDimensions } from 'react-native'; -import { - FocusedInputLayoutChangedEvent, - FocusedInputTextChangedEvent, - useFocusedInputHandler, - useReanimatedFocusedInput -} from 'react-native-keyboard-controller'; -import Reanimated, { - interpolate, - scrollTo, - useAnimatedReaction, - useAnimatedRef, - useAnimatedScrollHandler, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated'; -import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler'; -import { debounce } from './utils'; - -type KeyboardAwareScrollViewProps = { - bottomOffset?: number; -} & ScrollViewProps; - -/** - * Everything begins from `onStart` handler. This handler is called every time, - * when keyboard changes its size or when focused `TextInput` was changed. In - * this handler we are calculating/memoizing values which later will be used - * during layout movement. For that we calculate: - * - layout of focused field (`layout`) - to understand whether there will be overlap - * - initial keyboard size (`initialKeyboardSize`) - used in scroll interpolation - * - future keyboard height (`keyboardHeight`) - used in scroll interpolation - * - current scroll position (`scrollPosition`) - used to scroll from this point - * - * Once we've calculated all necessary variables - we can actually start to use them. - * It happens in `onMove` handler - this function simply calls `maybeScroll` with - * current keyboard frame height. This functions makes the smooth transition. - * - * When the transition has finished we go to `onEnd` handler. In this handler - * we verify, that the current field is not overlapped within a keyboard frame. - * For full `onStart`/`onMove`/`onEnd` flow it may look like a redundant thing, - * however there could be some cases, when `onMove` is not called: - * - on iOS when TextInput was changed - keyboard transition is instant - * - on Android when TextInput was changed and keyboard size wasn't changed - * So `onEnd` handler handle the case, when `onMove` wasn't triggered. - * - * ====================================================================================================================+ - * -----------------------------------------------------Flow chart-----------------------------------------------------+ - * ====================================================================================================================+ - * - * +============================+ +============================+ +==================================+ - * + User Press on TextInput + => + Keyboard starts showing + => + As keyboard moves frame by frame + => - * + + + (run `onStart`) + + `onMove` is getting called + - * +============================+ +============================+ +==================================+ - * - * - * +============================+ +============================+ +=====================================+ - * + Keyboard is shown and we + => + User moved focus to + => + Only `onStart`/`onEnd` maybe called + - * + call `onEnd` handler + + another `TextInput` + + (without involving `onMove`) + - * +============================+ +============================+ +=====================================+ - * - */ -const KeyboardAwareScrollView: FC = ({ - children, - bottomOffset = 0, - ...rest -}) => { - const scrollViewAnimatedRef = useAnimatedRef(); - const scrollPosition = useSharedValue(0); - const position = useSharedValue(0); - const keyboardHeight = useSharedValue(0); - const tag = useSharedValue(-1); - const initialKeyboardSize = useSharedValue(0); - const scrollBeforeKeyboardMovement = useSharedValue(0); - const { input } = useReanimatedFocusedInput(); - const layout = useSharedValue(null); - - const { height } = useWindowDimensions(); - - const onScroll = useAnimatedScrollHandler( - { - onScroll: (e) => { - position.value = e.contentOffset.y; - }, - }, - [] - ); - - /** - * Function that will scroll a ScrollView as keyboard gets moving - */ - const maybeScroll = useCallback((e: number, animated: boolean = false) => { - 'worklet'; - - const visibleRect = height - keyboardHeight.value; - const absoluteY = layout.value?.layout.absoluteY || 0; - const inputHeight = layout.value?.layout.height || 0; - const point = absoluteY + inputHeight; - - if (visibleRect - point <= bottomOffset) { - const interpolatedScrollTo = interpolate( - e, - [initialKeyboardSize.value, keyboardHeight.value], - [0, keyboardHeight.value - (height - point) + bottomOffset] - ); - const targetScrollY = - Math.max(interpolatedScrollTo, 0) + scrollPosition.value; - scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated); - - return interpolatedScrollTo; - } - - if (absoluteY < 0) { - const positionOnScreen = visibleRect - inputHeight - bottomOffset; - const topOfScreen = scrollPosition.value + absoluteY; - - scrollTo(scrollViewAnimatedRef, 0, topOfScreen - positionOnScreen, animated); - } - - return 0; - }, [bottomOffset]); - - const onChangeText = useCallback(() => { - 'worklet'; - - // if typing a text caused layout shift, then we need to ignore this handler - // because this event will be handled in `useAnimatedReaction` below - if (layout.value?.layout.height !== input.value?.layout.height) { - return; - } - - const prevScrollPosition = scrollPosition.value; - const prevLayout = layout.value; - - scrollPosition.value = position.value; - layout.value = input.value; - maybeScroll(keyboardHeight.value, true); - scrollPosition.value = prevScrollPosition; - layout.value = prevLayout; - }, [maybeScroll]); - const onChangeTextHandler = useMemo(() => debounce(onChangeText, 200), [onChangeText]); - - useFocusedInputHandler({ - onChangeText: onChangeTextHandler, - }, [onChangeTextHandler]); - - useSmoothKeyboardHandler( - { - onStart: (e) => { - 'worklet'; - - const keyboardWillChangeSize = - keyboardHeight.value !== e.height && e.height > 0; - const keyboardWillAppear = e.height > 0 && keyboardHeight.value === 0; - const keyboardWillHide = e.height === 0; - const focusWasChanged = (tag.value !== e.target && e.target !== -1) || keyboardWillChangeSize; - - if (keyboardWillChangeSize) { - initialKeyboardSize.value = keyboardHeight.value; - } - - if (keyboardWillHide) { - // on back transition need to interpolate as [0, keyboardHeight] - initialKeyboardSize.value = 0; - scrollPosition.value = scrollBeforeKeyboardMovement.value; - } - - if (keyboardWillAppear || keyboardWillChangeSize || focusWasChanged) { - // persist scroll value - scrollPosition.value = position.value; - // just persist height - later will be used in interpolation - keyboardHeight.value = e.height; - } - - // focus was changed - if (focusWasChanged) { - tag.value = e.target; - - // save position of focused text input when keyboard starts to move - layout.value = input.value; - // save current scroll position - when keyboard will hide we'll reuse - // this value to achieve smooth hide effect - scrollBeforeKeyboardMovement.value = position.value; - } - - if (focusWasChanged && !keyboardWillAppear) { - // update position on scroll value, so `onEnd` handler - // will pick up correct values - position.value += maybeScroll(e.height, true); - } - }, - onMove: (e) => { - 'worklet'; - - maybeScroll(e.height); - }, - onEnd: (e) => { - 'worklet'; - - keyboardHeight.value = e.height; - scrollPosition.value = position.value; - }, - }, - [height, maybeScroll] - ); - - useAnimatedReaction(() => input.value, (current, previous) => { - if (current?.target === previous?.target && current?.layout.height !== previous?.layout.height) { - const prevLayout = layout.value; - - layout.value = input.value; - scrollPosition.value += maybeScroll(keyboardHeight.value, true); - layout.value = prevLayout; - } - }, []); - - const view = useAnimatedStyle( - () => ({ - paddingBottom: keyboardHeight.value, - }), - [] - ); - - return ( - - - {children} - - - ); -}; - -export default KeyboardAwareScrollView; diff --git a/example/src/components/AwareScrollView/index.ts b/example/src/components/AwareScrollView/index.ts deleted file mode 100644 index 5903f0ef7..000000000 --- a/example/src/components/AwareScrollView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./KeyboardAwareScrollView"; diff --git a/example/src/screens/Examples/AwareScrollView/index.tsx b/example/src/screens/Examples/AwareScrollView/index.tsx index 5376b41ec..8754cf59d 100644 --- a/example/src/screens/Examples/AwareScrollView/index.tsx +++ b/example/src/screens/Examples/AwareScrollView/index.tsx @@ -1,13 +1,10 @@ import React from 'react'; -import { useResizeMode } from 'react-native-keyboard-controller'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; -import KeyboardAwareScrollView from '../../../components/AwareScrollView'; import TextInput from '../../../components/TextInput'; import { styles } from './styles'; export default function AwareScrollView() { - useResizeMode(); - return ( {new Array(10).fill(0).map((_, i) => ( diff --git a/example/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx b/example/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx index ea488067c..95ffe3e7d 100644 --- a/example/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx +++ b/example/src/screens/Examples/AwareScrollViewStickyFooter/index.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { LayoutChangeEvent, View, Text, Button } from 'react-native'; import { StackScreenProps } from '@react-navigation/stack'; -import { useResizeMode, KeyboardStickyView } from 'react-native-keyboard-controller'; +import { KeyboardAwareScrollView, KeyboardStickyView } from 'react-native-keyboard-controller'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ExamplesStackParamList } from '../../../navigation/ExamplesStack'; -import KeyboardAwareScrollView from '../../../components/AwareScrollView'; import TextInput from '../../../components/TextInput'; import { styles } from './styles'; @@ -15,8 +14,6 @@ const variants = ['v1', 'v2', 'v3'] as const; type Variant = typeof variants[number]; export default function AwareScrollViewStickyFooter({ navigation }: Props) { - useResizeMode(); - const { bottom } = useSafeAreaInsets(); const [footerHeight, setFooterHeight] = useState(0); const [variant, setVariant] = useState("v1"); diff --git a/package.json b/package.json index 22abc6d13..1310e733a 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "text changed", "avoiding view", "sticky view", + "keyboard aware scroll view", "ios", "android" ], diff --git a/FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx b/src/components/KeyboardAwareScrollView/index.tsx similarity index 75% rename from FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx rename to src/components/KeyboardAwareScrollView/index.tsx index 9bd8032a4..317c2ca08 100644 --- a/FabricExample/src/components/AwareScrollView/KeyboardAwareScrollView.tsx +++ b/src/components/KeyboardAwareScrollView/index.tsx @@ -1,11 +1,5 @@ -import React, { FC, useCallback, useMemo } from 'react'; -import { ScrollViewProps, useWindowDimensions } from 'react-native'; -import { - FocusedInputLayoutChangedEvent, - FocusedInputTextChangedEvent, - useFocusedInputHandler, - useReanimatedFocusedInput -} from 'react-native-keyboard-controller'; +import React, { useCallback, useMemo } from 'react'; +import { useWindowDimensions } from 'react-native'; import Reanimated, { interpolate, scrollTo, @@ -15,9 +9,19 @@ import Reanimated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated'; + +import { + useFocusedInputHandler, + useReanimatedFocusedInput, +} from 'react-native-keyboard-controller'; + import { useSmoothKeyboardHandler } from './useSmoothKeyboardHandler'; import { debounce } from './utils'; +import type { FC } from 'react'; +import type { ScrollViewProps } from 'react-native'; +import type { FocusedInputLayoutChangedEvent } from 'react-native-keyboard-controller'; + type KeyboardAwareScrollViewProps = { bottomOffset?: number; } & ScrollViewProps; @@ -89,36 +93,44 @@ const KeyboardAwareScrollView: FC = ({ /** * Function that will scroll a ScrollView as keyboard gets moving */ - const maybeScroll = useCallback((e: number, animated: boolean = false) => { - 'worklet'; + const maybeScroll = useCallback( + (e: number, animated: boolean = false) => { + 'worklet'; - const visibleRect = height - keyboardHeight.value; - const absoluteY = layout.value?.layout.absoluteY || 0; - const inputHeight = layout.value?.layout.height || 0; - const point = absoluteY + inputHeight; - - if (visibleRect - point <= bottomOffset) { - const interpolatedScrollTo = interpolate( - e, - [initialKeyboardSize.value, keyboardHeight.value], - [0, keyboardHeight.value - (height - point) + bottomOffset] - ); - const targetScrollY = - Math.max(interpolatedScrollTo, 0) + scrollPosition.value; - scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated); - - return interpolatedScrollTo; - } + const visibleRect = height - keyboardHeight.value; + const absoluteY = layout.value?.layout.absoluteY || 0; + const inputHeight = layout.value?.layout.height || 0; + const point = absoluteY + inputHeight; - if (absoluteY < 0) { - const positionOnScreen = visibleRect - inputHeight - bottomOffset; - const topOfScreen = scrollPosition.value + absoluteY; + if (visibleRect - point <= bottomOffset) { + const interpolatedScrollTo = interpolate( + e, + [initialKeyboardSize.value, keyboardHeight.value], + [0, keyboardHeight.value - (height - point) + bottomOffset] + ); + const targetScrollY = + Math.max(interpolatedScrollTo, 0) + scrollPosition.value; + scrollTo(scrollViewAnimatedRef, 0, targetScrollY, animated); - scrollTo(scrollViewAnimatedRef, 0, topOfScreen - positionOnScreen, animated); - } + return interpolatedScrollTo; + } - return 0; - }, [bottomOffset]); + if (absoluteY < 0) { + const positionOnScreen = visibleRect - inputHeight - bottomOffset; + const topOfScreen = scrollPosition.value + absoluteY; + + scrollTo( + scrollViewAnimatedRef, + 0, + topOfScreen - positionOnScreen, + animated + ); + } + + return 0; + }, + [bottomOffset] + ); const onChangeText = useCallback(() => { 'worklet'; @@ -138,11 +150,17 @@ const KeyboardAwareScrollView: FC = ({ scrollPosition.value = prevScrollPosition; layout.value = prevLayout; }, [maybeScroll]); - const onChangeTextHandler = useMemo(() => debounce(onChangeText, 200), [onChangeText]); + const onChangeTextHandler = useMemo( + () => debounce(onChangeText, 200), + [onChangeText] + ); - useFocusedInputHandler({ - onChangeText: onChangeTextHandler, - }, [onChangeTextHandler]); + useFocusedInputHandler( + { + onChangeText: onChangeTextHandler, + }, + [onChangeTextHandler] + ); useSmoothKeyboardHandler( { @@ -153,7 +171,8 @@ const KeyboardAwareScrollView: FC = ({ keyboardHeight.value !== e.height && e.height > 0; const keyboardWillAppear = e.height > 0 && keyboardHeight.value === 0; const keyboardWillHide = e.height === 0; - const focusWasChanged = (tag.value !== e.target && e.target !== -1) || keyboardWillChangeSize; + const focusWasChanged = + (tag.value !== e.target && e.target !== -1) || keyboardWillChangeSize; if (keyboardWillChangeSize) { initialKeyboardSize.value = keyboardHeight.value; @@ -184,7 +203,7 @@ const KeyboardAwareScrollView: FC = ({ } if (focusWasChanged && !keyboardWillAppear) { - // update position on scroll value, so `onEnd` handler + // update position on scroll value, so `onEnd` handler // will pick up correct values position.value += maybeScroll(e.height, true); } @@ -204,15 +223,22 @@ const KeyboardAwareScrollView: FC = ({ [height, maybeScroll] ); - useAnimatedReaction(() => input.value, (current, previous) => { - if (current?.target === previous?.target && current?.layout.height !== previous?.layout.height) { - const prevLayout = layout.value; + useAnimatedReaction( + () => input.value, + (current, previous) => { + if ( + current?.target === previous?.target && + current?.layout.height !== previous?.layout.height + ) { + const prevLayout = layout.value; - layout.value = input.value; - scrollPosition.value += maybeScroll(keyboardHeight.value, true); - layout.value = prevLayout; - } - }, []); + layout.value = input.value; + scrollPosition.value += maybeScroll(keyboardHeight.value, true); + layout.value = prevLayout; + } + }, + [] + ); const view = useAnimatedStyle( () => ({ @@ -228,9 +254,7 @@ const KeyboardAwareScrollView: FC = ({ onScroll={onScroll} scrollEventThrottle={16} > - - {children} - + {children} ); }; diff --git a/example/src/components/AwareScrollView/useSmoothKeyboardHandler.ts b/src/components/KeyboardAwareScrollView/useSmoothKeyboardHandler.ts similarity index 99% rename from example/src/components/AwareScrollView/useSmoothKeyboardHandler.ts rename to src/components/KeyboardAwareScrollView/useSmoothKeyboardHandler.ts index a30ce6e85..aad6d0c11 100644 --- a/example/src/components/AwareScrollView/useSmoothKeyboardHandler.ts +++ b/src/components/KeyboardAwareScrollView/useSmoothKeyboardHandler.ts @@ -5,6 +5,7 @@ import { useSharedValue, withTiming, } from 'react-native-reanimated'; + import { useKeyboardHandler } from 'react-native-keyboard-controller'; const IS_ANDROID_ELEVEN_OR_HIGHER = diff --git a/example/src/components/AwareScrollView/utils.ts b/src/components/KeyboardAwareScrollView/utils.ts similarity index 86% rename from example/src/components/AwareScrollView/utils.ts rename to src/components/KeyboardAwareScrollView/utils.ts index 0fd416496..7af503d8b 100644 --- a/example/src/components/AwareScrollView/utils.ts +++ b/src/components/KeyboardAwareScrollView/utils.ts @@ -1,4 +1,7 @@ -export const debounce = ) => ReturnType>(worklet: F, wait = 0) => { +export const debounce = ) => ReturnType>( + worklet: F, + wait = 0 +) => { 'worklet'; const value = { @@ -19,5 +22,5 @@ export const debounce = ) => ReturnType>(wo value.time = t; return worklet(...args); - } -}; \ No newline at end of file + }; +}; diff --git a/src/components/index.ts b/src/components/index.ts index 430899a3a..1b3a0e614 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,3 @@ export { default as KeyboardAvoidingView } from './KeyboardAvoidingView'; export { default as KeyboardStickyView } from './KeyboardStickyView'; +export { default as KeyboardAwareScrollView } from './KeyboardAwareScrollView'; diff --git a/src/index.ts b/src/index.ts index 33747a75a..fc892a5fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,4 +6,8 @@ export * from './hooks'; export * from './constants'; export * from './types'; -export { KeyboardAvoidingView, KeyboardStickyView } from './components'; +export { + KeyboardAvoidingView, + KeyboardStickyView, + KeyboardAwareScrollView, +} from './components';