chore: added initial keyboard behavior

This commit is contained in:
Mo Gorhom
2021-03-05 22:50:43 +00:00
parent f115115f22
commit 4e18cb8939
13 changed files with 411 additions and 191 deletions

View File

@@ -13,10 +13,9 @@ import Animated, {
useAnimatedStyle,
useSharedValue,
} from 'react-native-reanimated';
import { TextInput } from 'react-native-gesture-handler';
import { useSafeArea } from 'react-native-safe-area-context';
import BottomSheet from '@gorhom/bottom-sheet';
import BottomSheetKeyboardView from '../../src/components/bottomSheetKeyboardView';
import SearchHandle from './components/searchHandle'
import Button from './components/button';
import ContactList from './components/contactList';
@@ -106,7 +105,7 @@ const BasicExample = () => {
// renders
return (
<View style={containerStyle}>
<Button
{/* <Button
label="Increase Dynamic Snap Point"
style={styles.buttonContainer}
onPress={handleIncreaseDynamicSnapPoint}
@@ -115,7 +114,7 @@ const BasicExample = () => {
label="Snap To 150"
style={styles.buttonContainer}
onPress={() => handleSnapPress(0)}
/>
/> */}
<Button
label="Close"
style={styles.buttonContainer}
@@ -127,12 +126,14 @@ const BasicExample = () => {
snapPoints={snapPoints}
animateOnMount={true}
animatedPosition={animatedPosition}
keyboardBehavior="fullScreen"
handleComponent={SearchHandle}
containerHeight={windowHeight}
topInset={StatusBar.currentHeight || topSafeArea}
onChange={handleSheetChanges}
>
{/* <ContactList type="ScrollView" count={15} /> */}
<View
<ContactList type="FlatList" count={20} />
{/* <View
style={{
height: dynamicSnapPoint,
}}
@@ -154,12 +155,12 @@ const BasicExample = () => {
backgroundColor: 'red',
}}
/>
</View>
</View> */}
</BottomSheet>
<Animated.View pointerEvents="none" style={sheetLineStyle} />
{/* <Animated.View pointerEvents="none" style={sheetLineStyle} />
<View pointerEvents="none" style={secondSnapPointLineStyle} />
<View pointerEvents="none" style={firstSnapPointLineStyle} />
<View pointerEvents="none" style={safeBottomLineStyle} />
<View pointerEvents="none" style={safeBottomLineStyle} /> */}
</View>
);
};
@@ -174,8 +175,11 @@ const styles = StyleSheet.create({
marginBottom: 6,
},
textInput: {
backgroundColor: 'blue',
backgroundColor: 'red',
opacity: 1,
padding: 6,
margin: 6,
borderRadius: 24,
},
line: {
position: 'absolute',

View File

@@ -169,6 +169,7 @@ const styles = StyleSheet.create({
},
container: {
overflow: 'visible',
flex: 1,
},
contentContainer: {
paddingHorizontal: 24,

View File

@@ -6,7 +6,6 @@ import {
NativeSyntheticEvent,
TextInputChangeEventData,
} from 'react-native';
import { useBottomSheet } from '@gorhom/bottom-sheet';
import { TextInput } from 'react-native-gesture-handler';
import isEqual from 'lodash.isequal';
import { useAppearance } from '../../hooks';
@@ -19,7 +18,6 @@ const BottomSheetHandleComponent = () => {
const [value, setValue] = useState('');
// hooks
const { snapTo } = useBottomSheet();
const { appearance } = useAppearance();
// styles
@@ -45,9 +43,6 @@ const BottomSheetHandleComponent = () => {
},
[]
);
const handleInputFocus = useCallback(() => {
snapTo(2);
}, [snapTo]);
// render
return (
@@ -59,7 +54,6 @@ const BottomSheetHandleComponent = () => {
textContentType="location"
placeholder="Search for a place or address"
onChange={handleInputChange}
onFocus={handleInputFocus}
/>
</View>
);

View File

@@ -49,6 +49,9 @@ import {
WINDOW_HEIGHT,
KEYBOARD_STATE,
KEYBOARD_EASING_MAPPER,
KEYBOARD_BEHAVIOR,
SHEET_STATE,
SCROLLABLE_STATE,
} from '../../constants';
import {
DEFAULT_ANIMATION_EASING,
@@ -61,6 +64,7 @@ import {
DEFAULT_ENABLE_FLASH_SCROLLABLE_INDICATOR_ON_EXPAND,
DEFAULT_ANIMATE_ON_MOUNT,
DECELERATION_RATE,
DEFAULT_KEYBOARD_BEHAVIOR,
} from './constants';
import type { ScrollableRef, BottomSheetMethods } from '../../types';
import type { BottomSheetProps } from './types';
@@ -93,6 +97,7 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
enableHandlePanningGesture = DEFAULT_ENABLE_HANDLE_PANNING_GESTURE,
enableOverDrag = DEFAULT_ENABLE_OVER_DRAG,
enableFlashScrollableIndicatorOnExpand = DEFAULT_ENABLE_FLASH_SCROLLABLE_INDICATOR_ON_EXPAND,
keyboardBehavior = DEFAULT_KEYBOARD_BEHAVIOR,
style: _providedStyle,
// layout
@@ -178,13 +183,15 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
// scrollable variables
const {
scrollableContentOffsetY,
scrollableDecelerationRate,
// scrollableDecelerationRate,
setScrollableRef,
removeScrollableRef,
flashScrollableIndicators,
} = useScrollable();
// keyboard
const isExtendedByKeyboard = useSharedValue(false);
const animatedKeyboardOffset = useSharedValue(0);
const {
state: keyboardState,
height: keyboardHeight,
@@ -273,24 +280,32 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
//#region gesture interaction / animation
// variables
const animatedKeyboardOffset = useDerivedValue(
() =>
withTiming(
keyboardState.value === KEYBOARD_STATE.SHOWN
? keyboardHeight.value * -1
: 0,
{
duration: keyboardAnimationDuration.value,
easing: KEYBOARD_EASING_MAPPER[keyboardAnimationEasing.value],
}
),
[
keyboardState,
keyboardHeight,
keyboardAnimationDuration,
keyboardAnimationEasing,
]
);
const animatedSheetHeight = useDerivedValue(() => {
if (!isExtendedByKeyboard.value) {
return sheetHeight;
}
if (
keyboardBehavior === KEYBOARD_BEHAVIOR.none ||
keyboardBehavior === KEYBOARD_BEHAVIOR.extend
) {
return sheetHeight;
}
if (keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen) {
return safeContainerHeight - topInset - safeHandleHeight;
}
return (
sheetHeight + (isExtendedByKeyboard.value ? keyboardHeight.value : 0)
);
}, [
sheetHeight,
keyboardBehavior,
safeContainerHeight,
topInset,
safeHandleHeight,
]);
const animationState = useSharedValue(ANIMATION_STATE.UNDETERMINED);
const animatedSnapPoints = useReactiveSharedValue(snapPoints);
const animatedPosition = useSharedValue(initialPosition);
@@ -316,6 +331,22 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
)
: -1;
}, [snapPoints, safeContainerHeight, isLayoutCalculated]);
const animatedSheetState = useDerivedValue(() => {
const extendedSheetPosition =
safeContainerHeight - safeHandleHeight - sheetHeight;
if (animatedPosition.value >= safeContainerHeight) {
return SHEET_STATE.CLOSED;
} else if (animatedPosition.value === extendedSheetPosition) {
return SHEET_STATE.EXTENDED;
} else if (animatedPosition.value === topInset) {
return SHEET_STATE.FULL_SCREEN;
} else if (animatedPosition.value < extendedSheetPosition) {
return SHEET_STATE.OVER_EXTENDED;
}
return SHEET_STATE.OPENED;
});
// callbacks
const animateToPointCompleted = useWorkletCallback(() => {
@@ -358,31 +389,46 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
]
);
const scrollableState = useDerivedValue(() => {
return animatedSheetState.value === SHEET_STATE.FULL_SCREEN ||
animatedSheetState.value === SHEET_STATE.EXTENDED
? SCROLLABLE_STATE.UNLOCKED
: SCROLLABLE_STATE.LOCKED;
});
// hooks
const [
contentPanGestureHandler,
contentPanGestureState,
] = useInteractivePanGestureHandler(
GESTURE.CONTENT,
animatedPosition,
animatedSnapPoints,
animateToPoint,
] = useInteractivePanGestureHandler({
type: GESTURE.CONTENT,
enableOverDrag,
overDragResistanceFactor,
scrollableContentOffsetY
);
keyboardState,
keyboardHeight,
keyboardBehavior: keyboardBehavior,
animatedPosition,
animatedSnapPoints,
isExtendedByKeyboard,
scrollableContentOffsetY,
animateToPoint,
});
const [
handlePanGestureHandler,
handlePanGestureState,
] = useInteractivePanGestureHandler(
GESTURE.HANDLE,
] = useInteractivePanGestureHandler({
type: GESTURE.HANDLE,
enableOverDrag,
overDragResistanceFactor,
keyboardState,
keyboardHeight,
keyboardBehavior,
animatedPosition,
animatedSnapPoints,
isExtendedByKeyboard,
animateToPoint,
enableOverDrag,
overDragResistanceFactor
);
});
//#endregion
//#region layout callbacks
@@ -457,9 +503,10 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
animatedIndex,
animatedPosition,
animationState,
animatedSheetState,
contentPanGestureHandler,
scrollableState,
scrollableContentOffsetY,
scrollableDecelerationRate,
simultaneousHandlers: _providedSimultaneousHandlers,
waitFor: _providedWaitFor,
activeOffsetX: _providedActiveOffsetX,
@@ -474,11 +521,12 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
animatedIndex,
animatedPosition,
animationState,
animatedSheetState,
contentPanGestureHandler,
handleSettingScrollableRef,
removeScrollableRef,
scrollableState,
scrollableContentOffsetY,
scrollableDecelerationRate,
enableContentPanningGesture,
_providedSimultaneousHandlers,
_providedWaitFor,
@@ -531,13 +579,16 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
() => [_providedStyle, styles.container, containerAnimatedStyle],
[_providedStyle, containerAnimatedStyle]
);
const contentContainerStyle = useMemo<ViewStyle>(
const contentContainerAnimatedStyle = useAnimatedStyle(
() => ({
...styles.contentContainer,
height: sheetHeight,
height: animatedSheetHeight.value,
}),
[sheetHeight]
);
const contentContainerStyle = useMemo(
() => [styles.contentContainer, contentContainerAnimatedStyle],
[contentContainerAnimatedStyle]
);
/**
* added safe area to prevent the sheet from floating above
@@ -593,22 +644,74 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
}
}, [isLayoutCalculated, snapPoints, animateToPoint]);
/**
* handle keyboard appearance behavior
*/
useAnimatedReaction(
() => keyboardState.value,
(state, previousState) => {
if (state === previousState) {
return;
}
/**
* Handle extend behavior
*/
if (
keyboardBehavior === KEYBOARD_BEHAVIOR.extend &&
state === KEYBOARD_STATE.SHOWN
) {
const newSnapPoint = snapPoints[snapPoints.length - 1];
animateToPoint(newSnapPoint);
return;
}
/**
* Handle full screen behavior
*/
if (
keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen &&
state === KEYBOARD_STATE.SHOWN
) {
isExtendedByKeyboard.value = true;
animateToPoint(topInset);
return;
}
// if (
// state === KEYBOARD_STATE.SHOWN &&
// keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen
// ) {
// animateToPoint(topInset);
// } else if (keyboardBehavior === KEYBOARD_BEHAVIOR.interactive) {
// animatedKeyboardOffset.value = withTiming(
// state === KEYBOARD_STATE.SHOWN ? keyboardHeight.value * -1 : 0,
// {
// duration: keyboardAnimationDuration.value,
// easing: KEYBOARD_EASING_MAPPER[keyboardAnimationEasing.value],
// }
// );
// }
},
[snapPoints, keyboardBehavior, topInset, animateToPoint]
);
/**
* set scrollable deceleration rate based on sheet
* position.
*/
useAnimatedReaction(
() => animatedIndex.value === snapPoints.length - 1,
(shouldNormalizeDecelerationRate: boolean) => {
const newDecelerationRate = shouldNormalizeDecelerationRate
? DECELERATION_RATE
: 0;
if (scrollableDecelerationRate.value !== newDecelerationRate) {
scrollableDecelerationRate.value = newDecelerationRate;
}
},
[snapPoints.length]
);
// useAnimatedReaction(
// () => [animatedSheetState, animatedPosition],
// ([_state, _position]) => {
// // const newDecelerationRate = shouldNormalizeDecelerationRate
// // ? DECELERATION_RATE
// // : 0;
// // if (scrollableDecelerationRate.value !== newDecelerationRate) {
// // scrollableDecelerationRate.value = newDecelerationRate;
// // }
// },
// [snapPoints.length]
// );
/**
* sets provided animated position
@@ -668,7 +771,14 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
//#endregion
// render
// console.log('BottomSheet', 'render', snapPoints, sheetHeight);
console.log(
'BottomSheet',
'render',
snapPoints,
safeContainerHeight,
safeHandleHeight,
sheetHeight
);
return (
<BottomSheetProvider value={externalContextVariables}>
<BottomSheetBackdropContainer
@@ -728,10 +838,12 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
values={{
animatedIndex,
animatedPosition,
contentPanGestureState,
keyboardState,
keyboardHeight,
keyboardAnimationDuration,
animatedSheetHeight,
animatedSheetState,
scrollableState,
}}
/>
</BottomSheetContainer>

View File

@@ -1,5 +1,6 @@
import { Platform } from 'react-native';
import Animated, { Easing } from 'react-native-reanimated';
import { KEYBOARD_BEHAVIOR } from '../../constants';
import { exp } from '../../utilities/easingExp';
const DEFAULT_ANIMATION_EASING: Animated.EasingFunction = Easing.out(exp);
@@ -13,6 +14,9 @@ const DEFAULT_ENABLE_OVER_DRAG = true;
const DEFAULT_ENABLE_FLASH_SCROLLABLE_INDICATOR_ON_EXPAND = true;
const DEFAULT_ANIMATE_ON_MOUNT = false;
const DEFAULT_KEYBOARD_BEHAVIOR =
KEYBOARD_BEHAVIOR.none;
const DECELERATION_RATE = Platform.select({
ios: 0.998,
android: 0.985,
@@ -29,5 +33,6 @@ export {
DEFAULT_ENABLE_OVER_DRAG,
DEFAULT_ENABLE_FLASH_SCROLLABLE_INDICATOR_ON_EXPAND,
DEFAULT_ANIMATE_ON_MOUNT,
DEFAULT_KEYBOARD_BEHAVIOR,
DECELERATION_RATE,
};

View File

@@ -5,6 +5,7 @@ import type { PanGestureHandlerProps } from 'react-native-gesture-handler';
import type { BottomSheetHandleProps } from '../bottomSheetHandle';
import type { BottomSheetBackdropProps } from '../bottomSheetBackdrop';
import type { BottomSheetBackgroundProps } from '../bottomSheetBackground';
import type { KEYBOARD_BEHAVIOR } from '../../constants';
export interface BottomSheetProps
extends BottomSheetAnimationConfigs,
@@ -92,6 +93,16 @@ export interface BottomSheetProps
* @default false
*/
animateOnMount?: boolean;
/**
* Defines the keyboard appearance behavior.
* - `none`: do nothing.
* - `extend`: extend the sheet to its maximum snap point.
* - `fullScreen`: extend the sheet to full screen.
* - `interactive`: offset the sheet by the size of the keyboard.
* @type `none` | `extend` | `interactive`
* @default none
*/
keyboardBehavior?: keyof typeof KEYBOARD_BEHAVIOR;
/**
* View style to be applied at the sheet container,

View File

@@ -3,8 +3,8 @@ import { StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
container: {
position: 'absolute',
left: 4,
top: 4,
right: 4,
top: 200,
padding: 2,
backgroundColor: 'rgba(0, 0,0,0.75)',
},

View File

@@ -5,6 +5,7 @@ import { PanGestureHandler } from 'react-native-gesture-handler';
import { useBottomSheetInternal } from '../../hooks';
import type { BottomSheetDraggableViewProps } from './types';
import { styles } from './styles';
import { StyleSheet } from 'react-native';
const BottomSheetDraggableViewComponent = ({
nativeGestureRef,
@@ -47,10 +48,17 @@ const BottomSheetDraggableViewComponent = ({
}, [_providedSimultaneousHandlers, nativeGestureRef]);
// styles
const containerStyle = useMemo(
() => (style ? [styles.container, style] : styles.container),
[style]
);
const containerStyle = useMemo(() => {
if (!style) {
return styles.container;
}
if (Array.isArray(style)) {
return [styles.container, ...style];
}
return [styles.container, style];
}, [style]);
return (
<PanGestureHandler

View File

@@ -1,4 +1,4 @@
import { Dimensions } from 'react-native';
import { Dimensions, Platform } from 'react-native';
import { Easing } from 'react-native-reanimated';
const { height: WINDOW_HEIGHT, width: WINDOW_WIDTH } = Dimensions.get('window');
@@ -9,6 +9,19 @@ enum GESTURE {
HANDLE,
}
enum SHEET_STATE {
CLOSED = 0,
OPENED,
EXTENDED,
OVER_EXTENDED,
FULL_SCREEN,
}
enum SCROLLABLE_STATE {
LOCKED = 0,
UNLOCKED,
}
enum ANIMATION_STATE {
UNDETERMINED = 0,
RUNNING,
@@ -21,11 +34,27 @@ enum KEYBOARD_STATE {
HIDDEN,
}
const SCROLLABLE_DECELERATION_RATE_MAPPER = {
[SCROLLABLE_STATE.LOCKED]: 0,
[SCROLLABLE_STATE.UNLOCKED]: Platform.select({
ios: 0.998,
android: 0.985,
default: 1,
}),
};
const MODAL_STACK_BEHAVIOR = {
replace: 'replace',
push: 'push',
};
const KEYBOARD_BEHAVIOR = {
none: 'none',
extend: 'extend',
fullScreen: 'fullScreen',
interactive: 'interactive',
} as const;
const KEYBOARD_EASING_MAPPER = {
easeIn: Easing.in(Easing.ease),
easeOut: Easing.out(Easing.ease),
@@ -36,10 +65,14 @@ const KEYBOARD_EASING_MAPPER = {
export {
GESTURE,
SHEET_STATE,
ANIMATION_STATE,
SCROLLABLE_STATE,
KEYBOARD_STATE,
WINDOW_HEIGHT,
WINDOW_WIDTH,
SCROLLABLE_DECELERATION_RATE_MAPPER,
MODAL_STACK_BEHAVIOR,
KEYBOARD_BEHAVIOR,
KEYBOARD_EASING_MAPPER,
};

View File

@@ -1,7 +1,11 @@
import { createContext, RefObject } from 'react';
import type { PanGestureHandlerProps } from 'react-native-gesture-handler';
import type Animated from 'react-native-reanimated';
import type { ANIMATION_STATE } from '../constants';
import type {
ANIMATION_STATE,
SCROLLABLE_STATE,
SHEET_STATE,
} from '../constants';
import type { Scrollable, ScrollableRef } from '../types';
export interface BottomSheetInternalContextType
@@ -19,7 +23,9 @@ export interface BottomSheetInternalContextType
animatedPosition: Animated.SharedValue<number>;
animatedIndex: Animated.SharedValue<number>;
animationState: Animated.SharedValue<ANIMATION_STATE>;
animatedSheetState: Animated.SharedValue<SHEET_STATE>;
contentPanGestureHandler: any;
scrollableState: Animated.SharedValue<SCROLLABLE_STATE>;
scrollableContentOffsetY: Animated.SharedValue<number>;
scrollableDecelerationRate: Animated.SharedValue<number>;
setScrollableRef: (ref: ScrollableRef) => void;

View File

@@ -10,21 +10,40 @@ import {
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import { clamp, snapPoint } from 'react-native-redash';
import { GESTURE } from '../constants';
import { GESTURE, KEYBOARD_BEHAVIOR, KEYBOARD_STATE } from '../constants';
interface useInteractivePanGestureHandlerConfigs {
type: GESTURE;
enableOverDrag: boolean;
overDragResistanceFactor: number;
isExtendedByKeyboard: Animated.SharedValue<boolean>;
keyboardState: Animated.SharedValue<KEYBOARD_STATE>;
keyboardHeight: Animated.SharedValue<number>;
keyboardBehavior: keyof typeof KEYBOARD_BEHAVIOR;
animatedSnapPoints: Animated.SharedValue<number[]>;
animatedPosition: Animated.SharedValue<number>;
scrollableContentOffsetY?: Animated.SharedValue<number>;
animateToPoint: (point: number, velocity: number) => void;
}
type InteractivePanGestureHandlerContextType = {
currentPosition: number;
keyboardState: KEYBOARD_STATE;
};
export const useInteractivePanGestureHandler = (
type: GESTURE,
animatedPosition: Animated.SharedValue<number>,
snapPoints: Animated.SharedValue<number[]>,
animateToPoint: (point: number, velocity: number) => void,
enableOverDrag: boolean,
overDragResistanceFactor: number,
scrollableContentOffsetY?: Animated.SharedValue<number>
): [
export const useInteractivePanGestureHandler = ({
type,
enableOverDrag,
overDragResistanceFactor,
keyboardState,
keyboardHeight,
keyboardBehavior,
isExtendedByKeyboard,
animatedPosition,
animatedSnapPoints,
scrollableContentOffsetY,
animateToPoint,
}: useInteractivePanGestureHandlerConfigs): [
(event: PanGestureHandlerGestureEvent) => void,
Animated.SharedValue<State>,
Animated.SharedValue<number>,
@@ -37,108 +56,127 @@ export const useInteractivePanGestureHandler = (
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
InteractivePanGestureHandlerContextType
>(
{
onStart: ({ state, translationY, velocityY }, context) => {
// cancel current animation
cancelAnimation(animatedPosition);
>({
onStart: ({ state, translationY, velocityY }, context) => {
// cancel current animation
cancelAnimation(animatedPosition);
// store current animated position
context.currentPosition = animatedPosition.value;
// store current animated position
context.currentPosition = animatedPosition.value;
context.keyboardState = keyboardState.value;
// set variables
gestureState.value = state;
gestureTranslationY.value = translationY;
gestureVelocityY.value = velocityY;
},
onActive: ({ state, translationY, velocityY }, context) => {
gestureState.value = state;
gestureTranslationY.value = translationY;
gestureVelocityY.value = velocityY;
if (
keyboardState.value === KEYBOARD_STATE.SHOWN &&
(keyboardBehavior === KEYBOARD_BEHAVIOR.interactive ||
keyboardBehavior === KEYBOARD_BEHAVIOR.fullScreen)
) {
isExtendedByKeyboard.value = true;
}
runOnJS(Keyboard.dismiss)();
const position = context.currentPosition + translationY;
const negativeScrollableContentOffset =
context.currentPosition ===
snapPoints.value[snapPoints.value.length - 1] &&
scrollableContentOffsetY
? scrollableContentOffsetY.value * -1
: 0;
const clampedPosition = clamp(
position + negativeScrollableContentOffset,
snapPoints.value[snapPoints.value.length - 1],
snapPoints.value[0]
);
if (enableOverDrag) {
if (
type === GESTURE.HANDLE &&
position <= snapPoints.value[snapPoints.value.length - 1]
) {
const resistedPosition =
snapPoints.value[snapPoints.value.length - 1] -
Math.sqrt(
1 + (snapPoints.value[snapPoints.value.length - 1] - position)
) *
overDragResistanceFactor;
animatedPosition.value = resistedPosition;
return;
}
if (position >= snapPoints.value[0]) {
const resistedPosition =
snapPoints.value[0] +
Math.sqrt(1 + (position - snapPoints.value[0])) *
overDragResistanceFactor;
animatedPosition.value = resistedPosition;
return;
}
}
animatedPosition.value = clampedPosition;
},
onEnd: ({ state }, context) => {
gestureState.value = state;
const destinationPoint = snapPoint(
gestureTranslationY.value + context.currentPosition,
gestureVelocityY.value,
snapPoints.value
);
/**
* if destination point is the same as the current position,
* then no need to perform animation.
*/
if (destinationPoint === animatedPosition.value) {
return;
}
if (
(scrollableContentOffsetY ? scrollableContentOffsetY.value : 0) > 0 &&
context.currentPosition ===
snapPoints.value[snapPoints.value.length - 1] &&
animatedPosition.value ===
snapPoints.value[snapPoints.value.length - 1]
) {
return;
}
animateToPoint(destinationPoint, gestureVelocityY.value / 2);
},
onCancel: ({ state }) => {
gestureState.value = state;
},
onFail: ({ state }) => {
gestureState.value = state;
},
onFinish: ({ state }) => {
gestureState.value = state;
},
// set variables
gestureState.value = state;
gestureTranslationY.value = translationY;
gestureVelocityY.value = velocityY;
},
[snapPoints, enableOverDrag, overDragResistanceFactor]
);
onActive: ({ state, translationY, velocityY }, context) => {
gestureState.value = state;
gestureTranslationY.value = translationY;
gestureVelocityY.value = velocityY;
runOnJS(Keyboard.dismiss)();
const position = context.currentPosition + translationY;
const maxSnapPoint = isExtendedByKeyboard.value
? context.currentPosition
: animatedSnapPoints.value[animatedSnapPoints.value.length - 1];
const negativeScrollableContentOffset =
context.currentPosition === maxSnapPoint && scrollableContentOffsetY
? scrollableContentOffsetY.value * -1
: 0;
const clampedPosition = clamp(
position + negativeScrollableContentOffset,
maxSnapPoint,
animatedSnapPoints.value[0]
);
if (enableOverDrag) {
if (type === GESTURE.HANDLE && position <= maxSnapPoint) {
const resistedPosition =
maxSnapPoint -
Math.sqrt(1 + (maxSnapPoint - position)) * overDragResistanceFactor;
animatedPosition.value = resistedPosition;
return;
}
if (position >= animatedSnapPoints.value[0]) {
const resistedPosition =
animatedSnapPoints.value[0] +
Math.sqrt(1 + (position - animatedSnapPoints.value[0])) *
overDragResistanceFactor;
animatedPosition.value = resistedPosition;
return;
}
}
animatedPosition.value = clampedPosition;
},
onEnd: ({ state }, context) => {
gestureState.value = state;
/**
*
*/
console.log(
'currentPosition',
context.currentPosition,
'animatedPosition',
animatedPosition.value
);
if (
isExtendedByKeyboard.value &&
context.currentPosition >= animatedPosition.value
) {
return;
}
isExtendedByKeyboard.value = false;
const destinationPoint = snapPoint(
gestureTranslationY.value + context.currentPosition,
gestureVelocityY.value,
animatedSnapPoints.value
);
/**
* if destination point is the same as the current position,
* then no need to perform animation.
*/
if (destinationPoint === animatedPosition.value) {
return;
}
if (
(scrollableContentOffsetY ? scrollableContentOffsetY.value : 0) > 0 &&
context.currentPosition ===
animatedSnapPoints.value[animatedSnapPoints.value.length - 1] &&
animatedPosition.value ===
animatedSnapPoints.value[animatedSnapPoints.value.length - 1]
) {
return;
}
animateToPoint(destinationPoint, gestureVelocityY.value / 2);
},
onCancel: ({ state }) => {
gestureState.value = state;
},
onFail: ({ state }) => {
gestureState.value = state;
},
onFinish: ({ state }) => {
gestureState.value = state;
},
});
return [gestureHandler, gestureState, gestureTranslationY, gestureVelocityY];
};

