Хабрахабр

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

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

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

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

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

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

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

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

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

Делаем draggable-list

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

yarn add react-native-draggable-flatlist

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

import React, 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.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть