React Native: делаем draggable & swipeable список

от автора

Сегодня трудно кого-то удивить возможностью свайпать элементы списка в мобильных приложениях. В одном нашем react-native приложении тоже была такая функциональность, но недавно возникла необходимость расширить её возможностью перетаскивать элементы списка. А поскольку процесс поиска решения стоил мне некоторого количества нервных клеток, я решил запилить небольшую статью, чтобы сэкономить драгоценное время будущим поколениям.



В нашем приложении для создания swipeable-списка мы использовали пакет react-native-swipe-list-view. Первой мыслью было взять какой-нибудь пакет с drag’n’drop функциональностью и скрестить ежа с ужом.

Поиск по просторам интернета дал трёх кандидатов: react-native-draggable-list, react-native-sortable-list и react-native-draggable-flatlist.

С помощью первого пакета не удалось запустить даже прилагаемый пример (впрочем, не только мне, о соответствующей проблеме указано в issues).

Со вторым пакетом пришлось повозиться, но создать draggable & swipable список получилось. Однако, результат не вдохновил — компонент безбожно глючило: мигание перерисовки, проваливание элементов далеко за пределы списка, а то и вовсе их исчезновение. Стало понятно, что в таком виде им пользоваться нельзя.

Последний пакет поначалу тоже вел себя как капризная дама, однако потом оказалось, что я просто не умел его готовить. Подобрав ключик к сердцу этой «дамы», мне удалось добиться приемлемого результата.

В нашем проекте был swipeable список, к которому нужно прикрутить drag and drop, но на практике лучше начать с другого края: сначала сделать перетаскиваемый список, а потом добавить возможность свайпать.

Предполагается, что читатели знают, как создать проект react-native, поэтому сосредоточимся на создании нужного нам списка. В обсуждаемом ниже примере приведен код на TypeScript.

Делаем draggable-list

Итак, начнем с установки пакета:

yarn add react-native-draggable-flatlist

Импортируем нужные модули:

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'

Здесь DraggableFlatList — это компонент из установленного пакета, реализующий возможность перетаскивания, ListItem — наш компонент для отображения элемента списка (код будет представлен ниже), fakeData — json файл, в котором содержатся фейковые данные — в данном случае, массив объектов вида:

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

В реальном приложении эти данные скорее всего придут в ваш компонент из пропсов или будут загружены из сети, но в нашем случае обойдёмся малой кровью.

Так как в данном примере используется TypeScript, опишем некоторые сущности:

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

Тип Language говорит нам о том, какие поля будут иметь элементы списка.

В данном примере мы ничего не будем получать из пропсов, поэтому интерфейс AppProps тривиален, а в стейте мы будем хранить массив объектов Language, что и указано в интерфейсе AppState.

Поскольку код компонента не очень большой, приведу его целиком:

код компонента App

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}       />     )   } }

Метод onMoveEnd вызывается, когда перемещение элемента закончено. В этом случае, нам необходимо положить список с новым порядком элементов в стейт, поэтому вызываем метод this.setState.

Метод renderItem служит для отображения элемента списка и принимает объект типа RenderItemInfo<Language>. Этот объект включает в себя следующие поля:

  • item — очередно элемент массива, переданного в качестве данных в список,
  • move и moveEnd — функции, вызываемые при перемещении элемента списка, эти функции предоставляет компонент DraggableFlatList,
  • isActive — поле логического типа, определяющее, является ли элемент перетаскиваемым в данный момент.

Компонент для отображения элемента списка, фактически, представляет собой TouchableOpacity, который при долгом нажатии вызывает move, а при отпускании — moveEnd.

код компонента ListItem

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

Стили для всех компонентов вынесены в отдельные файлы и здесь не приводятся, но их можно посмотреть в репозитории.

Получившийся результат:

Добавляем возможность свайпать

Ну что ж, с первой частью мы успешно справились, приступаем ко второй части Марлезонского балета.

Для добавления возможности свайпать элементы списка воспользуемся пакетом react-native-swipe-list-view.

Для начала давайте его установим:

yarn add react-native-swipe-list-view

В этом пакете есть компонент SwipeRow, который, согласно документации, должен включать в себя два компонента:

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

Обратите внимание, что первый View рисуется под вторым.

Давайте изменим код компонента ListItem.

код компонента ListItem

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

Во-первых, мы добавили компонент SwipeRow со свойством rightOpenValue, которое определяет расстояние, на которое можно свайпать элемент.

Во-вторых, мы переместили внутрь SwipeRow наш TouchableOpacity и добавили View, который будет рисоваться под этой кнопкой.

Внутри этой View рисуется картинка, определяющая, является ли язык любимым. При нажатии на неё значение должно меняться на противоположное, а так как данные находятся в родительском компоненте, то необходимо прокинуть сюда коллбэк, выполняющий это действие.

Внесём необходимые изменения в родительский компонент:

код компонента App

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

Исходники проекта на GitHub.

Результат представлен ниже:


ссылка на оригинал статьи https://habr.com/ru/post/460567/