React Native:如何实现2列刷卡?

React Native: How to implement 2 columns of swipping cards?

我正在尝试在 2 列中实现可滚动的卡片列表。卡片应可向左或向右滑出屏幕以移除。

基本上,它应该像 Chrome 应用程序当前显示的选项卡列表一样,可以滑动关闭。请参阅示例图片 here

我能够使用 FlatList 在 2 列中实现卡片列表。但是,我无法让卡片可以刷卡。我试过 react-tinder-card 但它不能限制上下滑动,因此列表变得不可滚动。 react-native-deck-swiper 也不适用于列表。

感谢任何帮助。谢谢!

我要实现一个满足以下要求的组件:

  1. 创建两列 FlatList,其中的项目是您的卡片。
  2. 实施识别 swipeLeftswipeRight 操作的手势处理,这些操作将移除刷过的卡。
  3. swipe 动作应该是动画的,这意味着我们有某种 drag of the screen 行为。

我将使用基本的 react-native FlatListnumColumns={2}react-native-swipe-list-view 来处理 swipeLeftswipeRight 操作以及想要的动画。

我将执行一个 fire and forget 操作,因此删除一个项目后,它就永远消失了。如果我们希望能够恢复已删除的项目,我们将在稍后实施 restore mechanism

我的初始实现如下:

  1. 使用 numColumns={2} 创建一个 FlatList 和一些额外的虚拟样式以增加一些边距。
  2. 使用 useState 创建状态,其中包含代表我们卡片的对象数组。
  3. 实现一个函数,从提供了 id 的状态中删除一个项目。
  4. 将要呈现的项目包装在 SwipeRow 中。
  5. removeItem 函数传递给 swipeGestureEnded 属性。
import React, { useState } from "react"
import { FlatList, SafeAreaView, Text, View } from "react-native"
import { SwipeRow } from "react-native-swipe-list-view"

const data = [
  {
    id: "0",
    title: "Title 1",
  },
  {
    id: "1",
    title: "Title 2",
  },
  {
    id: "2",
    title: "Title 3",
  },
  {
    id: "3",
    title: "Title 4",
  },
  {
    id: "4",
    title: "Title 5",
  },
  {
    id: "5",
    title: "Title 6",
  },
  {
    id: "6",
    title: "Title 7",
  },
  {
    id: "7",
    title: "Title 8",
  },
]

export function Test() {
  const [cards, setCards] = useState(data)
  const [removed, setRemoved] = useState([])

  function removeItem(id) {
    let previous = [...cards]
    let itemToRemove = previous.find((x) => x.id === id)
    setCards(previous.filter((c) => c.id !== id))
    setRemoved([...removed, itemToRemove])
  }

  return (
    <SafeAreaView style={{ margin: 20 }}>
      <FlatList
        data={cards}
        numColumns={2}
        keyExtractor={(item) => item.id}
        renderItem={({ index, item }) => (
          <SwipeRow swipeGestureEnded={() => removeItem(item.id)}>
            <View />
            <View style={{ margin: 20, borderWidth: 1, padding: 20 }}>
              <Text>{item.title}</Text>
            </View>
          </SwipeRow>
        )}
      />
    </SafeAreaView>
  )
}

请注意,我们的对象中需要某种 属性 以确定要删除的对象。我在这里使用了基本的 id 属性,这在使用 FlatList 时很常见。如果您从 API 中检索数据,而 API 不提供相同的 id,那么我们可以先进行一些预处理 (normalization),然后自己添加 id 属性.

初始视图如下所示。

滑动,假设向右或向左 'Title 6' 的项目将其删除。

可能还需要实现以下功能。

  • 如果该项目位于第一列,则只需向左滑动即可删除该项目。
  • 如果项目在第二列,则只有向右滑动才能删除项目。

这很容易实现,使用传递给 renderItem 函数的 index 参数和传递给 swipeGestureEndedgestureStatevx 属性] 函数。

这是完整的工作实施。

import React, { useState } from "react"
import { FlatList, SafeAreaView, Text, View } from "react-native"
import { SwipeRow } from "react-native-swipe-list-view"

const data = [
  {
    id: "0",
    title: "Title 1",
  },
  {
    id: "1",
    title: "Title 2",
  },
  {
    id: "2",
    title: "Title 3",
  },
  {
    id: "3",
    title: "Title 4",
  },
  {
    id: "4",
    title: "Title 5",
  },
  {
    id: "5",
    title: "Title 6",
  },
  {
    id: "6",
    title: "Title 7",
  },
  {
    id: "7",
    title: "Title 8",
  },
]

