IndexBar
字母索引列表,右缘悬浮索引条
索引栏(IndexBar)是「分组列表 + 右缘悬浮字母条」的组合,典型场景是城市选择、通讯录。列表本体整套复用 AnchorNav(滚动定位的高度模型、点击侧栏时的程序化滚动抑制、触感策略都只在那边有一份),IndexBar 只负责把「下标」这层身份换成「字母」。
import { IndexBar } from '@skyroc/native-ui';基础用法
items 的每一项是一个分组:title 既是分组标题也是侧栏上的那个字符,因此必须在 items 内唯一。滚动列表时右侧字母会联动高亮,点击字母则滚动到对应分组。
import { Divider, IndexBar, Text } from '@skyroc/native-ui';
import type { IndexBarChild } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { CITY_ITEMS } from './shared';
const IndexBarBasic = () => {
const [activeIndex, setActiveIndex] = useState(CITY_ITEMS[0].title);
const [pressedItem, setPressedItem] = useState<IndexBarChild | null>(null);
return (
<View className="flex-1 bg-background">
<View className="flex-row items-center gap-2 px-4 py-3">
<Text className="text-xs text-muted-foreground">当前索引:{activeIndex}</Text>
<Text className="flex-1 text-right text-xs text-muted-foreground">
{pressedItem ? `点击了 ${pressedItem.text}` : '试着滚动列表看高亮联动'}
</Text>
</View>
<Divider className="my-0" />
<View className="flex-1">
<IndexBar
items={CITY_ITEMS}
onIndexChange={setActiveIndex}
onPressItem={setPressedItem}
/>
</View>
</View>
);
};
export { IndexBarBasic };onIndexChange 的参数是分组的 title(而不是下标),点击侧栏与滚动联动都会触发。onPressItem 是子项点击回调。
IndexBar 需要一个有确定高度的父容器(例如 flex-1 的 View),否则列表撑不开。
何时使用
- 长列表按首字母分组,需要快速跳转:城市、联系人、品牌。
- 分组不需要字母索引条时用
AnchorNav;只有几个分类用Sidebar。
吸顶与触感
sticky(默认 true)控制分组标题吸顶,haptic(默认 true)控制点击侧栏索引时的轻触反馈(仅原生端有效)。
import { Button, Divider, IndexBar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { CITY_ITEMS } from './shared';
const IndexBarBehavior = () => {
const [activeIndex, setActiveIndex] = useState(CITY_ITEMS[0].title);
const [sticky, setSticky] = useState(true);
const [haptic, setHaptic] = useState(true);
return (
<View className="flex-1 bg-background">
<View className="flex-row flex-wrap gap-2 px-4 py-3">
<Button
size="sm"
variant={sticky ? 'solid' : 'outline'}
onPress={() => setSticky(current => !current)}
>
{sticky ? '吸顶已开启' : '吸顶已关闭'}
</Button>
<Button
size="sm"
variant={haptic ? 'solid' : 'outline'}
onPress={() => setHaptic(current => !current)}
>
{haptic ? '触感已开启' : '触感已关闭'}
</Button>
</View>
<View className="flex-row items-center gap-2 px-4 pb-3">
<Text className="text-xs text-muted-foreground">当前索引:{activeIndex}</Text>
<Text className="flex-1 text-right text-xs text-muted-foreground">haptic 仅在原生端触发</Text>
</View>
<Divider className="my-0" />
<View className="flex-1">
<IndexBar
haptic={haptic}
items={CITY_ITEMS}
sticky={sticky}
onIndexChange={setActiveIndex}
/>
</View>
</View>
);
};
export { IndexBarBehavior };高度度量
itemHeight(默认 40)与 sectionHeaderHeight(默认 32)不只是样式:滚动定位按这两个值累加算出目标偏移量,改了样式高度却不改它们,跳转就会偏。
import { Divider, IndexBar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { CITY_ITEMS } from './shared';
const ITEM_HEIGHT = 52;
const SECTION_HEADER_HEIGHT = 40;
const IndexBarMetrics = () => {
const [activeIndex, setActiveIndex] = useState(CITY_ITEMS[0].title);
return (
<View className="flex-1 bg-background">
<View className="flex-row items-center gap-2 px-4 py-3">
<Text className="text-xs text-muted-foreground">当前索引:{activeIndex}</Text>
<Text className="flex-1 text-right text-xs text-muted-foreground">
itemHeight={ITEM_HEIGHT} · sectionHeaderHeight={SECTION_HEADER_HEIGHT}
</Text>
</View>
<Divider className="my-0" />
<View className="flex-1">
<IndexBar
itemHeight={ITEM_HEIGHT}
items={CITY_ITEMS}
sectionHeaderHeight={SECTION_HEADER_HEIGHT}
onIndexChange={setActiveIndex}
/>
</View>
</View>
);
};
export { IndexBarMetrics };自定义子项
renderItem(item, section) 自定义子项内容,外层仍然会被钉在 itemHeight 上,所以内容必须能在这个高度内显示完整(demo 里用 h-full 撑满)。
import { Divider, IndexBar, Text } from '@skyroc/native-ui';
import type { IndexBarChild, IndexBarSection } from '@skyroc/native-ui';
import { useState } from 'react';
import { Pressable, View } from 'react-native';
import { CITY_ITEMS, toAreaCode } from './shared';
/** 自定义子项的高度,同时是 IndexBar 的滚动定位度量,所以只在这里写一次 */
const CUSTOM_ITEM_HEIGHT = 64;
const IndexBarCustomItem = () => {
const [activeIndex, setActiveIndex] = useState(CITY_ITEMS[0].title);
const [pressedItem, setPressedItem] = useState<IndexBarChild | null>(null);
/** 外层已经被钉在 CUSTOM_ITEM_HEIGHT 上,这里只负责把内容撑满并垂直居中 */
function renderCityItem(item: IndexBarChild, section: IndexBarSection) {
return (
<Pressable
className="h-full flex-row items-center gap-3 px-3 active:opacity-80"
onPress={() => setPressedItem(item)}
>
<View className="h-10 w-10 items-center justify-center rounded-full bg-primary/10">
<Text className="text-sm font-semibold text-primary">{section.title}</Text>
</View>
<View className="flex-1 gap-1">
<Text className="text-sm font-medium text-foreground">{item.text}</Text>
<Text className="text-xs text-muted-foreground">区号 {toAreaCode(item.key)}</Text>
</View>
</Pressable>
);
}
return (
<View className="flex-1 bg-background">
<View className="flex-row items-center gap-2 px-4 py-3">
<Text className="text-xs text-muted-foreground">当前索引:{activeIndex}</Text>
<Text className="flex-1 text-right text-xs text-muted-foreground">
{pressedItem ? `点击了 ${pressedItem.text}` : '试着滚动列表看高亮联动'}
</Text>
</View>
<Divider className="my-0" />
<View className="flex-1">
<IndexBar
itemHeight={CUSTOM_ITEM_HEIGHT}
items={CITY_ITEMS}
renderItem={renderCityItem}
sectionHeaderHeight={28}
onIndexChange={setActiveIndex}
/>
</View>
</View>
);
};
export { IndexBarCustomItem };传了 renderItem 后 onPressItem 不再生效 —— 点击交互由自定义内容自行处理。
命令式定位
ref.scrollToIndex(index) 按字母定位,行为与点击侧栏一致(滚动 + 更新高亮);字母不在 items 中时静默忽略。
import { Button, Divider, IndexBar, Text } from '@skyroc/native-ui';
import type { IndexBarChild, IndexBarRef } from '@skyroc/native-ui';
import { useRef, useState } from 'react';
import { View } from 'react-native';
import { CITY_ITEMS } from './shared';
/** 命令式定位的几个落点,取首、中、尾 */
const QUICK_INDEXES = ['A', 'M', 'Z'];
const IndexBarImperative = () => {
const [activeIndex, setActiveIndex] = useState(CITY_ITEMS[0].title);
const [pressedItem, setPressedItem] = useState<IndexBarChild | null>(null);
const indexBarRef = useRef<IndexBarRef>(null);
return (
<View className="flex-1 bg-background">
{/* 对外的定位入口是字母而不是下标 */}
<View className="flex-row items-center gap-3 px-4 py-3">
{QUICK_INDEXES.map(index => (
<Button
key={index}
color="primary"
size="sm"
variant="outline"
onPress={() => indexBarRef.current?.scrollToIndex(index)}
>
{`跳到 ${index}`}
</Button>
))}
</View>
<View className="flex-row items-center gap-2 px-4 pb-3">
<Text className="text-xs text-muted-foreground">当前索引:{activeIndex}</Text>
<Text className="flex-1 text-right text-xs text-muted-foreground">
{pressedItem ? `点击了 ${pressedItem.text}` : '试着滚动列表看高亮联动'}
</Text>
</View>
<Divider className="my-0" />
<View className="flex-1">
<IndexBar
ref={indexBarRef}
items={CITY_ITEMS}
onIndexChange={setActiveIndex}
onPressItem={setPressedItem}
/>
</View>
</View>
);
};
export { IndexBarImperative };样式覆盖
className 作用于根节点,classNames 按 slot 细粒度覆盖:
| slot | 作用位置 |
|---|---|
root | 根节点 |
content | 列表内容容器(右内边距给索引条让位) |
sectionHeader | 分组标题容器 |
sectionHeaderText | 分组标题文字 |
item | 子项容器 |
itemText | 子项文字 |
separator | 子项之间的分隔线 |
sidebar | 右缘悬浮的索引条容器 |
sidebarItem | 单个索引字母的容器 |
sidebarItemText | 索引字母文字(激活态加粗变主题色) |
import { Divider, IndexBar, Text } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { CITY_ITEMS } from './shared';
const IndexBarSlots = () => {
const [activeIndex, setActiveIndex] = useState(CITY_ITEMS[0].title);
return (
<View className="flex-1 bg-background">
<View className="flex-row items-center gap-2 px-4 py-3">
<Text className="text-xs text-muted-foreground">当前索引:{activeIndex}</Text>
</View>
<Divider className="my-0" />
<View className="flex-1">
<IndexBar
className="border-y border-border"
items={CITY_ITEMS}
classNames={{
// 索引条加宽了,列表的右内边距要跟着加宽,否则文字会钻到字母底下
content: 'bg-secondary pr-10',
item: 'mx-2 rounded-xl bg-background px-4',
itemText: 'text-sm font-medium text-primary',
sectionHeader: 'bg-primary/10 px-4',
sectionHeaderText: 'text-sm font-semibold text-primary',
// 只把线藏起来,不动高度:分隔线的占位是滚动定位的度量之一
separator: 'mx-0 my-0 opacity-0',
sidebar: 'w-10',
sidebarItem: 'h-6 w-6',
// 只放大字号,颜色留给 active 变体去决定,覆盖了就分不出激活态了
sidebarItemText: 'text-sm'
}}
onIndexChange={setActiveIndex}
/>
</View>
</View>
);
};
export { IndexBarSlots };三个容易踩的点:
- 加宽
sidebar时要同步加大content的右内边距,否则列表文字会钻到字母底下(两者默认都是 28)。 separator只能改颜色 / 透明度,不要改粗细或外边距 —— 它的占位是滚动定位的度量之一。sidebarItemText覆盖颜色会盖掉激活态,只改字号即可。
索引条是绝对定位悬浮在列表之上的,激活态因此只改文字本身、不加底衬 —— 任何色块都会挡住底下的内容。索引项之间也刻意不留 gap:间隙既会漏出点不到的死区,也会让相邻项的 hitSlop 互相重叠,把点击判给隔壁字母。
无障碍
每个索引字母是 accessibilityRole="button",带 accessibilityLabel(字母本身)与 selected 状态;索引项只有 20dp 宽,横向用 hitSlop 补到 44pt,纵向靠各项首尾相接兜住。
API
IndexBar
除下表外,IndexBar 透传 View 的属性(style、testID 等,children 除外)。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| items* | 分组数据,顺序即侧栏索引顺序 | IndexBarSection[] | - |
| onIndexChange | 激活索引变化回调,参数是该分组的 title;点击侧栏与滚动联动都会触发 | (index: string) => void | - |
| onPressItem | 子项点击回调;传了 renderItem 时不生效 | (item: IndexBarChild) => void | - |
| renderItem | 自定义子项渲染,外层仍会套上 itemHeight 的固定高度 | (item: IndexBarChild, section: IndexBarSection) => ReactNode | - |
| itemHeight | 子项高度;同时是滚动定位的度量,并被强制套在每个子项外层 | number | 40 |
| sectionHeaderHeight | 分组标题高度;同样参与滚动定位计算 | number | 32 |
| sticky | 是否开启分组标题吸顶 | boolean | true |
| haptic | 点击侧栏索引时是否触发轻触反馈(仅原生端) | boolean | true |
| className | 根节点类名 | string | - |
| classNames | 各 slot 的类名覆盖,见「样式覆盖」一节 | SlotClassNames<IndexBarSlots> | - |
| ref | 组件 ref,用于命令式定位 | Ref<IndexBarRef> | - |
类型
import type { IndexBarChild, IndexBarProps, IndexBarRef, IndexBarSection, IndexBarSlots } from '@skyroc/native-ui';IndexBarSlots
可通过 classNames 覆盖的 slot 名称。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 IndexBarSlots。
IndexBarSection
分组数据。
| 字段 | 类型 | 说明 |
|---|---|---|
| title* | string | 索引字母,同时是分组标题与侧栏字符,必须在 items 内唯一。 |
| children* | IndexBarChild[] | 该分组的子项列表。 |
IndexBarChild
子项数据。
| 字段 | 类型 | 说明 |
|---|---|---|
| key* | string | 唯一标识。 |
| text* | string | 显示文本。 |
IndexBarRef
组件实例暴露的方法。
| 字段 | 类型 | 说明 |
|---|---|---|
| scrollToIndex* | (index: string) => void | 定位到指定索引,行为与点击侧栏一致;索引不存在时静默忽略。 |
包内还导出了 indexBarVariants。