В нашем приложении для создания 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
.
Поскольку код компонента не очень большой, приведу его целиком:
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
.
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
.
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 рисуется картинка, определяющая, является ли язык любимым. При нажатии на неё значение должно меняться на противоположное, а так как данные находятся в родительском компоненте, то необходимо прокинуть сюда коллбэк, выполняющий это действие.
Внесём необходимые изменения в родительский компонент:
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/
Добавить комментарий