import React, { createContext, useContext, useMemo, useRef, useState } from "react"; import { KeyboardEventListener, NativeScrollEvent, NativeSyntheticEvent, Platform, ScrollViewProps, View, ScrollView } from "react-native"; import Animated, { scrollTo, useAnimatedRef, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming } from "react-native-reanimated"; import { useKeyboardHandlers } from "../hooks"; import { closeAnimation, measureFocusedInputBottomYAsync } from "../utilities"; import { CommonProps, defaultCommonProps } from "./common-props"; const ScrollContext = createContext<{ registerView: ({view, id,}:{view: View, id: string}) => void, unregisterView: (id: string)=>void, } | null>(null); type Props = ScrollViewProps & CommonProps & { /** * What to do when the keyboard hides on iOS. * @option 'stay' - *Default* scroll view will not move when the keyboard hides (it will stay where it is.) * @option 'revert' - Scroll view will return to its original position when the keyboard hides. */ iosHideBehavior?: 'stay' | 'revert', } async function measureView(view: View) { return new Promise<{ top: number, bottom: number }>(resolve => { view.measure((x, y, width, height, px, py) => { resolve({ bottom: py + height, top: py }); }) }) } export default function KeyboardAvoiderScrollView({ animationEasing=defaultCommonProps.animationEasing, animationTime=defaultCommonProps.animationTime, extraSpace=defaultCommonProps.extraSpace, iosHideBehavior='stay', ...props }: Props) { const scrollviewRef = useAnimatedRef(); const registeredSectionViews = useRef<{[id: string]: View}>({}); const keyboardTopRef = useRef(0); const currentScroll = useRef(0); const scrollMax = useRef(0); const androidPannedBy = useRef(0); const androidFocusScrolledBy = useRef(0); const scrollViewHeight = useRef(0); const spacerHasLayout = useRef(false); const scroll = useSharedValue(0); const yTranslate = useSharedValue(0); const yTranslateStyle = useAnimatedStyle(() => ({ transform: [{ translateY: yTranslate.value }] })) useDerivedValue(() => { scrollTo(scrollviewRef, 0, scroll.value, false) }) function scrollBy(y: number) { yTranslate.value = withTiming(-y, { duration: animationTime }) } async function scrollToKeyboard(y: number, keyboardY: number, inputBottomY: number) { spacerHasLayout.current = false; if (Platform.OS == 'android') { androidPannedBy.current = Math.max(0, inputBottomY - keyboardY); console.log("Panned by: ", androidPannedBy.current) const s = Math.max(y - keyboardY + extraSpace - androidPannedBy.current, 0); scrollBy(s) return; } else if (Platform.OS == 'ios') { const scrollByY = y + extraSpace - keyboardY; if (scrollByY <= 0) return; scrollBy(scrollByY) } } const handleKeyboardWillShow: KeyboardEventListener = (e) => { keyboardTopRef.current = e.endCoordinates.screenY; const promises = Object.values(registeredSectionViews.current).map(e => measureView(e)); (async function () { const inputMeasurePromise = measureFocusedInputBottomYAsync(); const sectionViewMeasures = await Promise.all(promises); const inputMeasure = await inputMeasurePromise; for (var sectionMeasure of sectionViewMeasures) { if (inputMeasure <= sectionMeasure.bottom && inputMeasure >= sectionMeasure.top) { scrollToKeyboard(sectionMeasure.bottom, e.endCoordinates.screenY, inputMeasure) return; } } scrollToKeyboard(inputMeasure, e.endCoordinates.screenY, inputMeasure) })() } function handleKeyboardWillHide() { if(Platform.OS == 'android' || iosHideBehavior == 'revert') { yTranslate.value = withTiming(0, closeAnimation(animationTime, animationEasing)) return; } const scrollsToAdjustedForTranslate = currentScroll.current - yTranslate.value; const scrollY = Math.min(scrollsToAdjustedForTranslate, scrollMax.current) if(scrollsToAdjustedForTranslate >= scrollMax.current) { // In cases where there is no room to actually scroll the scroll view we just animate back to the // start position. yTranslate.value = withTiming(0, closeAnimation(animationTime, animationEasing)) return; } scrollviewRef.current?.scrollTo({ y: scrollY, animated: false },) yTranslate.value = 0; } useKeyboardHandlers({ showHandler: handleKeyboardWillShow, hideHandler: handleKeyboardWillHide }) return ( { return { registerView: ({id, view}) => { registeredSectionViews.current[id] = view }, unregisterView: (id)=>{ delete registeredSectionViews.current[id];// } } }, []) } > ) => { if (props.onScroll) props.onScroll(e); currentScroll.current = e.nativeEvent.contentOffset.y scrollMax.current = e.nativeEvent.contentSize.height - e.nativeEvent.layoutMeasurement.height scrollViewHeight.current = e.nativeEvent.layoutMeasurement.height; }} > {props.children} ) } export function useScrollViewContext() { return useContext(ScrollContext); }