react-native-swipeable-item
Version:
A horizontally swipeable flatlist row compatible with react-native-draggable-flatlist
290 lines (249 loc) • 12.3 kB
Markdown
# React Native Swipeable Item
A swipeable component with underlay for React Native.<br />
Fully native interactions powered by [Reanimated](https://github.com/kmagiera/react-native-reanimated) and [React Native Gesture Handler](https://github.com/kmagiera/react-native-gesture-handler)
Compatible with [React Native Draggable Flatlist](https://github.com/computerjazz/react-native-draggable-flatlist)

## Install
1. Follow installation instructions for [reanimated](https://github.com/kmagiera/react-native-reanimated) and [react-native-gesture-handler](https://github.com/kmagiera/react-native-gesture-handler)
2. `npm install` or `yarn add` `react-native-swipeable-item`
3. `import SwipeableItem from 'react-native-swipeable-item'`
### Props
_NOTE:_ Naming is hard. When you swipe _right_, you reveal the item on the _left_. So what do you name these things? I have decided to name everything according to swipe direction. Therefore, a swipe left reveals the `renderUnderlayLeft()` component with width `underlayWidthLeft`. Not perfect but it works.
```ts
type OpenCloseOptions = { animated?: boolean };
type OpenPromiseFn = (
snapPoint?: number,
options?: OpenCloseOptions
) => Promise<void>;
type ClosePromiseFn = (options?: OpenCloseOptions) => Promise<void>;
export type UnderlayParams<T> = {
item: T;
open: OpenPromiseFn;
close: ClosePromiseFn;
percentOpen: Animated.DerivedValue<number>;
isGestureActive: Animated.DerivedValue<boolean>;
direction: OpenDirection;
};
export type OverlayParams<T> = {
item: T;
openLeft: OpenPromiseFn;
openRight: OpenPromiseFn;
close: ClosePromiseFn;
openDirection: OpenDirection;
percentOpenLeft: Animated.DerivedValue<number>;
percentOpenRight: Animated.DerivedValue<number>;
};
```
| Name | Type | Description |
| :-------------------- | :---------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `renderUnderlayLeft` | `RenderUnderlay` | Component to be rendered underneath row on left swipe. |
| `renderUnderlayRight` | `RenderUnderlay` | Component to be rendered underneath row on right swipe. |
| `snapPointsLeft` | `number[]` | Pixel values left-swipe snaps to (eg. `[100, 300]`) |
| `snapPointsRight` | `number[]` | Pixel values right-swipe snaps to (eg. `[100, 300]`) |
| `renderOverlay` | `RenderOverlay` | Component to be rendered on top. Use if you need access to programmatic open/close methods. May altenatively pass children to SwipeableItem. |
| `onChange` | `(params: { openDirection: OpenDirection, snapPoint: number }) => void` | Called when row is opened or closed. |
| `swipeEnabled` | `boolean` | Enable/disable swipe. Defaults to `true`. |
| `activationThreshold` | `number` | Distance finger must travel before swipe engages. Defaults to 20. |
| `swipeDamping` | `number` | How much swipe velocity determines snap position. A smaller number means swipe velocity will have a larger effect and row will swipe open more easily. Defaults to `10`. |
### Hooks
| Name | Type | Description |
| :----------------------- | :--------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `useSwipeableItemParams` | `() => OverlayParams<T> & { open: OpenPromiseFn, percentOpen: Animated.DerivedValue<number> }` | Utility hook that reutrns the same params as the render functions are called with. `open()` and `percentOpen` params reflect the context in which the hook is called (i.e. within an underlay or overlay component). |
| |
```tsx
function MyUnderlayComponent() {
// Underlay components "know" which direction to open, so we don't need to call `openLeft()` or `openRight()`, we can just call 'open()'
// Underlay components also receive the `percentOpen` value of their own direction (`percentOpenLeft` or `percentOpenRight`)
const swipeableItemParams = useSwipeableItemParams();
return <TouchableOpacity onPress={swipeableItemParams.open} />;
}
function MyOverlayComponent() {
// Overlay components get the same params, but have defaults filled in for `open()` and `percentOpen` params.
const swipeableItemParams = useSwipeableItemParams();
return <TouchableOpacity onPress={swipeableItemParams.openLeft} />;
}
```
### Instance Methods
| Name | Type | Description |
| :------ | :------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------- |
| `open` | `(OpenDirection.LEFT \| OpenDirection.RIGHT, snapIndex?: number, options?: { animated: boolean }) => Promise<void>` | Imperatively open left or right. Promise resolves once open. |
| `close` | `(options?: { animated?: boolean}) => Promise<void>` | Close all. Promise resolves once closed. |
```tsx
// Imperative open example
const itemRef: SwipeableItem | null = null
...
<SwipeableItem ref={ref => itemRef = ref} />
...
if (itemRef) itemRef.open(OpenDirection.LEFT)
```
### Notes
Gesture handlers can sometimes capture a gesture unintentionally. If you are using with `react-native-draggable-flatlist` and the list is periodically not scrolling, try adding a small `activationDistance` (see example below).
### Example
https://snack.expo.io/@computerjazz/swipeable-draggable-list
```typescript
import React, { useState, useRef, useCallback } from "react";
import {
Text,
View,
StyleSheet,
FlatList,
LayoutAnimation,
TouchableOpacity,
Platform,
UIManager,
} from "react-native";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
import SwipeableItem, {
useSwipeableItemParams,
OpenDirection,
} from "react-native-swipeable-item";
import DraggableFlatList, {
RenderItemParams,
ScaleDecorator,
} from "react-native-draggable-flatlist";
const { multiply, sub } = Animated;
if (Platform.OS === "android") {
UIManager.setLayoutAnimationEnabledExperimental &&
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const OVERSWIPE_DIST = 20;
const NUM_ITEMS = 20;
function getColor(i: number) {
const multiplier = 255 / (NUM_ITEMS - 1);
const colorVal = i * multiplier;
return `rgb(${colorVal}, ${Math.abs(128 - colorVal)}, ${255 - colorVal})`;
}
type Item = {
key: string;
text: string;
backgroundColor: string;
height: number;
};
const initialData: Item[] = [...Array(NUM_ITEMS)].fill(0).map((d, index) => {
const backgroundColor = getColor(index);
return {
text: `row ${index}d`,
key: `key-${backgroundColor}`,
backgroundColor,
height: 100,
};
});
function App() {
const [data, setData] = useState(initialData);
const itemRefs = useRef(new Map());
const renderItem = useCallback((params: RenderItemParams<Item>) => {
return <RowItem {...params} itemRefs={itemRefs} />;
}, []);
return (
<View style={styles.container}>
<DraggableFlatList
keyExtractor={(item) => item.key}
data={data}
renderItem={renderItem}
onDragEnd={({ data }) => setData(data)}
activationDistance={20}
/>
</View>
);
}
export default App;
type RowItemProps = {
item: Item;
drag: () => void;
itemRefs: React.MutableRefObject<Map<any, any>>;
};
function RowItem({ item, itemRefs, drag }: RowItemProps) {
return (
<ScaleDecorator>
<SwipeableItem
key={item.key}
item={item}
ref={(ref) => {
if (ref && !itemRefs.current.get(item.key)) {
itemRefs.current.set(item.key, ref);
}
}}
onChange={({ openDirection }) => {
if (openDirection !== OpenDirection.NONE) {
// Close all other open items
[...itemRefs.current.entries()].forEach(([key, ref]) => {
if (key !== item.key && ref) ref.close();
});
}
}}
overSwipe={OVERSWIPE_DIST}
renderUnderlayLeft={() => <UnderlayLeft drag={drag} />}
renderUnderlayRight={() => <UnderlayRight />}
snapPointsLeft={[50, 150, 175]}
snapPointsRight={[175]}
>
<View
style={[
styles.row,
{ backgroundColor: item.backgroundColor, height: item.height },
]}
>
<TouchableOpacity onPressIn={drag}>
<Text style={styles.text}>{item.text}</Text>
</TouchableOpacity>
</View>
</SwipeableItem>
</ScaleDecorator>
);
}
const UnderlayLeft = ({ drag }: { drag: () => void }) => {
const { item, percentOpen } = useSwipeableItemParams<Item>();
const animStyle = useAnimatedStyle(
() => ({
opacity: percentOpen.value,
}),
[percentOpen]
);
return (
<Animated.View
style={[styles.row, styles.underlayLeft, animStyle]} // Fade in on open
>
<TouchableOpacity onPressIn={drag}>
<Text style={styles.text}>{`[drag]`}</Text>
</TouchableOpacity>
</Animated.View>
);
};
function UnderlayRight() {
const { close } = useSwipeableItemParams<Item>();
return (
<Animated.View style={[styles.row, styles.underlayRight]}>
<TouchableOpacity onPressOut={close}>
<Text style={styles.text}>CLOSE</Text>
</TouchableOpacity>
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
row: {
flexDirection: "row",
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 15,
},
text: {
fontWeight: "bold",
color: "white",
fontSize: 32,
},
underlayRight: {
flex: 1,
backgroundColor: "teal",
justifyContent: "flex-start",
},
underlayLeft: {
flex: 1,
backgroundColor: "tomato",
justifyContent: "flex-end",
},
});
```