View File

@@ -1,6 +1,7 @@
import { useCallback, RefObject, useRef } from 'react';
import { findNodeHandle, Platform } from 'react-native';
import { useSharedValue } from 'react-native-reanimated';
import { SCROLLABLE_STATE } from '../constants';
import type { ScrollableRef, Scrollable } from '../types';
export const useScrollable = () => {
@@ -9,6 +10,9 @@ export const useScrollable = () => {
const previousScrollableRef = useRef<ScrollableRef>(null);
// variables
const scrollableState = useSharedValue<SCROLLABLE_STATE>(
SCROLLABLE_STATE.LOCKED
);
const scrollableContentOffsetY = useSharedValue<number>(0);
const scrollableDecelerationRate = useSharedValue<number>(0);
@@ -77,6 +81,7 @@ export const useScrollable = () => {
return {
scrollableRef,
scrollableState,
scrollableContentOffsetY,
scrollableDecelerationRate,
setScrollableRef,

View File

@@ -10,7 +10,11 @@ import {
} from 'react-native-reanimated';
import { useBottomSheetInternal } from './useBottomSheetInternal';
import type { Scrollable, ScrollableType } from '../types';
import { ANIMATION_STATE } from '../constants';
import {
ANIMATION_STATE,
SCROLLABLE_DECELERATION_RATE_MAPPER,
SCROLLABLE_STATE,
} from '../constants';
export const useScrollableInternal = (type: ScrollableType) => {
// refs
@@ -21,10 +25,8 @@ export const useScrollableInternal = (type: ScrollableType) => {
// hooks
const {
snapPointsCount,
scrollableState,
animationState,
animatedIndex,
scrollableDecelerationRate,
scrollableContentOffsetY: _rootScrollableContentOffsetY,
setScrollableRef,
removeScrollableRef,
@@ -32,13 +34,14 @@ export const useScrollableInternal = (type: ScrollableType) => {
// variables
const scrollableAnimatedProps = useAnimatedProps(() => ({
decelerationRate: scrollableDecelerationRate.value,
decelerationRate:
SCROLLABLE_DECELERATION_RATE_MAPPER[scrollableState.value],
}));
// callbacks
const handleScrollEvent = useAnimatedScrollHandler({
onBeginDrag: ({ contentOffset: { y } }: NativeScrollEvent) => {
if (animatedIndex.value !== snapPointsCount - 1) {
if (scrollableState.value === SCROLLABLE_STATE.LOCKED) {
initialScrollingPosition.value = y;
justStartedScrolling.value = 1;
scrollableContentOffsetY.value = 0;
@@ -55,7 +58,7 @@ export const useScrollableInternal = (type: ScrollableType) => {
scrollTo(scrollableRef, 0, initialScrollingPosition.value, false);
return;
}
if (animatedIndex.value !== snapPointsCount - 1) {
if (scrollableState.value === SCROLLABLE_STATE.LOCKED) {
// @ts-ignore
scrollTo(scrollableRef, 0, 0, false);
scrollableContentOffsetY.value = 0;
@@ -63,7 +66,7 @@ export const useScrollableInternal = (type: ScrollableType) => {
}
},
onEndDrag: ({ contentOffset: { y } }: NativeScrollEvent) => {
if (animatedIndex.value !== snapPointsCount - 1) {
if (scrollableState.value === SCROLLABLE_STATE.LOCKED) {
// @ts-ignore
scrollTo(scrollableRef, 0, 0, false);
scrollableContentOffsetY.value = 0;
@@ -75,7 +78,7 @@ export const useScrollableInternal = (type: ScrollableType) => {
}
},
onMomentumEnd: ({ contentOffset: { y } }: NativeScrollEvent) => {
if (animatedIndex.value !== snapPointsCount - 1) {
if (scrollableState.value === SCROLLABLE_STATE.LOCKED) {
// @ts-ignore
scrollTo(scrollableRef, 0, 0, false);
scrollableContentOffsetY.value = 0;