mirror of
https://github.com/zhigang1992/react-native-bottom-sheet.git
synced 2026-01-12 22:50:12 +08:00
chore: added initial keyboard behavior
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -169,6 +169,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
container: {
|
||||
overflow: 'visible',
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: 24,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
11
src/components/bottomSheet/types.d.ts
vendored
11
src/components/bottomSheet/types.d.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user