Skyroc Native UI

SwipeCell

可左右滑动的单元格,露出两侧操作区

滑动单元格(SwipeCell)横向滑动主体内容,露出左右两侧的操作区(常见于列表项的「删除 / 置顶」)。手势基于 react-native-gesture-handler,位移由 react-native-reanimated 的弹簧动画驱动,两侧操作区固定在底层,内容区盖在上面平移。

import { SwipeCell } from '@skyroc/native-ui';

组件内部用到了 GestureDetector,请确保 App 根节点已经包了 GestureHandlerRootView

基础用法

leading / trailing 分别是左右操作区,宽度默认由内容 onLayout 自动测量。展开态点击主体内容即可收起。

SwipeCellBasic.tsx
Loading…
import { Cell, SwipeCell, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const SwipeCellBasic = () => {
  return (
    <View className="bg-muted">
      <SwipeCell
        leading={
          <View className="w-16 items-center justify-center bg-primary">
            <Text className="text-sm text-primary-foreground">选择</Text>
          </View>
        }
        trailing={
          <>
            <View className="w-16 items-center justify-center bg-primary">
              <Text className="text-sm text-primary-foreground">编辑</Text>
            </View>
            <View className="w-16 items-center justify-center bg-destructive">
              <Text className="text-sm text-destructive-foreground">删除</Text>
            </View>
          </>
        }
      >
        <Cell
          title="单元格"
          trailing="内容"
        />
      </SwipeCell>
    </View>
  );
};

export { SwipeCellBasic };

松手时的停靠由速度优先于位移决定:滑动速度超过 500 时按方向停靠,否则看位移是否超过该侧宽度的 30%。速度优先是必要的 —— 从左侧展开态快速左划时位移仍是正的,只看位移会误判成「继续展开」。拖出边界后按 0.25 的阻尼衰减,不会硬撞墙。

何时使用

  • 列表项的次要操作(删除、标记、置顶),不值得常驻一个按钮。
  • 主体本身可点时慎用:展开态下主体会盖一层透明遮罩,点击只会收起而不会触发原有的点击。

关闭拦截

beforeClose 返回 false(或 resolve 为 false)可以阻止收起,常用于删除前弹确认框。点击主体、手势回弹、实例 close() 三条路径都会经过它。

SwipeCellBeforeClose.tsx
Loading…
import { Button, Cell, SwipeCell, Text } from '@skyroc/native-ui';
import type { SwipeCellBeforeCloseParams, SwipeCellInstance } from '@skyroc/native-ui';
import { useRef, useState } from 'react';
import { Alert, View } from 'react-native';

const SwipeCellBeforeClose = () => {
  const [lastDecision, setLastDecision] = useState('—');

  const guardedRef = useRef<SwipeCellInstance>(null);

  function handleBeforeClose({ position }: SwipeCellBeforeCloseParams) {
    return new Promise<boolean>(resolve => {
      Alert.alert('提示', `确定要关闭吗?(来源:${position})`, [
        {
          onPress: () => {
            setLastDecision(`${position} · 拦下了,保持展开`);
            resolve(false);
          },
          style: 'cancel',
          text: '取消'
        },
        {
          onPress: () => {
            setLastDecision(`${position} · 放行,收起`);
            resolve(true);
          },
          text: '确定'
        }
      ]);
    });
  }

  return (
    <View className="bg-muted">
      <SwipeCell
        beforeClose={handleBeforeClose}
        name="guarded"
        ref={guardedRef}
        trailing={
          <View className="w-16 items-center justify-center bg-destructive">
            <Text className="text-sm text-destructive-foreground">删除</Text>
          </View>
        }
      >
        <Cell
          title="单元格"
          trailing="向左滑开后再试着收起"
        />
      </SwipeCell>
      <View className="mt-3 flex-row items-center gap-3 px-4">
        <Button
          size="sm"
          variant="outline"
          onPress={() => guardedRef.current?.close()}
        >
          关闭
        </Button>
        <Text className="flex-1 text-sm text-muted-foreground">上次结果:{lastDecision}</Text>
      </View>
    </View>
  );
};

export { SwipeCellBeforeClose };

挂了拦截后,操作区在拿到结果之前不会移动:确认才收起,拒绝则保持展开,不存在「先关掉又弹回来」的中间态 —— 那读起来像动画 bug,而不是一次被拒绝的关闭。代价是异步确认期间关闭动画会推迟到 Promise resolve 之后。

被其他 SwipeCell 挤掉、以及 disabled 强制收起这两条路径不经过 beforeClose:那不是用户对本实例的主动操作,此时弹确认框只会打断手势。

操作区宽度

leadingWidth / trailingWidth 跳过自动测量、直接指定停靠距离。

SwipeCellWidth.tsx
Loading…
import { Cell, SwipeCell, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const SwipeCellWidth = () => {
  return (
    <View className="bg-muted">
      <SwipeCell
        leading={
          <View className="w-[100px] items-center justify-center bg-primary">
            <Text className="text-sm text-primary-foreground">收藏</Text>
          </View>
        }
        leadingWidth={100}
        trailing={
          <View className="w-[80px] items-center justify-center bg-destructive">
            <Text className="text-sm text-destructive-foreground">删除</Text>
          </View>
        }
        trailingWidth={80}
      >
        <Cell
          title="单元格"
          trailing="自定义宽度"
        />
      </SwipeCell>
    </View>
  );
};

export { SwipeCellWidth };

某一侧宽度为 0(没有内容或尚未测量)时,open(side) 会被忽略 —— 否则会进入「视觉没展开、状态却是展开」的死区。

互斥展开

默认同一时刻只有一个 SwipeCell 处于展开态:新的展开会挤掉旧的。列表里同时展开多个几乎总是误操作。需要并存时传 exclusive={false} —— 退出互斥的实例既不会挤掉别人,也不会被别人挤掉。

SwipeCellExclusive.tsx
Loading…
import { Cell, SwipeCell, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const SwipeCellExclusive = () => {
  return (
    <View className="gap-4 bg-muted p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <SwipeCell
          trailing={
            <View className="w-20 items-center justify-center bg-destructive">
              <Text className="text-sm text-destructive-foreground">操作 A</Text>
            </View>
          }
        >
          <Cell title="默认互斥 A" />
        </SwipeCell>
        <SwipeCell
          trailing={
            <View className="w-20 items-center justify-center bg-destructive">
              <Text className="text-sm text-destructive-foreground">操作 B</Text>
            </View>
          }
        >
          <Cell title="默认互斥 B" />
        </SwipeCell>
      </View>

      <View className="overflow-hidden rounded-xl border border-border">
        <SwipeCell
          exclusive={false}
          trailing={
            <View className="w-20 items-center justify-center bg-primary">
              <Text className="text-sm text-primary-foreground">操作 C</Text>
            </View>
          }
        >
          <Cell title="可同时展开 C" />
        </SwipeCell>
        <SwipeCell
          exclusive={false}
          trailing={
            <View className="w-20 items-center justify-center bg-primary">
              <Text className="text-sm text-primary-foreground">操作 D</Text>
            </View>
          }
        >
          <Cell title="可同时展开 D" />
        </SwipeCell>
      </View>
    </View>
  );
};

export { SwipeCellExclusive };

命令式控制

ref 暴露两个方法:

方法说明
open(side)展开指定一侧;该侧宽度为 0 时不生效
close()收起,会经过 beforeClose
SwipeCellImperative.tsx
Loading…
import { Button, Cell, SwipeCell, Text } from '@skyroc/native-ui';
import type { SwipeCellInstance } from '@skyroc/native-ui';
import { useRef } from 'react';
import { View } from 'react-native';

const SwipeCellImperative = () => {
  const swipeCellRef = useRef<SwipeCellInstance>(null);

  return (
    <View className="bg-muted">
      <View className="mb-3 flex-row gap-3 px-4">
        <Button
          size="sm"
          onPress={() => swipeCellRef.current?.open('left')}
        >
          打开左侧
        </Button>
        <Button
          size="sm"
          onPress={() => swipeCellRef.current?.open('right')}
        >
          打开右侧
        </Button>
        <Button
          size="sm"
          variant="outline"
          onPress={() => swipeCellRef.current?.close()}
        >
          关闭
        </Button>
      </View>
      <SwipeCell
        ref={swipeCellRef}
        leading={
          <View className="w-16 items-center justify-center bg-primary">
            <Text className="text-sm text-primary-foreground">选择</Text>
          </View>
        }
        trailing={
          <View className="w-16 items-center justify-center bg-destructive">
            <Text className="text-sm text-destructive-foreground">删除</Text>
          </View>
        }
      >
        <Cell
          title="单元格"
          trailing="编程式控制"
        />
      </SwipeCell>
    </View>
  );
};

export { SwipeCellImperative };

事件监听

name 会随回调参数返回,用于在列表里区分实例。onOpenposition 是展开的一侧(left / right),onCloseposition 还多一个 cell,表示是点击主体收起的。

SwipeCellEvents.tsx
Loading…
import { Cell, SwipeCell, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';

const SwipeCellEvents = () => {
  const [lastEvent, setLastEvent] = useState('等待滑动');

  return (
    <View className="bg-muted">
      <SwipeCell
        name="event-demo"
        trailing={
          <>
            <View className="w-16 items-center justify-center bg-primary">
              <Text className="text-sm text-primary-foreground">编辑</Text>
            </View>
            <View className="w-16 items-center justify-center bg-destructive">
              <Text className="text-sm text-destructive-foreground">删除</Text>
            </View>
          </>
        }
        onClose={({ name, position }) => setLastEvent(`onClose · ${name} · ${position}`)}
        onOpen={({ name, position }) => setLastEvent(`onOpen · ${name} · ${position}`)}
      >
        <Cell
          title="单元格"
          trailing="滑动查看事件"
        />
      </SwipeCell>
      <Text className="px-4 pt-3 text-sm text-muted-foreground">{lastEvent}</Text>
    </View>
  );
};

export { SwipeCellEvents };

禁用

disabled 关闭手势,并立即收起已展开的操作区 —— 否则会卡在展开态且手势已经关掉,再也收不回来。

SwipeCellDisabled.tsx
Loading…
import { Cell, SwipeCell, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const SwipeCellDisabled = () => {
  return (
    <View className="bg-muted">
      <SwipeCell
        disabled
        trailing={
          <View className="w-16 items-center justify-center bg-destructive">
            <Text className="text-sm text-destructive-foreground">删除</Text>
          </View>
        }
      >
        <Cell
          title="单元格"
          trailing="禁用状态"
        />
      </SwipeCell>
    </View>
  );
};

export { SwipeCellDisabled };

样式覆盖

className 追加到根容器上,classNames 按 slot 细粒度覆盖:

slot作用位置
root根容器(overflow-hidden,裁掉两侧操作区)
content主体内容容器(默认 bg-card,必须有底色才盖得住下层)
leading左侧操作区容器
trailing右侧操作区容器
overlay展开态盖在主体上的透明遮罩,点击即收起
SwipeCellStyles.tsx
Loading…
import { Cell, SwipeCell, Text } from '@skyroc/native-ui';
import { View } from 'react-native';

const SwipeCellStyles = () => {
  return (
    <View className="bg-background p-4">
      <SwipeCell
        className="rounded-2xl border border-primary-200"
        classNames={{
          content: 'bg-primary-50',
          leading: 'bg-success',
          overlay: 'bg-foreground/5',
          root: 'shadow-sm',
          trailing: 'bg-destructive'
        }}
        leading={
          <View className="w-20 items-center justify-center">
            <Text className="text-sm text-success-foreground">左侧</Text>
          </View>
        }
        trailing={
          <View className="w-20 items-center justify-center">
            <Text className="text-sm text-destructive-foreground">右侧</Text>
          </View>
        }
      >
        <Cell title="滑动查看各 slot" />
      </SwipeCell>
    </View>
  );
};

export { SwipeCellStyles };

放进滚动列表

手势设了纵向失败判定(failOffsetY([-12, 12])),斜向滑动不会把外层 ScrollView 的滚动手势抢走;横向则要划过 10pt 才激活。

API

SwipeCell

属性说明类型默认值
children主体内容ReactNode-
leading左侧操作区内容ReactNode-
trailing右侧操作区内容ReactNode-
leadingWidth左侧操作区宽度,不传则自动测量number-
trailingWidth右侧操作区宽度,不传则自动测量number-
beforeClose关闭前的拦截,返回 false 阻止关闭;互斥收起与 disabled 强制收起不经过(params: SwipeCellCloseParams) => boolean | Promise<boolean>-
exclusive展开时是否自动收起其他 SwipeCellbooleantrue
disabled禁用滑动,并立即收起已展开的操作区booleanfalse
name组件标识,会随事件回调返回number | string''
onOpen展开时触发(params: SwipeCellOpenParams) => void-
onClose收起时触发(params: SwipeCellCloseParams) => void-
className根容器类名,合并在 classNames.root 之后string-
classNames各 slot 的类名覆盖,见「样式覆盖」一节SlotClassNames<SwipeCellSlots>-
style根容器的原生样式StyleProp<ViewStyle>-
ref组件实例引用,用于 open / closeRef<SwipeCellInstance>-

类型

import type {
  SwipeCellBeforeCloseParams,
  SwipeCellCloseParams,
  SwipeCellInstance,
  SwipeCellOpenParams,
  SwipeCellPosition,
  SwipeCellProps,
  SwipeCellSide,
  SwipeCellSlots,
  SwipeCellWidths
} from '@skyroc/native-ui';

SwipeCellSide

滑动展开的一侧。

'left' | 'right'

SwipeCellPosition

触发关闭的来源:cell 为点击主体收起,left / right 为从对应一侧的展开态收起。

'cell' | 'left' | 'right'

SwipeCellSlots

可通过 classNames 覆盖的 slot 名称。

'content' | 'leading' | 'overlay' | 'root' | 'trailing'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 SwipeCellSlots。

Partial<Record<Slots, string>>

SwipeCellCloseParams

beforeClose 与 onClose 的参数;SwipeCellBeforeCloseParams 是它的别名。

字段类型说明
name*number | string组件标识。
position*SwipeCellPosition触发关闭的来源。

SwipeCellOpenParams

onOpen 的参数。

字段类型说明
name*number | string组件标识。
position*SwipeCellSide展开的一侧。

SwipeCellWidths

两侧操作区的实际宽度,由 props 指定或 onLayout 测量得到。

字段类型说明
leading*number左侧操作区宽度。
trailing*number右侧操作区宽度。

SwipeCellInstance

组件实例暴露的方法。

字段类型说明
open*(side: SwipeCellSide) => void展开指定一侧;该侧宽度为 0 时不生效。
close*() => void收起,会经过 beforeClose。

包内还导出了 swipeCellVariants