diff --git a/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx b/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx index 56fb7954f..5c79e574e 100644 --- a/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx +++ b/src/components/bottomSheetScrollable/createBottomSheetScrollableComponent.tsx @@ -39,6 +39,7 @@ export function createBottomSheetScrollableComponent( progressViewOffset, refreshControl, scrollBuffer, + preserveScrollMomentum, // events onScroll, onScrollBeginDrag, @@ -59,7 +60,8 @@ export function createBottomSheetScrollableComponent( onScroll, onScrollBeginDrag, onScrollEndDrag, - scrollBuffer + scrollBuffer, + preserveScrollMomentum ); const { enableContentPanningGesture, @@ -73,8 +75,7 @@ export function createBottomSheetScrollableComponent( //#region variables const scrollableAnimatedProps = useAnimatedProps( () => ({ - decelerationRate: - SCROLLABLE_DECELERATION_RATE_MAPPER[animatedScrollableState.value], + ...(preserveScrollMomentum ? {} : {decelerationRate: SCROLLABLE_DECELERATION_RATE_MAPPER[animatedScrollableState.value]}), showsVerticalScrollIndicator: showsVerticalScrollIndicator ? animatedScrollableState.value === SCROLLABLE_STATE.UNLOCKED : showsVerticalScrollIndicator, @@ -127,6 +128,7 @@ export function createBottomSheetScrollableComponent( scrollableContentOffsetY, onRefresh !== undefined, scrollBuffer, + preserveScrollMomentum, focusHook ); //#endregion diff --git a/src/components/bottomSheetScrollable/types.d.ts b/src/components/bottomSheetScrollable/types.d.ts index ba4c2145e..949b785bc 100644 --- a/src/components/bottomSheetScrollable/types.d.ts +++ b/src/components/bottomSheetScrollable/types.d.ts @@ -52,6 +52,11 @@ export interface BottomSheetScrollableProps { * An initial scroll buffer to prevent the bottom sheet from immediately following the scroll gesture. */ scrollBuffer?: number; + + /** + * Whether or not to preserve scroll momentum when expanding a scrollable bottom sheet component. + */ + preserveScrollMomentum?: boolean; } export type ScrollableProps = diff --git a/src/hooks/useScrollEventsHandlersDefault.ts b/src/hooks/useScrollEventsHandlersDefault.ts index c7ea4f0bd..b09dc8eed 100644 --- a/src/hooks/useScrollEventsHandlersDefault.ts +++ b/src/hooks/useScrollEventsHandlersDefault.ts @@ -14,7 +14,8 @@ export type ScrollEventContextType = { export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( scrollableRef, scrollableContentOffsetY, - scrollBuffer + scrollBuffer, + preserveScrollMomentum ) => { // hooks const { @@ -25,6 +26,7 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( isScrollableLocked, } = useBottomSheetInternal(); const awaitingFirstScroll = useSharedValue(false); + const scrollEnded = useSharedValue(false); //#region callbacks const handleOnScroll: ScrollEventHandlerCallbackType = @@ -36,9 +38,9 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( * handleOnBeginDrag, and the scrollable shouldn't be locked when scrolling back to the * start of the list. */ - if (scrollBuffer && awaitingFirstScroll.value && !isScrollableLocked.value) { + if ((preserveScrollMomentum || scrollBuffer) && awaitingFirstScroll.value && !isScrollableLocked.value) { const isScrollingTowardsBottom = context.initialContentOffsetY < event.contentOffset.y; - if (isScrollingTowardsBottom && event.contentOffset.y > scrollBuffer && context.shouldLockInitialPosition) { + if (isScrollingTowardsBottom && event.contentOffset.y > (scrollBuffer ?? 0) && context.shouldLockInitialPosition) { isScrollableLocked.value = true; animatedScrollableState.value = SCROLLABLE_STATE.LOCKED; context.shouldLockInitialPosition = true; @@ -58,12 +60,14 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( } if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { - const lockPosition = context.shouldLockInitialPosition - ? context.initialContentOffsetY ?? 0 - : 0; - // @ts-ignore - scrollTo(scrollableRef, 0, lockPosition, false); - scrollableContentOffsetY.value = lockPosition; + if (!(preserveScrollMomentum && scrollEnded.value)) { + const lockPosition = context.shouldLockInitialPosition + ? context.initialContentOffsetY ?? 0 + : 0; + // @ts-ignore + scrollTo(scrollableRef, 0, lockPosition, false); + scrollableContentOffsetY.value = lockPosition; + } return; } }, @@ -82,6 +86,7 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( rootScrollableContentOffsetY.value = y; context.initialContentOffsetY = y; awaitingFirstScroll.value = true; + scrollEnded.value = false; if (scrollBuffer) { if (y <= 0 && ( @@ -93,7 +98,7 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( isScrollableLocked.value = false; } } else { - isScrollableLocked.value = true; + isScrollableLocked.value = preserveScrollMomentum ? y <= 0 : true; } /** @@ -118,8 +123,9 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( ); const handleOnEndDrag: ScrollEventHandlerCallbackType = useWorkletCallback( - ({ contentOffset: { y } }, context) => { + ({ contentOffset: { y }}, context) => { awaitingFirstScroll.value = false; + scrollEnded.value = true; if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { const lockPosition = context.shouldLockInitialPosition ? context.initialContentOffsetY ?? 0 @@ -146,12 +152,14 @@ export const useScrollEventsHandlersDefault: ScrollEventsHandlersHookType = ( useWorkletCallback( ({ contentOffset: { y } }, context) => { if (animatedScrollableState.value === SCROLLABLE_STATE.LOCKED) { - const lockPosition = context.shouldLockInitialPosition - ? context.initialContentOffsetY ?? 0 - : 0; - // @ts-ignore - scrollTo(scrollableRef, 0, lockPosition, false); - scrollableContentOffsetY.value = 0; + if (!(preserveScrollMomentum && scrollEnded.value)) { + const lockPosition = context.shouldLockInitialPosition + ? context.initialContentOffsetY ?? 0 + : 0; + // @ts-ignore + scrollTo(scrollableRef, 0, lockPosition, false); + scrollableContentOffsetY.value = 0; + } return; } if (animatedAnimationState.value !== ANIMATION_STATE.RUNNING) { diff --git a/src/hooks/useScrollHandler.ts b/src/hooks/useScrollHandler.ts index 49fecfab5..dfdc6a3d2 100644 --- a/src/hooks/useScrollHandler.ts +++ b/src/hooks/useScrollHandler.ts @@ -13,7 +13,8 @@ export const useScrollHandler = ( onScroll?: ScrollableEvent, onScrollBeginDrag?: ScrollableEvent, onScrollEndDrag?: ScrollableEvent, - scrollBuffer?: number + scrollBuffer?: number, + preserveScrollMomentum?: boolean ) => { // refs const scrollableRef = useAnimatedRef(); @@ -28,7 +29,7 @@ export const useScrollHandler = ( handleOnEndDrag = noop, handleOnMomentumEnd = noop, handleOnMomentumBegin = noop, - } = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY, scrollBuffer); + } = useScrollEventsHandlers(scrollableRef, scrollableContentOffsetY, scrollBuffer, preserveScrollMomentum); // callbacks const scrollHandler = useAnimatedScrollHandler( diff --git a/src/hooks/useScrollableSetter.ts b/src/hooks/useScrollableSetter.ts index fbdb3a096..bbe6565be 100644 --- a/src/hooks/useScrollableSetter.ts +++ b/src/hooks/useScrollableSetter.ts @@ -11,6 +11,7 @@ export const useScrollableSetter = ( contentOffsetY: Animated.SharedValue, refreshable: boolean, scrollBuffer: number | undefined, + preserveScrollMomentum: boolean | undefined, useFocusHook = useEffect ) => { // hooks @@ -30,7 +31,7 @@ export const useScrollableSetter = ( rootScrollableContentOffsetY.value = contentOffsetY.value; animatedScrollableType.value = type; isScrollableRefreshable.value = refreshable; - isScrollableLocked.value = !scrollBuffer; + isScrollableLocked.value = !preserveScrollMomentum && !scrollBuffer; isContentHeightFixed.value = false; // set current scrollable ref diff --git a/src/types.d.ts b/src/types.d.ts index 5a5db0840..2cd97c324 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -157,6 +157,7 @@ export type ScrollEventsHandlersHookType = ( ref: React.RefObject, contentOffsetY: SharedValue, scrollBuffer: number | undefined, + preserveScrollMomentum: boolean | undefined, ) => { handleOnScroll?: ScrollEventHandlerCallbackType; handleOnBeginDrag?: ScrollEventHandlerCallbackType;