Last active
January 13, 2022 04:20
-
-
Save steniowagner/e1cb6e8ff2595f3410554b10df9714fa to your computer and use it in GitHub Desktop.
Simple code to perform "swipe-actions" with flatlist in react-native
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useState } from "react"; | |
import LoginComponent from "./LoginComponent"; | |
const usersTest = Array(8) | |
.fill({ name: "", cns: "" }) | |
.map((_, index) => ({ | |
name: `User ${index}`, | |
cns: `123.456.${index}`, | |
})); | |
const Login: React.FC = () => { | |
const [usersSelected, setUsersSelected] = useState<Array<string>>([]); | |
const [users, setUsers] = useState(usersTest); | |
const onSwipe = (userId: string) => { | |
const isUserExistsSelectedList = usersSelected.includes(userId); | |
if (isUserExistsSelectedList) { | |
return; | |
} | |
setUsersSelected(previousUsersSelected => [ | |
...previousUsersSelected, | |
userId, | |
]); | |
}; | |
const onUnswipe = (userId: string) => { | |
const isUserExistsSelectedList = usersSelected.includes(userId); | |
if (!isUserExistsSelectedList) { | |
return; | |
} | |
setUsersSelected(previousUsersSelected => | |
previousUsersSelected.filter( | |
previousUserSelected => previousUserSelected !== userId | |
) | |
); | |
}; | |
const onRemoveUser = (idUserToRemove: string) => { | |
setUsers(prevUsers => | |
prevUsers.filter(prevUser => prevUser.cns !== idUserToRemove) | |
); | |
onUnswipe(idUserToRemove); | |
}; | |
return ( | |
<LoginComponent | |
usersSelected={usersSelected} | |
onRemoveUser={onRemoveUser} | |
onUnswipe={onUnswipe} | |
onSwipe={onSwipe} | |
users={users} | |
/> | |
); | |
}; | |
export default Login; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { FlatList, View } from "react-native"; | |
import styled from "styled-components"; | |
import SwipeListItem from "./SwipeListItem"; | |
import UsersListItem from "./UsersListItem"; | |
const Container = styled(View)` | |
width: ${({ theme }) => theme.metrics.width}px; | |
height: 100%; | |
justify-content: space-between; | |
background-color: ${({ theme }) => theme.colors.white}; | |
`; | |
const Divider = styled(View)` | |
width: 100%; | |
height: ${({ theme }) => theme.metrics.smallSize}px; | |
`; | |
interface Props { | |
onRemoveUser: (userSelectedId: string) => void; | |
onUnswipe: (cns: string) => void; | |
onSwipe: (cns: string) => void; | |
usersSelected: Array<string>; | |
users: Array<any>; | |
} | |
const LoginComponent: React.FC<Props> = ({ | |
usersSelected, | |
onRemoveUser, | |
onUnswipe, | |
onSwipe, | |
users, | |
}: Props) => { | |
const swipeOptions = [ | |
{ | |
color: "#20BCB5", | |
icon: "❌", | |
}, | |
{ | |
color: "#0647A6", | |
icon: "✏️", | |
}, | |
]; | |
return ( | |
<Container> | |
<FlatList | |
renderItem={({ item }) => ( | |
<SwipeListItem | |
background="#bbb" | |
updateRule={usersSelected.includes(item.cns)} | |
onUnswipe={() => onUnswipe(item.cns)} | |
onSwipe={() => onSwipe(item.cns)} | |
border={6} | |
options={swipeOptions.map((swipeOption, optionIndex) => { | |
let action; | |
if (optionIndex === 0) { | |
action = () => onRemoveUser(item.cns); | |
} | |
if (optionIndex === 1) { | |
action = () => console.warn("Edit: ", item); | |
} | |
return { | |
...swipeOption, | |
action, | |
}; | |
})} | |
autoclose | |
> | |
<UsersListItem | |
name={item.name} | |
cns={item.cns} | |
/> | |
</SwipeListItem> | |
)} | |
contentContainerStyle={{ | |
padding: 14, | |
}} | |
ItemSeparatorComponent={() => <Divider />} | |
keyExtractor={item => item.cns} | |
data={users} | |
/> | |
</Container> | |
); | |
}; | |
export default LoginComponent; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { memo, useCallback, useState, useRef } from "react"; | |
import { | |
LayoutChangeEvent, | |
LayoutAnimation, | |
PanResponder, | |
UIManager, | |
Animated, | |
View, | |
} from "react-native"; | |
import styled from "styled-components"; | |
import SwipeOptionButton from "./SwipeOptionButton"; | |
const Wrapper = styled(View)` | |
background-color: ${({ background }) => background}; | |
border-radius: ${({ border }) => border}px; | |
`; | |
const OptionsWrapper = styled(View)` | |
height: 100%; | |
flex-direction: row; | |
position: absolute; | |
`; | |
interface OptionProps { | |
action: () => void; | |
color: string; | |
icon: string; | |
} | |
interface Props { | |
options: Array<OptionProps>; | |
children: JSX.Element; | |
onUnswipe: () => void; | |
onSwipe: () => void; | |
updateRule: boolean; | |
autoclose?: boolean; | |
background: string; | |
border?: number; | |
} | |
const AREA_OCCUPIED_OPTION = 0.2; | |
const MIN_PIXELS_TO_MOVE = -10; | |
const MAX_OPTIONS_ALLOWED = 3; | |
const shouldComponentUpdate = (prevProps: Props, nextProps: Props) => { | |
if (prevProps.updateRule !== nextProps.updateRule) { | |
return false; | |
} | |
return true; | |
}; | |
const SwipeListItem: React.FC<Props> = memo<Props>( | |
({ | |
background, | |
onUnswipe, | |
autoclose, | |
children, | |
onSwipe, | |
options, | |
border, | |
}: Props) => { | |
const [isContainerRefSet, setIsContainerRefSet] = useState(false); | |
const panRef = useRef(new Animated.ValueXY()); | |
const containerRef = useRef(null); | |
if (UIManager.setLayoutAnimationEnabledExperimental) { | |
UIManager.setLayoutAnimationEnabledExperimental(true); | |
} | |
const animateWithSpring = (newX: number): void => { | |
const bounciness = newX === 0 ? 0 : 8; | |
Animated.spring(panRef.current, { | |
bounciness, | |
toValue: { | |
x: newX, | |
y: 0, | |
}, | |
}).start(); | |
}; | |
const getMaxAreaOccupiedByOptions = (): number => { | |
if (!containerRef.current) { | |
return 0; | |
} | |
const { width } = containerRef.current; | |
const maxAreaOccupiedByOptions = | |
AREA_OCCUPIED_OPTION * options.length * width; | |
return maxAreaOccupiedByOptions; | |
}; | |
const checkIsSwipingNegatively = (dx: number): boolean => | |
(panRef.current.x as any)._value === 0 && dx < 0; | |
const onPanResponderRelease = (dx: number): void => { | |
if (checkIsSwipingNegatively(dx)) { | |
return; | |
} | |
const maxAreaOccupiedByOptions = getMaxAreaOccupiedByOptions(); | |
const swipedEnoughToShowOptions = dx >= maxAreaOccupiedByOptions / 2; | |
const newX = swipedEnoughToShowOptions ? maxAreaOccupiedByOptions : 0; | |
if (swipedEnoughToShowOptions) { | |
onSwipe(); | |
} | |
if (!swipedEnoughToShowOptions) { | |
onUnswipe(); | |
} | |
animateWithSpring(newX); | |
}; | |
const onPanResponderMove = (dx: number): void => { | |
if (checkIsSwipingNegatively(dx)) { | |
return; | |
} | |
const maxAreaOccupiedByOptions = getMaxAreaOccupiedByOptions(); | |
const currentPosition = (panRef.current.x as any)._value; | |
const isSwipingBeoyndOptionsArea = | |
dx + currentPosition >= maxAreaOccupiedByOptions; | |
if (isSwipingBeoyndOptionsArea) { | |
const { width } = containerRef.current; | |
const newX = maxAreaOccupiedByOptions + width * 0.1; | |
animateWithSpring(newX); | |
return; | |
} | |
const isSwipedEnough = dx > Math.abs(MIN_PIXELS_TO_MOVE); | |
if (isSwipedEnough) { | |
panRef.current.setValue({ x: dx + MIN_PIXELS_TO_MOVE, y: 0 }); | |
} | |
if (dx < 0) { | |
animateWithSpring(0); | |
} | |
}; | |
const panResponder = PanResponder.create({ | |
onPanResponderRelease: (_, { dx }) => onPanResponderRelease(dx), | |
onPanResponderMove: (_, { dx }) => onPanResponderMove(dx), | |
onPanResponderTerminationRequest: () => false, | |
onStartShouldSetPanResponder: () => false, | |
onMoveShouldSetPanResponder: () => true, | |
}); | |
const onLayout = useCallback((event: LayoutChangeEvent): void => { | |
containerRef.current = event.nativeEvent.layout; | |
setIsContainerRefSet(true); | |
}, []); | |
const onPressSwipeOptionButton = ({ action }: OptionProps) => { | |
if (autoclose) { | |
animateWithSpring(0); | |
} | |
LayoutAnimation.configureNext( | |
LayoutAnimation.create(300, "linear", "opacity") | |
); | |
action(); | |
}; | |
const maxAreaOccupiedByOptions = getMaxAreaOccupiedByOptions(); | |
return ( | |
<Wrapper | |
background={background} | |
onLayout={onLayout} | |
border={border}> | |
<OptionsWrapper> | |
{isContainerRefSet && ( | |
<> | |
{options | |
.slice(0, MAX_OPTIONS_ALLOWED) | |
.map((item, optionIndex) => ( | |
<SwipeOptionButton | |
width={maxAreaOccupiedByOptions / options.length} | |
onPress={() => onPressSwipeOptionButton(item)} | |
key={String(optionIndex)} | |
index={optionIndex} | |
color={item.color} | |
icon={item.icon} | |
border={border} | |
/> | |
))} | |
</> | |
)} | |
</OptionsWrapper> | |
<Animated.View | |
// eslint-disable-next-line react/jsx-props-no-spreading | |
{...panResponder.panHandlers} | |
style={panRef.current.getLayout()} | |
> | |
{children} | |
</Animated.View> | |
</Wrapper> | |
); | |
}, | |
shouldComponentUpdate | |
); | |
export default SwipeListItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { View, Text } from "react-native"; | |
import styled from "styled-components"; | |
const Wrapper = styled(View)` | |
width: 100%; | |
flex-direction: row; | |
justify-content: space-between; | |
align-items: center; | |
padding: ${({ theme }) => theme.metrics.largeSize}px; | |
background-color: ${({ theme }) => theme.colors.white}; | |
border-radius: ${({ theme }) => theme.metrics.extraSmallSize}px; | |
`; | |
const TextContenWrapper = styled(View)` | |
width: 70%; | |
`; | |
const UserNameText = styled(Text).attrs({ | |
numberOfLines: 1, | |
})` | |
font-family: CircularStd-Bold; | |
color: ${({ theme }) => theme.colors.textColor}; | |
font-size: ${({ theme }) => theme.metrics.extraLargeSize}; | |
`; | |
const CNSText = styled(Text).attrs({ | |
numberOfLines: 1, | |
})` | |
font-family: CircularStd-Medium; | |
color: ${({ theme }) => theme.colors.secondaryTextColor}; | |
font-size: ${({ theme }) => theme.metrics.largeSize * 1.2}; | |
`; | |
type Props = { | |
name: string; | |
cns: string; | |
}; | |
const UsersListItem = ({ name, cns }: Props) => ( | |
<Wrapper | |
style={{ | |
shadowColor: "#000", | |
shadowOffset: { | |
width: 0, | |
height: 2, | |
}, | |
shadowOpacity: 0.25, | |
shadowRadius: 3.84, | |
elevation: 5, | |
}} | |
> | |
<TextContenWrapper> | |
<UserNameText>{name.toUpperCase()}</UserNameText> | |
<CNSText>{cns}</CNSText> | |
</TextContenWrapper> | |
</Wrapper> | |
); | |
export default UsersListItem; |
It's amazing feature dude, make a repository to share it with community!!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Nice!