跳到主要内容

Carousel 轮播

包装 react-native-reanimated-carousel@5.0.0-beta.5 的薄壳层,收口一组语义化 prop 屏蔽库内部繁多配置,同时:

  1. dot indicator 走 c.primary token,使用库内置 Pagination.Custom(useSharedValue + onProgressChange 驱动,无需自己跟 active state;选 Custom 而非 Basic 是为了做出 active dot "4 → 12 长条" 的宽度插值)
  2. indicatorPosition 控制指示器位置:'bottom' 独立行(占额外 16px 高度) / 'overlay-bottom-right' 浮在 banner 右下角
  3. 类型泛型 <T> 让 caller 拿 typed item(库默认 unknown)
  4. 后续库 v5 stable / 换其他库,只改本文件 caller 0 改动

实时预览

下方渲染的就是 src/components/ui/Carousel/Carousel.tsx 本体,通过 react-native-web 翻译成浏览器节点。

3 张色块 · autoPlay 3.5s · 底部 dot 指示器
A
B
C

用法

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

PropType默认说明
itemsReadonlyArray<T>数据数组;空数组建议 caller 自行 return null
renderItem(item: T, index: number) => ReactNode单项渲染
heightnumber每张高度(宽度默认 = 屏宽,被 itemWidth 覆盖)
itemWidthnumber?useWindowDimensions().width每张宽度;caller 外层有 marginHorizontal(例如 Dashboard banner inset 16)时必须传 屏宽 - 左右 margin*2,否则 slide 撑满全屏宽,右侧被裁掉
autoPlaynumber?自动播放间隔 ms,undefined / 0 不自动
showIndicatorboolean?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 项
styleStyleProp<ViewStyle>?外层 View 附加样式
testIDstring?E2E / 测试定位,子项自动加 ${testID}-item-${i}

无障碍(a11y)

来源:src/components/ui/Carousel/Carousel.tsxtypes.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 · active12×4 长条(space['5']),c.primary @ opacity 1
'bottom' 占位容器高度 = height + 16(space['7'])
'overlay-bottom-right'dot 浮层 absolute 在右下角,不占额外高度
loop默认开启 —— 最后一张滑回第一张

主题键(Tokens)

Token来源作用
c.primaryuseColors()dot indicator 颜色(默认 0.32 / active 1 opacity)
space['1'] / space['5']@unif/react-native-designdot 尺寸(4)/ active dot 宽(12)
space['7']@unif/react-native-design'bottom' 模式指示器预留行高(16)
radius.pill@unif/react-native-designdot 圆角(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 自动生成子项 testID carousel-item-${i} 可用于断言

不要

  • 不要把 Carousel 当 <ScrollView horizontal> 用 —— 简单横滚列表请用原生 <ScrollView> 即可
  • 不要传 items.length <= 1 —— 单张直接渲染 <Image>,不必上 Carousel
  • 不要忘了 itemWidth —— 一旦外层加了 marginHorizontal,默认全屏宽会让 slide 右侧被裁。屏宽 - margin*2 是稳妥值,useWindowDimensions() 拿屏宽