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

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

282soqzy-xmsxuup_ufv0imnp08.gif
В нашем приложении для создания 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 говорит нам о том, какие поля будут иметь элементы списка.

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

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

код компонента App
class App extends Component {
  constructor(props: AppProps) {
    super(props)

    this.state = {
      data: fakeData,
    }
  }

  onMoveEnd = ({ data }: OnMoveEndInfo) => {
    this.setState({ data: data ? [...data] : [] })
  }

  render() {
    return (
      
         item.id.toString()}
          scrollPercent={5}
          onMoveEnd={this.onMoveEnd}
        />
      
    )
  }

  renderItem = ({ item,  move, moveEnd, isActive }: RenderItemInfo) => {
    return (
      
    )
  }
}


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

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

  • 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 (
    
      {name}
    
  )
}

export default ListItem


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

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

yz2alrixtubziyk1vx4cuptrnxo.gif

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


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

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

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

yarn add react-native-swipe-list-view


В этом пакете есть компонент 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 (
    

      
        
          
        
      

      
        {item.name}
      
    
  )
}

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
}

class App extends Component {
  constructor(props: AppProps) {
    super(props)

    this.state = {
      data: fakeData,
    }
  }

  onMoveEnd = ({ data }: OnMoveEndInfo) => {
    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 (
      
         item.id.toString()}
          scrollPercent={5}
          onMoveEnd={this.onMoveEnd}
        />
      
    )
  }

  renderItem = ({ item, move, moveEnd, isActive }: RenderItemInfo) => {
    return (
       this.toggleFavorite(item)}
      />
    )
  }
}

export default App


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

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

282soqzy-xmsxuup_ufv0imnp08.gif

© Habrahabr.ru