⬆️ ⬇️

React Native: make a draggable & swipeable list

Today it is difficult to surprise someone with the ability to swipe elements of the list in mobile applications. In one of our react-native applications, there was also such functionality, but recently there was a need to expand its ability to drag and drop list items. And since the process of finding a solution cost me a certain number of nerve cells, I decided to write down a small article in order to save valuable time for future generations.







In our application for creating a swipeable list, we used the react-native-swipe-list-view package. The first thought was to take some package with drag'n'drop functionality and cross a hedgehog with a snake.



Search the Internet for three candidates: react-native-draggable-list , react-native-sortable-list and react-native-draggable-flatlist .

')

With the help of the first package, it was not possible to launch even the attached example (however, not only me, the relevant problem is indicated in the issues ).



I had to tinker with the second package, but I created a draggable & swipable list. However, the result was not inspired - the component was godlessly buggy: blinking redrawing, dropping elements far beyond the list, or even their disappearance. It became clear that in this form it can not be used.



The last package, at first, also behaved like a naughty lady, but then it turned out that I simply did not know how to cook it. Having picked up the key to the heart of this “lady”, I managed to achieve an acceptable result.



In our project there was a swipeable list to which you need to drag and drop, but in practice it is better to start from the other end: first make the drag list, and then add the ability to snap.



It is assumed that readers know how to create a react-native project, so we will focus on creating the list we need. In the following example, the code is in TypeScript.



Making a draggable-list



So, let's start by installing the package:



 yarn add react-native-draggable-flatlist 


We import the necessary modules:



 import React, { Component } from 'react' import { View } from 'react-native' import styles from './styles' import DraggableFlatList, { RenderItemInfo, OnMoveEndInfo } from 'react-native-draggable-flatlist' import ListItem from './components/ListItem' import fakeData from './fakeData.json' 


Here DraggableFlatList is a component from the installed package that implements the dragging feature, ListItem is our component for displaying a list item (the code will be presented below), fakeData is a json file that contains fake data - in this case, an array of objects of the form:



 {"id": 0, "name": "JavaScript", "favorite": false} 


In a real application, these data are most likely to come into your component from the props or will be downloaded from the network, but in our case we will manage with a little blood.



Since TypeScript is used in this example, we will describe some entities:



 type Language = { id: number, name: string, favorite: boolean, } interface AppProps {} interface AppState { data: Array<Language> } 


The Language type tells us which fields will have list items.



In this example, we will not get anything from the props, so the AppProps interface is trivial, and in the stack we will store an array of Language objects, as indicated in the AppState interface.



Since the component code is not very large, I will give it in its entirety:



App component code
 class App extends Component<AppProps, AppState> { constructor(props: AppProps) { super(props) this.state = { data: fakeData, } } onMoveEnd = ({ data }: OnMoveEndInfo<Language>) => { this.setState({ data: data ? [...data] : [] }) } render() { return ( <View style={styles.root}> <DraggableFlatList data={this.state.data} renderItem={this.renderItem} keyExtractor={(item) => item.id.toString()} scrollPercent={5} onMoveEnd={this.onMoveEnd} /> </View> ) } renderItem = ({ item, move, moveEnd, isActive }: RenderItemInfo<Language>) => { return ( <ListItem name={item.name} move={move} moveEnd={moveEnd} isActive={isActive} /> ) } } 




The onMoveEnd method onMoveEnd called when the item has been moved. In this case, we need to put a list with a new order of elements in the state, therefore we call the this.setState method.



The renderItem method serves to display a list item and accepts an object of type RenderItemInfo <Language>. This object includes the following fields:





The component for displaying a list item is actually TouchableOpacity , which, with a long press, causes move , and when released, moveEnd .



