From bd9846b13dff6bc0628a94765ff91ca993f67651 Mon Sep 17 00:00:00 2001 From: Mo Gorhom Date: Wed, 4 Nov 2020 23:22:09 +0100 Subject: [PATCH 1/8] chore: added dynamic snap point based on content height implementation --- src/components/bottomSheet/BottomSheet.tsx | 110 +++++++++++++------- src/components/bottomSheet/styles.ts | 8 +- src/components/bottomSheet/types.d.ts | 6 ++ src/components/bottomSheet/useTransition.ts | 45 ++++++-- 4 files changed, 125 insertions(+), 44 deletions(-) diff --git a/src/components/bottomSheet/BottomSheet.tsx b/src/components/bottomSheet/BottomSheet.tsx index 447b02e87..c5c29fd01 100644 --- a/src/components/bottomSheet/BottomSheet.tsx +++ b/src/components/bottomSheet/BottomSheet.tsx @@ -5,8 +5,9 @@ import React, { forwardRef, useImperativeHandle, memo, + useState, } from 'react'; -import { ViewStyle } from 'react-native'; +import { ViewStyle, Dimensions } from 'react-native'; import isEqual from 'lodash.isequal'; import invariant from 'invariant'; import Animated, { @@ -17,12 +18,12 @@ import Animated, { cond, neq, and, - // concat, - greaterThan, + lessThan, Extrapolate, set, - // defined, + // concat, sub, + abs, } from 'react-native-reanimated'; import { PanGestureHandler, @@ -59,6 +60,7 @@ const { interpolateNode: interpolateV2, } = require('react-native-reanimated'); const interpolate = interpolateV2 || interpolateV1; +const { height: windowHeight } = Dimensions.get('window'); type BottomSheet = BottomSheetMethods; @@ -77,6 +79,7 @@ const BottomSheetComponent = forwardRef( snapPoints: _snapPoints, topInset = 0, enabled = true, + shouldMeasureContentHeight = false, // animated nodes callback animatedPosition: _animatedPosition, animatedPositionIndex: _animatedPositionIndex, @@ -152,21 +155,43 @@ const BottomSheetComponent = forwardRef( flashScrollableIndicators, } = useScrollable(); + // content + const [contentHeight, setContentHeight] = useState(-1); + // normalize snap points const { snapPoints, sheetHeight } = useMemo(() => { - const normalizedSnapPoints = normalizeSnapPoints(_snapPoints, topInset); - const maxSnapPoint = - normalizedSnapPoints[normalizedSnapPoints.length - 1]; + let normalizedSnapPoints = normalizeSnapPoints(_snapPoints, topInset); + if (shouldMeasureContentHeight && contentHeight !== -1) { + normalizedSnapPoints = normalizedSnapPoints.filter( + snapPoint => snapPoint < contentHeight + ); + normalizedSnapPoints.push(contentHeight); + + // reset currentPositionIndexRef to the nearest point + if (currentPositionIndexRef.current >= normalizedSnapPoints.length) { + currentPositionIndexRef.current = normalizedSnapPoints.length - 1; + } + } + const maxSnapPoint = Math.max(...normalizedSnapPoints); + // console.log( + // '_snapPoints', + // _snapPoints, + // 'normalizedSnapPoints', + // normalizedSnapPoints + // ); return { snapPoints: normalizedSnapPoints.map( - normalizedSnapPoint => maxSnapPoint - normalizedSnapPoint + normalizedSnapPoint => windowHeight - topInset - normalizedSnapPoint ), sheetHeight: maxSnapPoint, }; - }, [_snapPoints, topInset]); + }, [_snapPoints, topInset, contentHeight, shouldMeasureContentHeight]); const initialPosition = useMemo(() => { - return initialSnapIndex < 0 ? sheetHeight : snapPoints[initialSnapIndex]; - }, [initialSnapIndex, sheetHeight, snapPoints]); + return currentPositionIndexRef.current < 0 + ? sheetHeight + : snapPoints[currentPositionIndexRef.current]; + }, [sheetHeight, snapPoints]); + // console.log('snapPoints', snapPoints); //#endregion //#region gestures @@ -225,26 +250,24 @@ const BottomSheetComponent = forwardRef( * Scrollable animated props. */ const decelerationRate = useMemo( - () => cond(greaterThan(position, 0), 0.001, NORMAL_DECELERATION_RATE), - [position] + () => + cond( + lessThan(position, snapPoints[snapPoints.length - 1]), + 0.001, + NORMAL_DECELERATION_RATE + ), + [position, snapPoints] ); //#endregion //#region styles - const containerStyle = useMemo( - () => ({ - ...styles.container, - height: sheetHeight, - }), - [sheetHeight] - ); - const contentContainerStyle = useMemo>( + const sheetContainerStyle = useMemo>( () => ({ - ...styles.container, - height: sheetHeight, + ...styles.sheetContainer, + ...(shouldMeasureContentHeight ? {} : { height: sheetHeight }), transform: [{ translateY: position }], }), - [sheetHeight, position] + [sheetHeight, position, shouldMeasureContentHeight] ); //#endregion @@ -276,6 +299,18 @@ const BottomSheetComponent = forwardRef( }, [setScrollableRef, refreshUIElements] ); + const handleContentOnLayout = useCallback( + ({ + nativeEvent: { + layout: { height }, + }, + }) => { + if (shouldMeasureContentHeight) { + setContentHeight(Math.min(height, windowHeight)); + } + }, + [shouldMeasureContentHeight] + ); //#endregion //#region methods @@ -292,8 +327,8 @@ const BottomSheetComponent = forwardRef( [snapPoints, manualSnapToPoint] ); const handleClose = useCallback(() => { - manualSnapToPoint.setValue(sheetHeight); - }, [sheetHeight, manualSnapToPoint]); + manualSnapToPoint.setValue(windowHeight); + }, [manualSnapToPoint]); const handleExpand = useCallback(() => { manualSnapToPoint.setValue(snapPoints[snapPoints.length - 1]); }, [snapPoints, manualSnapToPoint]); @@ -302,7 +337,7 @@ const BottomSheetComponent = forwardRef( }, [snapPoints, manualSnapToPoint]); //#endregion - //#region + //#region context variables const internalContextVariables = useMemo( () => ({ enabled, @@ -340,6 +375,10 @@ const BottomSheetComponent = forwardRef( close: handleClose, })); + // useEffect(() => { + // manualSnapToPoint.setValue(initialPosition); + // }, [manualSnapToPoint, initialPosition]); + /** * @DEV * here we track the current position and @@ -380,13 +419,13 @@ const BottomSheetComponent = forwardRef( and( eq(tapGestureState, State.FAILED), eq(currentGesture, GESTURE.CONTENT), - neq(position, 0) + neq(position, snapPoints[snapPoints.length - 1]) ), call([], () => { scrollToTop(); }) ), - [] + [snapPoints] ); //#endregion @@ -396,10 +435,13 @@ const BottomSheetComponent = forwardRef( - + {BackgroundComponent && ( )} @@ -419,9 +461,7 @@ const BottomSheetComponent = forwardRef( - - {children} - + {children} @@ -429,7 +469,7 @@ const BottomSheetComponent = forwardRef( {_animatedPosition && ( )} diff --git a/src/components/bottomSheet/styles.ts b/src/components/bottomSheet/styles.ts index 4c1601094..1d7aae924 100644 --- a/src/components/bottomSheet/styles.ts +++ b/src/components/bottomSheet/styles.ts @@ -3,12 +3,16 @@ import { StyleSheet } from 'react-native'; export const styles = StyleSheet.create({ container: { position: 'absolute', + top: 0, left: 0, right: 0, bottom: 0, }, - contentContainer: { - flex: 1, + sheetContainer: { + position: 'absolute', + left: 0, + right: 0, + top: 0, }, debug: { position: 'absolute', diff --git a/src/components/bottomSheet/types.d.ts b/src/components/bottomSheet/types.d.ts index ec90b6278..f09c0cddd 100644 --- a/src/components/bottomSheet/types.d.ts +++ b/src/components/bottomSheet/types.d.ts @@ -30,6 +30,12 @@ export interface BottomSheetProps extends BottomSheetAnimationConfigs { * @default true */ enabled?: boolean; + /** + * To messure content height and set it as largest snap point. + * @type boolean + * @default false + */ + shouldMeasureContentHeight?: boolean; /** * Animated value to be used as a callback of the position node internally. * @type Animated.Value diff --git a/src/components/bottomSheet/useTransition.ts b/src/components/bottomSheet/useTransition.ts index 1d74cd669..7f6a2de01 100644 --- a/src/components/bottomSheet/useTransition.ts +++ b/src/components/bottomSheet/useTransition.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import Animated, { eq, set, @@ -91,6 +91,7 @@ export const useTransition = ({ const finishTiming = useMemo( () => [ + // debug('finish timing', config.toValue), set(currentGesture, GESTURE.UNDETERMINED), set(shouldAnimate, 0), set(currentPosition, config.toValue), @@ -114,7 +115,7 @@ export const useTransition = ({ cond( eq(currentGesture, GESTURE.CONTENT), cond( - eq(currentPosition, 0), + eq(currentPosition, snapPoints[snapPoints.length - 1]), add( contentPanGestureTranslationY, multiply(scrollableContentOffsetY, -1) @@ -124,6 +125,7 @@ export const useTransition = ({ handlePanGestureTranslationY ), [ + snapPoints, contentPanGestureTranslationY, currentGesture, currentPosition, @@ -144,6 +146,7 @@ export const useTransition = ({ () => and(clockRunning(clock), or(isPanning, neq(manualSnapToPoint, -1))), [clock, isPanning, manualSnapToPoint] ); + const position = useMemo( () => block([ @@ -153,7 +156,7 @@ export const useTransition = ({ * set current position the the animated position. */ cond(isAnimationInterrupted, [ - // // debug('animation interrupted', isAnimationInterrupted), + // debug('animation interrupted', isAnimationInterrupted), finishTiming, set(currentPosition, animationState.position), ]), @@ -168,8 +171,16 @@ export const useTransition = ({ ), // debug('start panning', translateY), cond( - not(greaterOrEq(add(currentPosition, translateY), 0)), - [set(animationState.position, 0), set(animationState.finished, 0)], + not( + greaterOrEq( + add(currentPosition, translateY), + snapPoints[snapPoints.length - 1] + ) + ), + [ + set(animationState.position, snapPoints[snapPoints.length - 1]), + set(animationState.finished, 0), + ], cond( not(lessOrEq(add(currentPosition, translateY), snapPoints[0])), [ @@ -242,10 +253,30 @@ export const useTransition = ({ animationState.position, ]), - // eslint-disable-next-line react-hooks/exhaustive-deps - [snapPoints] + [ + animationState, + clock, + config, + contentPanGestureState, + currentGesture, + currentPosition, + finishTiming, + handlePanGestureState, + isAnimationInterrupted, + isPanning, + isPanningContent, + manualSnapToPoint, + shouldAnimate, + snapPoints, + translateY, + velocityY, + ] ); + // effects + useEffect(() => { + currentPosition.setValue(initialPosition); + }, [currentPosition, initialPosition]); return { position, manualSnapToPoint, From b46218b00439e79166b19965e512fa265cd444b6 Mon Sep 17 00:00:00 2001 From: Mo Gorhom Date: Wed, 4 Nov 2020 23:22:32 +0100 Subject: [PATCH 2/8] chore: updated examples --- example/ios/Podfile.lock | 2 +- .../components/contactList/ContactList.tsx | 2 +- example/src/components/handle/Handle.tsx | 65 ++++--- .../screens/advanced/CustomHandleExample.tsx | 11 +- example/src/screens/static/BasicExample.tsx | 158 +++++++----------- example/src/screens/static/BasicExamples.tsx | 1 + 6 files changed, 107 insertions(+), 132 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5ad80fb93..ae240e97c 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -481,4 +481,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: aff15ad1e9bd5c910cdc2b575487ce8c56864ae7 -COCOAPODS: 1.9.1 +COCOAPODS: 1.9.3 diff --git a/example/src/components/contactList/ContactList.tsx b/example/src/components/contactList/ContactList.tsx index 6b595d2fd..5a1d77130 100644 --- a/example/src/components/contactList/ContactList.tsx +++ b/example/src/components/contactList/ContactList.tsx @@ -133,7 +133,7 @@ const ContactList = ({ ); } else if (type === 'View') { return ( - + {header && header()} {data.map(renderScrollViewItem)} diff --git a/example/src/components/handle/Handle.tsx b/example/src/components/handle/Handle.tsx index 623f486c2..a0eeacbfe 100644 --- a/example/src/components/handle/Handle.tsx +++ b/example/src/components/handle/Handle.tsx @@ -10,26 +10,42 @@ interface HandleProps extends BottomSheetHandleProps { const Handle: React.FC = ({ style, animatedPositionIndex }) => { //#region animations - const borderTopRadius = interpolate(animatedPositionIndex, { - inputRange: [1, 2], - outputRange: [20, 0], - extrapolate: Extrapolate.CLAMP, - }); - const indicatorTransformOriginY = interpolate(animatedPositionIndex, { - inputRange: [0, 1, 2], - outputRange: [-1, 0, 1], - extrapolate: Extrapolate.CLAMP, - }); - const leftIndicatorRotate = interpolate(animatedPositionIndex, { - inputRange: [0, 1, 2], - outputRange: [toRad(-30), 0, toRad(30)], - extrapolate: Extrapolate.CLAMP, - }); - const rightIndicatorRotate = interpolate(animatedPositionIndex, { - inputRange: [0, 1, 2], - outputRange: [toRad(30), 0, toRad(-30)], - extrapolate: Extrapolate.CLAMP, - }); + const borderTopRadius = useMemo( + () => + interpolate(animatedPositionIndex, { + inputRange: [1, 2], + outputRange: [20, 0], + extrapolate: Extrapolate.CLAMP, + }), + [animatedPositionIndex] + ); + const indicatorTransformOriginY = useMemo( + () => + interpolate(animatedPositionIndex, { + inputRange: [0, 1, 2], + outputRange: [-1, 0, 1], + extrapolate: Extrapolate.CLAMP, + }), + [animatedPositionIndex] + ); + const leftIndicatorRotate = useMemo( + () => + interpolate(animatedPositionIndex, { + inputRange: [0, 1, 2], + outputRange: [toRad(-30), 0, toRad(30)], + extrapolate: Extrapolate.CLAMP, + }), + [animatedPositionIndex] + ); + const rightIndicatorRotate = useMemo( + () => + interpolate(animatedPositionIndex, { + inputRange: [0, 1, 2], + outputRange: [toRad(30), 0, toRad(-30)], + extrapolate: Extrapolate.CLAMP, + }), + [animatedPositionIndex] + ); //#endregion //#region styles @@ -42,8 +58,7 @@ const Handle: React.FC = ({ style, animatedPositionIndex }) => { borderTopRightRadius: borderTopRadius, }, ], - // eslint-disable-next-line react-hooks/exhaustive-deps - [style] + [style, borderTopRadius] ); const leftIndicatorStyle = useMemo( () => ({ @@ -57,8 +72,7 @@ const Handle: React.FC = ({ style, animatedPositionIndex }) => { } ), }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [indicatorTransformOriginY, leftIndicatorRotate] ); const rightIndicatorStyle = useMemo( () => ({ @@ -72,8 +86,7 @@ const Handle: React.FC = ({ style, animatedPositionIndex }) => { } ), }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [indicatorTransformOriginY, rightIndicatorRotate] ); //#endregion diff --git a/example/src/screens/advanced/CustomHandleExample.tsx b/example/src/screens/advanced/CustomHandleExample.tsx index 39f393852..3c091679e 100644 --- a/example/src/screens/advanced/CustomHandleExample.tsx +++ b/example/src/screens/advanced/CustomHandleExample.tsx @@ -15,7 +15,7 @@ const CustomHandleExample = () => { const headerHeight = useHeaderHeight(); // variables - const snapPoints = useMemo(() => [150, 300, 450], []); + const snapPoints = useMemo(() => [100, 200], []); const enableButtonText = useMemo(() => (enabled ? 'Disable' : 'Enable'), [ enabled, ]); @@ -49,17 +49,17 @@ const CustomHandleExample = () => { return (