import React, { memo, useCallback, useEffect, useRef, useMemo, useState, } from 'react'; import {ScrollableView} from './types'; import { Platform, StyleSheet, ScrollView, ScrollViewProps, View, DeviceEventEmitter, } from 'react-native'; import RefreshControlContainer from './RefreshControlContainer'; import {NativeViewGestureHandler} from 'react-native-gesture-handler'; import {NormalSceneProps, HPageViewProps} from './types'; import { useSceneContext, useSharedScrollableRef, useSyncInitialPosition, useRefreshDerivedValue, useVerifyProps, } from './hook'; import {mScrollTo, animateToRefresh, snapAfterGlideOver} from './utils'; import Animated, { runOnJS, useDerivedValue, useAnimatedScrollHandler, useSharedValue, useAnimatedStyle, useAnimatedReaction, withTiming, cancelAnimation, interpolate, Extrapolate, } from 'react-native-reanimated'; import {Events} from 'react-native-head-tab-view-flashlist-support/enum'; const __IOS = Platform.OS === 'ios'; const createCollapsibleScrollView = (Component: ScrollableView) => { const AnimatePageView = Animated.createAnimatedComponent(Component); return React.forwardRef((props: any, ref) => { return ( ); }); }; const SceneComponent: React.FC = ({ index, bounces, scrollEnabled = true, forwardedRef, onScroll, onStartRefresh, onContentSizeChange, onScrollBeginDrag, renderLoadingView, ContainerView, isRefreshing: _isRefreshing = false, renderRefreshControl: _renderRefreshControl, StickyHeaderComponent, ...restProps }) => { if (onScroll !== undefined) { console.warn('Please do not assign onScroll'); } const { shareAnimatedValue = useSharedValue(0), tabbarHeight, headerHeight, expectHeight, tabsRefreshEnabled, curIndexValue, tabsIsWorking, isTouchTabs, isSlidingHeader, refreshHeight, overflowPull, frozeTop, pullExtendedCoefficient, enableSnap, scrollingCheckDuration, refHasChanged, updateSceneInfo, floatingButtonHeight, componentId, } = useSceneContext(); const _scrollView = useSharedScrollableRef(forwardedRef); const panRef = useRef(); const scrollY = useSharedValue(0); const realY = useSharedValue(0); const trans = useSharedValue(0); const refreshTrans = useSharedValue(refreshHeight); const isTouchTabsPrev = useSharedValue(false); const isSlidingHeaderPrev = useSharedValue(false); const isRefreshing = useSharedValue(false); const isRefreshingWithAnimation = useSharedValue(false); const isDragging: {value: boolean} = useSharedValue(false); const isLosingMomentum: {value: boolean} = useSharedValue(false); const {opacityValue, syncInitialPosition} = useSyncInitialPosition(_scrollView); const needSnap = useSharedValue(false); const isScrolling = useSharedValue(0); const calcHeight = useMemo(() => { return tabbarHeight + headerHeight; }, [tabbarHeight, headerHeight]); const isInitial = useRef(true); const scrollEnabledValue = useDerivedValue(() => { return ( !isDragging.value && !tabsIsWorking.value && !isRefreshing.value && !isRefreshingWithAnimation.value && curIndexValue.value === index ); }); const canSnapFunc = () => { 'worklet'; return ( needSnap.value && !isTouchTabs.value && !isSlidingHeader.value && !isRefreshing.value && !isRefreshingWithAnimation.value && !tabsIsWorking.value ); }; const refreshValue = useDerivedValue(() => { if (isRefreshing.value && isRefreshingWithAnimation.value) { return refreshHeight - refreshTrans.value; } return trans.value - shareAnimatedValue.value; }); useAnimatedReaction( () => { return scrollEnabledValue.value; }, (mScrollEnabled) => { _scrollView && _scrollView.current && _scrollView.current.setNativeProps({scrollEnabled: mScrollEnabled}); }, [scrollEnabledValue, _scrollView], ); const updateScrollYTrans = useCallback( (value: number) => { 'worklet'; scrollY.value = Math.max(value, 0); }, [scrollY], ); const updateShareValue = useCallback( (value: number) => { 'worklet'; if (curIndexValue.value !== index) return; //Avoid causing updates to the ShareAnimatedValue after the drop-down has finished if (isRefreshing.value !== isRefreshingWithAnimation.value) return; shareAnimatedValue.value = value; }, [ curIndexValue.value, shareAnimatedValue, index, isRefreshing.value, isRefreshingWithAnimation.value, ], ); const tryToSnap = useCallback(() => { 'worklet'; if (!enableSnap) return; cancelAnimation(isScrolling); if (canSnapFunc()) { isScrolling.value = 1; isScrolling.value = withTiming( 0, {duration: scrollingCheckDuration}, (isFinished) => { if (isFinished && canSnapFunc()) { needSnap.value = false; snapAfterGlideOver({ sceneRef: _scrollView, shareAnimatedValue, headerHeight, frozeTop, }); } }, ); } }, [ isScrolling.value, _scrollView, needSnap, shareAnimatedValue, headerHeight, frozeTop, enableSnap, scrollingCheckDuration, ]); const onScrollAnimateEvent = useAnimatedScrollHandler( { onScroll: (event, ctx) => { realY.value = event.contentOffset.y; let moveY = Math.max(event.contentOffset.y, 0); if (isRefreshingWithAnimation.value || isRefreshing.value) return; tryToSnap(); moveY = isRefreshing.value && isRefreshingWithAnimation.value ? moveY + refreshHeight : moveY; updateScrollYTrans(moveY); updateShareValue(moveY); }, onMomentumBegin: () => { isLosingMomentum.value = true; }, onMomentumEnd: () => { isLosingMomentum.value = false; }, }, [ curIndexValue, updateShareValue, updateScrollYTrans, isRefreshingWithAnimation, tryToSnap, ], ); const onRefreshStatusCallback = React.useCallback( (isToRefresh: boolean) => { if (isToRefresh) { animateToRefresh({ transRefreshing: refreshTrans, isRefreshing, isRefreshingWithAnimation, destPoi: shareAnimatedValue.value, isToRefresh: true, onStartRefresh, }); } else { const destPoi = shareAnimatedValue.value > headerHeight + refreshHeight ? shareAnimatedValue.value : shareAnimatedValue.value + refreshHeight; animateToRefresh({ transRefreshing: refreshTrans, isRefreshing, isRefreshingWithAnimation, destPoi, isToRefresh: false, }); } }, [onStartRefresh, refreshHeight, headerHeight], ); useEffect(() => { refHasChanged && refHasChanged(panRef); }, [refHasChanged, panRef]); useEffect(() => { if (_scrollView && _scrollView.current) { updateSceneInfo({ scrollRef: _scrollView, index, refreshTrans, isRefreshing, isRefreshingWithAnimation, canPullRefresh: onStartRefresh !== undefined, scrollY, isDragging, scrollEnabledValue, isLosingMomentum, onRefreshStatusCallback, }); } }, [ _scrollView, index, refreshTrans, isRefreshing, isRefreshingWithAnimation, onStartRefresh, scrollY, isDragging, onRefreshStatusCallback, ]); //adjust the scene size const _onContentSizeChange = useCallback( (contentWidth: number, contentHeight: number) => { onContentSizeChange && onContentSizeChange(contentWidth, contentHeight); //Some mobile phones measure less than their actual height. And the difference in height is not more than a pixel if (Math.ceil(contentHeight) >= expectHeight) { syncInitialPosition( isRefreshing.value ? shareAnimatedValue.value - refreshHeight : shareAnimatedValue.value, ); } }, [ onContentSizeChange, syncInitialPosition, expectHeight, isRefreshing, refreshTrans, shareAnimatedValue, ], ); //Pull-refresh useEffect(() => { if (_isRefreshing) { onRefreshStatusCallback(true); } else { if (isInitial.current) return; onRefreshStatusCallback(false); } isInitial.current = false; }, [_isRefreshing, onRefreshStatusCallback, isInitial]); //Finger off the screen useAnimatedReaction( () => { return isTouchTabs.value !== isTouchTabsPrev.value && enableSnap; }, (result) => { if (!result) return; isTouchTabsPrev.value = isTouchTabs.value; if (isTouchTabs.value === true) return; needSnap.value = true; tryToSnap(); }, [isTouchTabs, isTouchTabsPrev, tryToSnap, enableSnap], ); //Slide header over useAnimatedReaction( () => { return isSlidingHeader.value !== isSlidingHeaderPrev.value && enableSnap; }, (result) => { if (!result) return; isSlidingHeaderPrev.value = isSlidingHeader.value; if (isSlidingHeader.value === true) return; needSnap.value = true; tryToSnap(); }, [isSlidingHeader, isSlidingHeaderPrev, tryToSnap, enableSnap], ); useAnimatedReaction( () => { return refreshTrans.value; }, (mTrans) => { trans.value = Math.max(refreshHeight - mTrans, 0); }, [refreshTrans, refreshHeight], ); useAnimatedReaction( () => { return ( isRefreshing.value === false && isRefreshingWithAnimation.value === true && refreshTrans ); }, (isStart) => { if (!isStart) return; if (realY.value === refreshTrans.value - refreshHeight) return; mScrollTo(_scrollView, 0, refreshTrans.value - refreshHeight, false); }, [isRefreshing, isRefreshingWithAnimation, refreshTrans, refreshHeight], ); useAnimatedReaction( () => { return ( refreshTrans.value <= refreshHeight && (isDragging.value || isRefreshingWithAnimation.value) ); }, (isStart) => { if (!isStart) return; if (realY.value !== 0) { mScrollTo(_scrollView, 0, 0, false); } }, [refreshTrans, refreshHeight, isDragging, isRefreshingWithAnimation], ); useAnimatedReaction( () => { return ( refreshTrans.value >= 0 && isRefreshingWithAnimation.value && isRefreshing.value ); }, (isStart) => { if (!isStart) return; updateScrollYTrans(refreshTrans.value); updateShareValue(refreshTrans.value); }, [ refreshTrans, refreshHeight, isRefreshingWithAnimation, isRefreshing, _scrollView, updateShareValue, updateScrollYTrans, ], ); useAnimatedReaction( () => { return ( refreshTrans.value > refreshHeight && isRefreshing.value && isRefreshingWithAnimation.value ); }, (start) => { if (!start) return; if (realY.value !== refreshTrans.value - refreshHeight) { mScrollTo(_scrollView, 0, refreshTrans.value - refreshHeight, false); } }, [ refreshTrans, refreshHeight, isRefreshing, isRefreshingWithAnimation, _scrollView, ], ); const translateY = useRefreshDerivedValue({ animatedValue: trans, refreshHeight, overflowPull, pullExtendedCoefficient, }); const animatedStyle = useAnimatedStyle(() => { return { transform: [ { translateY: translateY.value, }, ], }; }); const renderRefreshControl = () => { if (!onStartRefresh) return; return ( ); }; const bouncesEnabled = useMemo(() => { return __IOS && !tabsRefreshEnabled && onStartRefresh === undefined; }, [tabsRefreshEnabled, onStartRefresh]); const sceneStyle = useAnimatedStyle(() => { return { opacity: opacityValue.value, }; }); return ( {opacityValue.value !== 1 && renderLoadingView ? ( {renderLoadingView(headerHeight)} ) : null} {renderRefreshControl()} ); }; interface SceneListComponentProps { panRef: any; ContainerView: any; zForwardedRef: any; headerHeight: number; expectHeight: number; } const SceneListComponent: React.FC< SceneListComponentProps & ScrollViewProps > = ({ panRef, ContainerView, zForwardedRef, headerHeight, expectHeight, scrollEventThrottle, directionalLockEnabled, contentContainerStyle, scrollIndicatorInsets, floatingButtonHeight, StickyHeaderComponent, realY, calcHeight, tabbarHeight, componentId, ...rest }) => { const { contentContainerStyle: _contentContainerStyle, scrollIndicatorInsets: _scrollIndicatorInsets, } = useVerifyProps({ scrollEventThrottle, directionalLockEnabled, contentContainerStyle, scrollIndicatorInsets, }); const cacheContentHeight = useRef({headerHeight, tabbarHeight}); const stickyHeaderAnimatedStyles = useAnimatedStyle(() => { return { transform: [ { translateY: interpolate( realY.value, [headerHeight, headerHeight - tabbarHeight], [tabbarHeight, 0], Extrapolate.CLAMP, ), }, ], }; }); useEffect(() => { cacheContentHeight.current = {headerHeight, tabbarHeight}; }, [headerHeight, tabbarHeight]); useEffect(() => { const scrollToTopEvent = DeviceEventEmitter.addListener( Events.LIST_SCROLL_TO_TOP, (eventParams) => { if (eventParams?.componentId === componentId) { zForwardedRef?.current?.scrollTo?.({ animated: true, y: 0, ...eventParams, }); } }, ); const scrollToTabBarEvent = DeviceEventEmitter.addListener( Events.LIST_SCROLL_DOWN_TO_TAB_BAR, (eventParams) => { if (eventParams?.componentId === componentId) { zForwardedRef?.current?.scrollTo?.({ animated: true, y: cacheContentHeight.current?.headerHeight + eventParams?.extraOffset ?? 0, ...eventParams, }); } }, ); const scrollToOffset = DeviceEventEmitter.addListener( Events.LIST_SCROLL_TO_OFFSET, (eventParams) => { if (eventParams?.componentId === componentId) { zForwardedRef?.current?.scrollTo?.({ animated: true, y: eventParams?.offset, ...eventParams, }); } }, ); return () => { scrollToTopEvent.remove(); scrollToTabBarEvent.remove(); scrollToOffset.remove(); }; }, []); return ( <> {StickyHeaderComponent && ( )} {rest.children} ); }; const MemoList = memo(SceneListComponent); const styles = StyleSheet.create({ container: { flex: 1, }, }); export default createCollapsibleScrollView;