ListItem component code
 import React from 'react' import { Text, TouchableOpacity } from 'react-native' import styles from './styles' interface ListItemProps { name: string, move: () => void, moveEnd: () => void, isActive: boolean, } const ListItem = ({ name, move, moveEnd, isActive }: ListItemProps) => { return ( <TouchableOpacity style={[styles.root, isActive && styles.active]} onLongPress={move} onPressOut={moveEnd} > <Text style={styles.text}>{name}</Text> </TouchableOpacity> ) } export default ListItem 




Styles for all components are rendered into separate files and are not shown here, but they can be viewed in the repository .



The result is:







Add the ability to svaypat



Well, we have successfully coped with the first part, we proceed to the second part of the Marlezonsky ballet.



To add the ability to snip elements of the list, we use the react-native-swipe-list-view package.



First, let's install it:



 yarn add react-native-swipe-list-view 


This package has a SwipeRow component, which, according to the documentation, should include two components:



 <SwipeRow> <View style={hiddenRowStyle} /> <View style={visibleRowStyle} /> </SwipeRow> 


Notice that the first View is drawn below the second.



Let's change the code of the ListItem component.



ListItem component code
 import React from 'react' import { Text, TouchableOpacity, View, Image } from 'react-native' import { SwipeRow } from 'react-native-swipe-list-view' import { Language } from '../../App' import styles from './styles' const heart = require('./icons8-heart-24.png') const filledHeart = require('./icons8-heart-24-filled.png') interface ListItemProps { item: Language, move: () => void, moveEnd: () => void, isActive: boolean, onHeartPress: () => void, } const ListItem = ({ item, move, moveEnd, isActive, onHeartPress }: ListItemProps) => { return ( <SwipeRow rightOpenValue={-180}> <View style={styles.hidden}> <TouchableOpacity onPress={onHeartPress}> <Image source={item.favorite ? filledHeart : heart} /> </TouchableOpacity> </View> <TouchableOpacity activeOpacity={1} style={[styles.root, isActive && styles.active]} onLongPress={move} onPressOut={moveEnd} > <Text style={styles.text}>{item.name}</Text> </TouchableOpacity> </SwipeRow> ) } export default ListItem 




First, we added the SwipeRow component with the SwipeRow property, which defines the distance that the element can be swapped.



Secondly, we moved our TouchableOpacity inside SwipeRow and added a View, which will be drawn under this button.



Inside this View, a picture is drawn that indicates whether the language is a favorite. When you click on it, the value should be reversed, and since the data is in the parent component, you need to drop the callback that performs this action here.



Make the necessary changes to the parent component:



App component code
 import React, { Component } from 'react' import { View } from 'react-native' import styles from './styles' import DraggableFlatList, { RenderItemInfo, OnMoveEndInfo } from 'react-native-draggable-flatlist' import ListItem from './components/ListItem' import fakeData from './fakeData.json' export type Language = { id: number, name: string, favorite: boolean, } interface AppProps {} interface AppState { data: Array<Language> } class App extends Component<AppProps, AppState> { constructor(props: AppProps) { super(props) this.state = { data: fakeData, } } onMoveEnd = ({ data }: OnMoveEndInfo<Language>) => { this.setState({ data: data ? [...data] : [] }) } toggleFavorite = (value: Language) => { const data = this.state.data.map(item => ( item.id !== value.id ? item : { ...item, favorite: !item.favorite } )) this.setState({ data }) } render() { return ( <View style={styles.root}> <DraggableFlatList data={this.state.data} renderItem={this.renderItem} keyExtractor={(item) => item.id.toString()} scrollPercent={5} onMoveEnd={this.onMoveEnd} /> </View> ) } renderItem = ({ item, move, moveEnd, isActive }: RenderItemInfo<Language>) => { return ( <ListItem item={item} move={move} moveEnd={moveEnd} isActive={isActive} onHeartPress={() => this.toggleFavorite(item)} /> ) } } export default App 




Project sources on GitHub .



The result is shown below:



Source: https://habr.com/ru/post/460567/



All Articles