A horizontally swipeable flatlist row compatible with react-native-draggable-flatlist
npm install react-native-swipeable-itemA swipeable component with underlay for React Native.
Fully native interactions powered by Reanimated and React Native Gesture Handler
Compatible with React Native Draggable Flatlist
1. Follow installation instructions for reanimated and react-native-gesture-handler
2. npm install or yarn add react-native-swipeable-item
3. import SwipeableItem from 'react-native-swipeable-item'
_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
type ClosePromiseFn = (options?: OpenCloseOptions) => Promise
export type UnderlayParams
item: T;
open: OpenPromiseFn;
close: ClosePromiseFn;
percentOpen: Animated.DerivedValue
isGestureActive: Animated.DerivedValue
direction: OpenDirection;
};
export type OverlayParams
item: T;
openLeft: OpenPromiseFn;
openRight: OpenPromiseFn;
close: ClosePromiseFn;
openDirection: OpenDirection;
percentOpenLeft: Animated.DerivedValue
percentOpenRight: Animated.DerivedValue
};
`
| 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. |
| Name | Type | Description |
| :----------------------- | :--------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| useSwipeableItemParams | () => OverlayParams | 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). |
| |
`tsxopenLeft()
function MyUnderlayComponent() {
// Underlay components "know" which direction to open, so we don't need to call or openRight(), we can just call 'open()'percentOpen
// Underlay components also receive the value of their own direction (percentOpenLeft or percentOpenRight)
const swipeableItemParams = useSwipeableItemParams();
return
}
function MyOverlayComponent() {
// Overlay components get the same params, but have defaults filled in for open() and percentOpen params.`
const swipeableItemParams = useSwipeableItemParams();
return
}
| Name | Type | Description |
| :------ | :------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------- |
| open | (OpenDirection.LEFT \| OpenDirection.RIGHT, snapIndex?: number, options?: { animated: boolean }) => Promise | Imperatively open left or right. Promise resolves once open. |close
| | (options?: { animated?: boolean}) => Promise | Close all. Promise resolves once closed. |
`tsx
// Imperative open example
const itemRef: SwipeableItem | null = null
...
...
if (itemRef) itemRef.open(OpenDirection.LEFT)
`
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).
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-${backgroundColor}
key: ,
backgroundColor,
height: 100,
};
});
function App() {
const [data, setData] = useState(initialData);
const itemRefs = useRef(new Map());
const renderItem = useCallback((params: RenderItemParams
return
}, []);
return (
data={data}
renderItem={renderItem}
onDragEnd={({ data }) => setData(data)}
activationDistance={20}
/>
);
}
export default App;
type RowItemProps = {
item: Item;
drag: () => void;
itemRefs: React.MutableRefObject
function RowItem({ item, itemRefs, drag }: RowItemProps) {
return (
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={() =>
renderUnderlayRight={() =>
snapPointsLeft={[50, 150, 175]}
snapPointsRight={[175]}
>
styles.row,
{ backgroundColor: item.backgroundColor, height: item.height },
]}
>
);
}
const UnderlayLeft = ({ drag }: { drag: () => void }) => {
const { item, percentOpen } = useSwipeableItemParams
const animStyle = useAnimatedStyle(
() => ({
opacity: percentOpen.value,
}),
[percentOpen]
);
return (
>
}
);
};
function UnderlayRight() {
const { close } = useSwipeableItemParams
return (
);
}
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",
},
});
``