Carousel 轮播
包装 react-native-reanimated-carousel@5.0.0-beta.5 的薄壳层,收口一组语义化 prop 屏蔽库内部繁多配置,同时:
- dot indicator 走
c.primarytoken,使用库内置Pagination.Custom(useSharedValue+onProgressChange驱动,无需自己跟 active state;选 Custom 而非 Basic 是为了做出 active dot "4 → 12 长条" 的宽度插值) indicatorPosition控制指示器位置:'bottom'独立行(占额外 16px 高度) /'overlay-bottom-right'浮在 banner 右下角- 类型泛型
<T>让 caller 拿 typed item(库默认unknown) - 后续库 v5 stable / 换其他库,只改本文件 caller 0 改动
实时预览
下方渲染的就是 src/components/ui/Carousel/Carousel.tsx 本体,通过 react-native-web 翻译成浏览器节点。
3 张色块 · autoPlay 3.5s · 底部 dot 指示器
用法
import { Carousel } from '@unif/react-native-design';
import { Image } from 'react-native';
type Banner = { id: string; imageUrl: string; linkUrl?: string };
const banners: Banner[] = [/* … */];
<Carousel<Banner>
items={banners}
height={140}
autoPlay={3500}
showIndicator
indicatorPosition="overlay-bottom-right"
onPressItem={item => {
if (item.linkUrl) openWebview(item.linkUrl);
}}
getAccessibilityLabel={item => item.title ?? 'Banner'}
renderItem={item => (
<Image source={{ uri: item.imageUrl }} style={{ flex: 1 }} />
)}
testID="dashboard-carousel"
/>
API
| Prop | Type | 默认 | 说明 |
|---|---|---|---|
items | ReadonlyArray<T> | — | 数据数组;空数组建议 caller 自行 return null |
renderItem | (item: T, index: number) => ReactNode | — | 单项渲染 |
height | number | — | 每张高度(宽度默认 = 屏宽,被 itemWidth 覆盖) |
itemWidth | number? | useWindowDimensions().width | 每张宽度;caller 外层有 marginHorizontal(例如 Dashboard banner inset 16)时必须传 屏宽 - 左右 margin*2,否则 slide 撑满全屏宽,右侧被裁掉 |
autoPlay | number? | — | 自动播放间隔 ms,undefined / 0 不自动 |
showIndicator | boolean? | true | 是否显示底部 dot indicator |
indicatorPosition | 'bottom' | 'overlay-bottom-right' | 'bottom' | 'bottom' 独立行 +16px / 'overlay-bottom-right' 浮层不占高 |
onPressItem | (item: T, index: number) => void | — | 单项点击回调 |
getAccessibilityLabel | (item: T, index: number) => string | — | 单项 a11y label 解析器,仅 onPressItem 存在时启用(Pressable 自动声明 accessibilityRole='button',纯展示型轮播不声明 button 语义);未传 fallback 第 N 项 |
style | StyleProp<ViewStyle>? | — | 外层 View 附加样式 |
testID | string? | — | E2E / 测试定位,子项自动加 ${testID}-item-${i} |
无障碍(a11y)
来源:src/components/ui/Carousel/Carousel.tsx、types.ts。
每张 slide 都包在一个 <Pressable> 里,但 a11y 语义是条件性的:
- 默认
accessibilityRole:仅当传了onPressItem时,slide 才声明accessibilityRole='button';纯展示型轮播(无onPressItem)不声明 button 语义,避免读屏器误导。 - a11y props:
getAccessibilityLabel(item, index)可选,仅在onPressItem存在时启用 —— 解析为每张 slide 的accessibilityLabel;不传则 fallback 为第 N 项(第 ${index + 1} 项)。 - 状态语义:源码未设
selected/ 当前页等状态语义。
// 可点轮播:role=button 自动,label 由 getAccessibilityLabel 提供
<Carousel
items={banners}
height={140}
onPressItem={(item) => openWebview(item.linkUrl)}
getAccessibilityLabel={(item) => item.title ?? 'Banner'}
renderItem={(item) => <Image source={{ uri: item.imageUrl }} style={{ flex: 1 }} />}
/>
视觉规范
| 元素 | 规则 |
|---|---|
| Dot · 默认 | 4×4 圆(space['1']),c.primary @ opacity 0.32 |
| Dot · active | 12×4 长条(space['5']),c.primary @ opacity 1 |
'bottom' 占位 | 容器高度 = height + 16(space['7']) |
'overlay-bottom-right' | dot 浮层 absolute 在右下角,不占额外高度 |
loop | 默认开启 —— 最后一张滑回第一张 |
主题键(Tokens)
| Token | 来源 | 作用 |
|---|---|---|
c.primary | useColors() | dot indicator 颜色(默认 0.32 / active 1 opacity) |
space['1'] / space['5'] | @unif/react-native-design | dot 尺寸(4)/ active dot 宽(12) |
space['7'] | @unif/react-native-design | 'bottom' 模式指示器预留行高(16) |
radius.pill | @unif/react-native-design | dot 圆角(clamp 成圆) |
备注
- 内部用了
useSharedValue+onProgressChange,re-render 全在 worklet 里走,JS 线程不掉帧 - 库版本固定
5.0.0-beta.5(package.json锁版本),v5 stable 发布后内部 API 可能有 break change —— 改动收敛在Carousel.tsx,caller 不受影响 - 测试中
react-native-reanimated-carousel走 jest mock(参考jest.setup.ts),testID自动生成子项 testIDcarousel-item-${i}可用于断言
不要
- 不要把 Carousel 当
<ScrollView horizontal>用 —— 简单横滚列表请用原生<ScrollView>即可 - 不要传
items.length <= 1—— 单张直接渲染<Image>,不必上 Carousel - 不要忘了
itemWidth—— 一旦外层加了marginHorizontal,默认全屏宽会让 slide 右侧被裁。屏宽 - margin*2是稳妥值,useWindowDimensions()拿屏宽