DropdownMenu
贴着标题栏展开的多列筛选菜单
下拉菜单(DropdownMenu)由一排标题和一个下拉面板组成,常见于列表页顶部的排序 / 筛选区。整块菜单只有一个面板,点哪一列就展示哪一列的选项;面板贴着标题栏展开,高度由内容实测后用 Reanimated 做动画,遮罩同步淡入。
import { DropdownMenu } from '@skyroc/native-ui';基础用法
items 定义有几列、每列有哪些选项。每列的初始值来自 defaultValues[index],没给的那项回退到该列的第一个选项——所以标题不会出现空白。标题默认显示当前选中项的文本。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
const SORT_ITEM: DropdownMenuItem = {
key: 'sort',
options: [
{ text: '综合排序', value: 'default' },
{ text: '好评优先', value: 'rating' },
{ text: '销量优先', value: 'sales' }
]
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
]
};
const DropdownMenuBasic = () => {
return (
<View className="bg-background">
<DropdownMenu
defaultValues={['rating', 'all']}
items={[SORT_ITEM, FILTER_ITEM]}
/>
</View>
);
};
export { DropdownMenuBasic };何时使用
- 列表页顶部的排序、筛选、分类切换,需要在不离开当前页的前提下改变列表条件时。
- 每列都是单选,且选项数量适中(十几条以内,再多用
maxHeight让面板内部滚动)。 - 需要弹出层承载复杂表单或多选时,改用
Popup/Sheet;只是一次性的操作列表用ActionSheet。
展开方向
direction 决定面板从标题栏的哪一侧铺开:down 贴在下沿(默认),up 贴在上沿,用于固定在屏幕底部的筛选栏。方向同时翻转三处——面板圆角只留在远离标题栏的那一侧、内容从贴栏的一侧开始堆叠、标题箭头改成 caret-up。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
const SORT_ITEM: DropdownMenuItem = {
key: 'sort',
options: [
{ text: '综合排序', value: 'default' },
{ text: '好评优先', value: 'rating' },
{ text: '销量优先', value: 'sales' }
]
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
]
};
const DropdownMenuDirection = () => {
return (
<View className="bg-background pt-20">
<View>
<DropdownMenu
direction="up"
items={[SORT_ITEM, FILTER_ITEM]}
/>
</View>
</View>
);
};
export { DropdownMenuDirection };面板的定位容器会撑到一整屏高度(useWindowDimensions().height),这是为了兜住遮罩:Android 不会把点击派发给超出父容器范围的子节点,遮罩比容器大就点不动。容器本身是 pointerEvents="box-none",关闭遮罩时整屏范围内的点击照常落到底下的页面上。
自定义标题
item.title 固定这一列的标题文本,不再跟随选中项;省略时标题显示当前选中项的 text。选中态下标题与箭头都会变成主题色并加粗。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
const SORT_ITEM: DropdownMenuItem = {
key: 'sort',
options: [
{ text: '综合排序', value: 'default' },
{ text: '好评优先', value: 'rating' },
{ text: '销量优先', value: 'sales' }
],
title: '排序'
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
],
title: '筛选'
};
const DropdownMenuTitle = () => {
return (
<View className="bg-background">
<DropdownMenu items={[SORT_ITEM, FILTER_ITEM]} />
</View>
);
};
export { DropdownMenuTitle };禁用
item.disabled 禁用整列,标题降到 50% 不透明度且点不开;option.disabled 只禁用单个选项,面板照常展开但该项不可选。命令式的 ref.open(index) 同样会跳过禁用列。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 单个选项禁用:标题能点开,但该项选不中 */
const DISABLED_ITEM: DropdownMenuItem = {
key: 'status',
options: [
{ text: '默认', value: 'default' },
{ disabled: true, text: '已下架', value: 'offline' },
{ text: '热门', value: 'hot' }
],
title: '选项禁用'
};
/** 整列禁用:标题点不开 */
const LOCKED_ITEM: DropdownMenuItem = {
disabled: true,
key: 'locked',
options: [{ text: '暂不可选', value: 'none' }],
title: '暂不可选'
};
const DropdownMenuDisabled = () => {
return (
<View className="bg-background">
<DropdownMenu items={[DISABLED_ITEM, LOCKED_ITEM]} />
</View>
);
};
export { DropdownMenuDisabled };面板高度
面板内容挂在 ScrollView 上,maxHeight 限制它的最大高度,超出后在面板内部滚动,默认是屏幕高度的 80%。展开动画的目标高度取「内容自然高度」与 maxHeight 的较小值,因此设了 maxHeight 也不会出现先展开过头再回缩。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
/** 用来演示面板超高后内部滚动的长列表 */
const CITY_ITEM: DropdownMenuItem = {
key: 'city',
options: Array.from({ length: 30 }, (_, index) => ({
text: `城市 ${index + 1}`,
value: `city-${index + 1}`
})),
title: '城市'
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
]
};
const DropdownMenuScrollable = () => {
return (
<View className="bg-background">
<DropdownMenu
items={[CITY_ITEM, FILTER_ITEM]}
maxHeight={240}
/>
</View>
);
};
export { DropdownMenuScrollable };面板已展开时增删选项,内容会重新触发测量并把高度平滑动画到新值,不需要手动关闭再打开。
遮罩与分隔线
overlay 控制背景遮罩(默认 true,bg-black/40,点击即关闭),showDivider 控制相邻选项之间的分隔线(默认 true)。关掉遮罩后面板外的内容仍可正常交互,适合嵌在卡片里的轻量筛选。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
const SORT_ITEM: DropdownMenuItem = {
key: 'sort',
options: [
{ text: '综合排序', value: 'default' },
{ text: '好评优先', value: 'rating' },
{ text: '销量优先', value: 'sales' }
]
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
]
};
const DropdownMenuNoOverlay = () => {
return (
<View className="bg-background">
<DropdownMenu
items={[SORT_ITEM, FILTER_ITEM]}
overlay={false}
showDivider={false}
/>
</View>
);
};
export { DropdownMenuNoOverlay };关掉遮罩也就没有了「点击外部关闭」的入口,记得留一条别的关闭路径(再点一次标题,或者 ref.close())。
选择行为
closeOnSelect 默认为 true,选中后自动收起面板;设为 false 可以让面板保持展开,适合连续调整多个条件。onSelect 拿到的是列索引和完整的选项对象(含 value / text / disabled),onValuesChange 拿到的是整个值数组。
import { DropdownMenu, Text } from '@skyroc/native-ui';
import type { DropdownMenuItem, DropdownMenuOption } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const STATUS_ITEM: DropdownMenuItem = {
key: 'status',
options: [
{ text: '全部状态', value: 'all' },
{ text: '进行中', value: 'active' },
{ text: '已完成', value: 'done' }
],
title: '保持展开'
};
const DropdownMenuSelectBehavior = () => {
const [selectedText, setSelectedText] = useState('尚未选择');
function handleSelect(_itemIndex: number, option: DropdownMenuOption) {
setSelectedText(option.text);
}
return (
<View className="bg-background pb-4">
<Text className="mb-3 px-4 text-sm text-muted-foreground">最近选择:{selectedText}</Text>
<DropdownMenu
closeOnSelect={false}
items={[STATUS_ITEM]}
onSelect={handleSelect}
/>
</View>
);
};
export { DropdownMenuSelectBehavior };受控模式
传入 values 即进入受控模式,外部状态成为选中值的唯一来源,组件内部不再自行更新——必须在 onValuesChange 里把新值写回去。数组下标与 items 一一对应,某项为 undefined 表示该列未选中(标题会显示空字符串,建议配合 item.title 使用)。
import { Button, DropdownMenu, Text } from '@skyroc/native-ui';
import type { DropdownMenuItem, DropdownMenuValue } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
const SORT_ITEM: DropdownMenuItem = {
key: 'sort',
options: [
{ text: '综合排序', value: 'default' },
{ text: '好评优先', value: 'rating' },
{ text: '销量优先', value: 'sales' }
],
title: '排序'
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
],
title: '筛选'
};
const DropdownMenuControlled = () => {
const [values, setValues] = useState<(DropdownMenuValue | undefined)[]>(['rating', 'promo']);
const controlledTexts = values.map(value => value ?? '-').join(' / ');
function handleReset() {
setValues(['default', 'all']);
}
return (
<View className="bg-background pb-4">
<Text className="mb-3 px-4 text-sm text-muted-foreground">当前值:{controlledTexts}</Text>
<DropdownMenu
items={[SORT_ITEM, FILTER_ITEM]}
values={values}
onValuesChange={setValues}
/>
<View className="mt-3 px-4">
<Button
color="primary"
size="sm"
variant="outline"
onPress={handleReset}
>
重置
</Button>
</View>
</View>
);
};
export { DropdownMenuControlled };命令式控制
ref 暴露 open(index) / close()。open 在索引越界或该列 disabled 时静默忽略,close 在已关闭时不做任何事。onOpenChange 在展开时回调索引、收起时回调 -1,可以用来同步外部的高亮状态。
import { Button, DropdownMenu, Text } from '@skyroc/native-ui';
import type { DropdownMenuItem, DropdownMenuRef } from '@skyroc/native-ui';
import { useRef, useState } from 'react';
import { View } from 'react-native';
const SORT_ITEM: DropdownMenuItem = {
key: 'sort',
options: [
{ text: '综合排序', value: 'default' },
{ text: '好评优先', value: 'rating' },
{ text: '销量优先', value: 'sales' }
],
title: '排序'
};
const FILTER_ITEM: DropdownMenuItem = {
key: 'filter',
options: [
{ text: '全部商品', value: 'all' },
{ text: '新品上架', value: 'new' },
{ text: '活动商品', value: 'promo' }
],
title: '筛选'
};
const DropdownMenuImperative = () => {
const [openIndex, setOpenIndex] = useState(-1);
const menuRef = useRef<DropdownMenuRef>(null);
function handleOpen() {
menuRef.current?.open(1);
}
function handleClose() {
menuRef.current?.close();
}
return (
<View className="bg-background pb-4">
<Text className="mb-3 px-4 text-sm text-muted-foreground">展开索引:{openIndex}</Text>
<View className="mb-3 flex-row gap-3 px-4">
<Button
size="sm"
onPress={handleOpen}
>
展开筛选
</Button>
<Button
size="sm"
variant="outline"
onPress={handleClose}
>
收起
</Button>
</View>
<DropdownMenu
ref={menuRef}
items={[SORT_ITEM, FILTER_ITEM]}
onOpenChange={setOpenIndex}
/>
</View>
);
};
export { DropdownMenuImperative };注意 onOpenChange(-1) 在收起动画开始时就触发,而面板节点要等动画播完才卸载。组件内部用序号校验收起回调,中途被新的展开打断时不会误卸载面板,所以快速连点不同标题也不会闪。
动画与触感
duration 同时控制面板高度、遮罩透明度和标题箭头旋转三条动画的时长(毫秒,默认 200)。haptic 控制点击标题与选项时的轻触反馈(Haptics.selectionAsync(),默认开启),在高频筛选的场景里可以关掉。
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
const MOTION_ITEM: DropdownMenuItem = {
key: 'motion',
options: [
{ text: '选项一', value: 'one' },
{ text: '选项二', value: 'two' },
{ text: '选项三', value: 'three' }
],
title: '600ms 动画'
};
const DropdownMenuMotion = () => {
return (
<View className="bg-background">
<DropdownMenu
duration={600}
haptic={false}
items={[MOTION_ITEM]}
/>
</View>
);
};
export { DropdownMenuMotion };样式覆盖
className 追加到根容器上,classNames 按 slot 细粒度覆盖。
| slot | 作用位置 |
|---|---|
root | 根容器 View(展开时会被加上 z-[100]) |
bar | 标题栏容器 |
title | 单个标题的 Pressable |
titleText | 标题文字 |
arrow | 标题箭头的 colorClassName,只接受 accent-* 颜色类 |
overlay | 背景遮罩 |
content | 面板内容的 ScrollView |
option | 单个选项的 Pressable |
optionText | 选项文字 |
selectedIcon | 选中勾选图标的 colorClassName,只接受 accent-* 颜色类 |
divider | 选项之间的分隔线 |
import { DropdownMenu } from '@skyroc/native-ui';
import type { DropdownMenuItem } from '@skyroc/native-ui';
import { View } from 'react-native';
const STYLE_ITEM: DropdownMenuItem = {
key: 'style',
options: [
{ text: '默认样式', value: 'default' },
{ text: '强调选项', value: 'accent' },
{ text: '柔和选项', value: 'muted' }
],
title: '自定义样式'
};
const DropdownMenuStyles = () => {
return (
<View className="bg-background p-4">
<DropdownMenu
className="rounded-xl border border-primary/20"
classNames={{
arrow: 'accent-primary',
bar: 'rounded-xl bg-primary/5',
divider: 'mx-3',
option: 'mx-2 rounded-lg px-3',
optionText: 'text-primary',
selectedIcon: 'accent-warning',
titleText: 'font-medium text-primary'
}}
items={[STYLE_ITEM]}
/>
</View>
);
};
export { DropdownMenuStyles };动画容器(高度包裹层、内容测量层)不开放覆盖——它们承载的是定位与裁剪,改了会直接破坏展开动画。arrow / selectedIcon 走的是矢量图标,text-* 类对它们无效,必须写 accent-*。
API
DropdownMenu
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| items* | 菜单列表,每一项是一列标题及其选项 | DropdownMenuItem[] | - |
| values | 各列当前选中值(受控),下标与 items 对应 | (DropdownMenuValue | undefined)[] | - |
| defaultValues | 各列默认选中值,某一列没给时回退到该列的第一个选项 | (DropdownMenuValue | undefined)[] | - |
| direction | 展开方向,同时决定面板锚点、圆角与箭头朝向 | 'down' | 'up' | 'down' |
| closeOnSelect | 选中后是否自动收起面板 | boolean | true |
| overlay | 是否显示背景遮罩,点击遮罩收起面板 | boolean | true |
| showDivider | 是否显示相邻选项之间的分隔线 | boolean | true |
| maxHeight | 面板最大高度,超出后在面板内部滚动 | number | 屏幕高度的 80% |
| duration | 展开 / 收起动画时长(毫秒) | number | 200 |
| haptic | 点击标题与选项时是否触发轻触反馈 | boolean | true |
| onOpenChange | 面板展开 / 收起回调,参数为展开列的索引,-1 表示已收起 | (index: number) => void | - |
| onSelect | 选中选项回调,参数为列索引与完整选项对象 | (itemIndex: number, option: DropdownMenuOption) => void | - |
| onValuesChange | 选中值变化回调,参数为整个值数组 | (values: (DropdownMenuValue | undefined)[]) => void | - |
| className | 根容器类名,合并到变体样式之后 | string | - |
| classNames | 各 slot 的类名覆盖,arrow / selectedIcon 作用于矢量图标的 colorClassName,只接受 accent-* 颜色类 | SlotClassNames<DropdownMenuSlots> | - |
| ref | 命令式句柄,暴露 open / close | Ref<DropdownMenuRef> | - |
类型
import type {
DropdownMenuDirection,
DropdownMenuItem,
DropdownMenuOption,
DropdownMenuProps,
DropdownMenuRef,
DropdownMenuSlots,
DropdownMenuValue
} from '@skyroc/native-ui';DropdownMenuValue
选项值,同时作为选项列表的 key。
DropdownMenuDirection
面板展开方向,决定锚在标题栏的上沿还是下沿。
DropdownMenuSlots
可通过 classNames 覆盖的 slot 名称,动画容器不在其中。
SlotClassNames
classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 DropdownMenuSlots。
DropdownMenuItem
一列菜单:一个标题加一组可选值。
| 字段 | 类型 | 说明 |
|---|---|---|
| options* | DropdownMenuOption[] | 该列的可选值列表。 |
| title | string | 固定标题文本,不设置时显示当前选中项的 text。 |
| key | string | 标题列表的 key,不传时回退到下标;items 会动态增删时建议传。 |
| disabled | boolean | 禁用整列,标题降低不透明度且不可展开。 |
DropdownMenuOption
面板中的单个选项。
| 字段 | 类型 | 说明 |
|---|---|---|
| value* | DropdownMenuValue | 选项值,同时作为渲染 key。 |
| text* | string | 选项显示文本,也是标题的回退文本。 |
| disabled | boolean | 禁用该选项,不可点击并降低不透明度。 |
DropdownMenuRef
ref 暴露的命令式方法。
| 字段 | 类型 | 说明 |
|---|---|---|
| open* | (index: number) => void | 展开指定索引的面板,索引越界或该列禁用时忽略。 |
| close* | () => void | 收起当前面板,已关闭时不做任何事。 |