SwipeCell
可左右滑动的单元格,露出两侧操作区
滑动单元格(SwipeCell)横向滑动主体内容,露出左右两侧的操作区(常见于列表项的「删除 / 置顶」)。手势基于 react-native-gesture-handler,位移由 react-native-reanimated 的弹簧动画驱动,两侧操作区固定在底层,内容区盖在上面平移。
import { SwipeCell } from '@skyroc/native-ui';组件内部用到了 GestureDetector,请确保 App 根节点已经包了 GestureHandlerRootView。
基础用法
leading / trailing 分别是左右操作区,宽度默认由内容 onLayout 自动测量。展开态点击主体内容即可收起。
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() 三条路径都会经过它。
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 跳过自动测量、直接指定停靠距离。
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} —— 退出互斥的实例既不会挤掉别人,也不会被别人挤掉。
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 |
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 会随回调参数返回,用于在列表里区分实例。onOpen 的 position 是展开的一侧(left / right),onClose 的 position 还多一个 cell,表示是点击主体收起的。
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 关闭手势,并立即收起已展开的操作区 —— 否则会卡在展开态且手势已经关掉,再也收不回来。
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 | 展开态盖在主体上的透明遮罩,点击即收起 |
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 | 展开时是否自动收起其他 SwipeCell | boolean | true |
| disabled | 禁用滑动,并立即收起已展开的操作区 | boolean | false |
| name | 组件标识,会随事件回调返回 | number | string | '' |
| onOpen | 展开时触发 | (params: SwipeCellOpenParams) => void | - |
| onClose | 收起时触发 | (params: SwipeCellCloseParams) => void | - |
| className | 根容器类名,合并在 classNames.root 之后 | string | - |
| classNames | 各 slot 的类名覆盖,见「样式覆盖」一节 | SlotClassNames<SwipeCellSlots> | - |
| style | 根容器的原生样式 | StyleProp<ViewStyle> | - |
| ref | 组件实例引用,用于 open / close | Ref<SwipeCellInstance> | - |
类型
import type {
SwipeCellBeforeCloseParams,
SwipeCellCloseParams,
SwipeCellInstance,
SwipeCellOpenParams,
SwipeCellPosition,
SwipeCellProps,
SwipeCellSide,
SwipeCellSlots,
SwipeCellWidths
} from '@skyroc/native-ui';SwipeCellSide
滑动展开的一侧。
SwipeCellPosition
触发关闭的来源:cell 为点击主体收起,left / right 为从对应一侧的展开态收起。
SwipeCellSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 SwipeCellSlots。
SwipeCellCloseParams
beforeClose 与 onClose 的参数;SwipeCellBeforeCloseParams 是它的别名。
| 字段 | 类型 | 说明 |
|---|---|---|
| name* | number | string | 组件标识。 |
| position* | SwipeCellPosition | 触发关闭的来源。 |
SwipeCellWidths
两侧操作区的实际宽度,由 props 指定或 onLayout 测量得到。
| 字段 | 类型 | 说明 |
|---|---|---|
| leading* | number | 左侧操作区宽度。 |
| trailing* | number | 右侧操作区宽度。 |
SwipeCellInstance
组件实例暴露的方法。
| 字段 | 类型 | 说明 |
|---|---|---|
| open* | (side: SwipeCellSide) => void | 展开指定一侧;该侧宽度为 0 时不生效。 |
| close* | () => void | 收起,会经过 beforeClose。 |
包内还导出了 swipeCellVariants。