export function Test() {
  const [cards, setCards] = useState(data)
  const [removed, setRemoved] = useState([])

  function removeItem(id) {
    let previous = [...cards]
    let itemToRemove = previous.find((x) => x.id === id)
    setCards(previous.filter((c) => c.id !== id))
    setRemoved([...removed, itemToRemove])
  }

  return (
    <SafeAreaView style={{ margin: 20 }}>
      <FlatList
        data={cards}
        numColumns={2}
        keyExtractor={(item) => item.id}
        renderItem={({ index, item }) => (
          <SwipeRow
            swipeGestureEnded={(key, event) => {
              if (event.gestureState.vx < 0) {
                if (index % 2 === 0) {
                  removeItem(item.id)
                }
              } else if (event.gestureState.vx >= 0) {
                if (index % 2 === 1) {
                  removeItem(item.id)
                }
              }
            }}
            disableLeftSwipe={index % 2 === 1}
            disableRightSwipe={index % 2 === 0}>
            <View />
            <View style={{ margin: 20, borderWidth: 1, padding: 20 }}>
              <Text>{item.title}</Text>
            </View>
          </SwipeRow>
        )}
      />
    </SafeAreaView>
  )
}

由于基于 FlatList 的索引为零,当且仅当 index % 2 === 1 时(例如,索引为 3 的项目总是在第二列中)第二列,因此不能被 2 整除),另一方面,当且仅当 index % 2 === 0index 可以被 2 整除时,项目才会出现在第一列中。

SwipeRowComponent 中有几个回调函数道具应该在某些情况下触发。但是,它们中的大多数在我的设置中不起作用,我仍然不知道为什么。我通过使用 event.gestureState.vx 属性 使它工作,如果我们向左滑动则为负,如果我们向右滑动则为正(包括零)。

可能需要实现一个 undo 按钮,因为它在此类功能中很常见。这可以按如下方式完成:

  • 实现第二​​个状态,表示 Queue 保存最后删除的项目。 undo 按钮然后只是 pops 最后删除的项目。

这是一个完全可用的实现,它带有一个虚拟 undo 按钮,可以实现这一点。

import React, { useState } from "react"
import { Button, FlatList, SafeAreaView, Text, View } from "react-native"
import { SwipeRow } from "react-native-swipe-list-view"

const data = [
  {
    id: "0",
    title: "Title 1",
  },
  {
    id: "1",
    title: "Title 2",
  },
  {
    id: "2",
    title: "Title 3",
  },
  {
    id: "3",
    title: "Title 4",
  },
  {
    id: "4",
    title: "Title 5",
  },
  {
    id: "5",
    title: "Title 6",
  },
  {
    id: "6",
    title: "Title 7",
  },
  {
    id: "7",
    title: "Title 8",
  },
]

export function Test() {
  const [cards, setCards] = useState(data)
  const [removed, setRemoved] = useState([])

  function removeItem(id) {
    let previous = [...cards]
    let itemToRemove = previous.find((x) => x.id === id)
    setCards(previous.filter((c) => c.id !== id))
    setRemoved([...removed, itemToRemove])
  }

  function undoRemove() {
    if (removed && removed.length > 0) {
      let itemToUndo = removed[removed.length - 1]
      setCards([...cards, itemToUndo])
      setRemoved(removed.filter((c) => c.id !== itemToUndo.id))
    }
  }

  return (
    <SafeAreaView style={{ margin: 20 }}>
      <FlatList
        data={cards}
        numColumns={2}
        keyExtractor={(item) => item.id}
        renderItem={({ index, item }) => (
          <SwipeRow
            swipeGestureEnded={(key, event) => {
              if (event.gestureState.vx < 0) {
                if (index % 2 === 0) {
                  removeItem(item.id)
                }
              } else if (event.gestureState.vx >= 0) {
                if (index % 2 === 1) {
                  removeItem(item.id)
                }
              }
            }}
            disableLeftSwipe={index % 2 === 1}
            disableRightSwipe={index % 2 === 0}>
            <View />
            <View style={{ margin: 20, borderWidth: 1, padding: 20 }}>
              <Text>{item.title}</Text>
            </View>
          </SwipeRow>
        )}
      />
      <Button onPress={undoRemove} title="Undo" />
    </SafeAreaView>
  )
}

请注意,我的 undo 按钮只是将删除的项目附加到列表的末尾。如果你想保留初始索引,那么你需要保存旧索引并将项目推到正确的位置。

这是我上次实施的工作 snack