Skyroc Native UI

DropdownMenu

贴着标题栏展开的多列筛选菜单

下拉菜单(DropdownMenu)由一排标题和一个下拉面板组成,常见于列表页顶部的排序 / 筛选区。整块菜单只有一个面板,点哪一列就展示哪一列的选项;面板贴着标题栏展开,高度由内容实测后用 Reanimated 做动画,遮罩同步淡入。

import { DropdownMenu } from '@skyroc/native-ui';

基础用法

items 定义有几列、每列有哪些选项。每列的初始值来自 defaultValues[index],没给的那项回退到该列的第一个选项——所以标题不会出现空白。标题默认显示当前选中项的文本。

DropdownMenuBasic.tsx
Loading…
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

DropdownMenuDirection.tsx
Loading…
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。选中态下标题与箭头都会变成主题色并加粗。

DropdownMenuTitle.tsx
Loading…
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) 同样会跳过禁用列。

DropdownMenuDisabled.tsx
Loading…
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 也不会出现先展开过头再回缩。

DropdownMenuScrollable.tsx
Loading…
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 控制背景遮罩(默认 truebg-black/40,点击即关闭),showDivider 控制相邻选项之间的分隔线(默认 true)。关掉遮罩后面板外的内容仍可正常交互,适合嵌在卡片里的轻量筛选。

DropdownMenuNoOverlay.tsx
Loading…
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 拿到的是整个值数组。

DropdownMenuSelectBehavior.tsx
Loading…
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 使用)。

DropdownMenuControlled.tsx
Loading…
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,可以用来同步外部的高亮状态。

DropdownMenuImperative.tsx
Loading…
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(),默认开启),在高频筛选的场景里可以关掉。

DropdownMenuMotion.tsx
Loading…
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选项之间的分隔线
DropdownMenuStyles.tsx
Loading…
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

属性说明类型默认值
items*菜单列表,每一项是一列标题及其选项DropdownMenuItem[]-
values各列当前选中值(受控),下标与 items 对应(DropdownMenuValue | undefined)[]-
defaultValues各列默认选中值,某一列没给时回退到该列的第一个选项(DropdownMenuValue | undefined)[]-
direction展开方向,同时决定面板锚点、圆角与箭头朝向'down' | 'up''down'
closeOnSelect选中后是否自动收起面板booleantrue
overlay是否显示背景遮罩,点击遮罩收起面板booleantrue
showDivider是否显示相邻选项之间的分隔线booleantrue
maxHeight面板最大高度,超出后在面板内部滚动number屏幕高度的 80%
duration展开 / 收起动画时长(毫秒)number200
haptic点击标题与选项时是否触发轻触反馈booleantrue
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 / closeRef<DropdownMenuRef>-

类型

import type {
  DropdownMenuDirection,
  DropdownMenuItem,
  DropdownMenuOption,
  DropdownMenuProps,
  DropdownMenuRef,
  DropdownMenuSlots,
  DropdownMenuValue
} from '@skyroc/native-ui';

DropdownMenuValue

选项值,同时作为选项列表的 key。

number | string

DropdownMenuDirection

面板展开方向,决定锚在标题栏的上沿还是下沿。

'down' | 'up'

DropdownMenuSlots

可通过 classNames 覆盖的 slot 名称,动画容器不在其中。

'arrow' | 'bar' | 'content' | 'divider' | 'option' | 'optionText' | 'overlay' | 'root' | 'selectedIcon' | 'title' | 'titleText'

SlotClassNames

classNames 的取值形态:把 slot 名映射到类名,每个 slot 都可选。本页用到的 slot 见 DropdownMenuSlots。

Partial<Record<Slots, string>>

DropdownMenuItem

一列菜单:一个标题加一组可选值。

字段类型说明
options*DropdownMenuOption[]该列的可选值列表。
titlestring固定标题文本,不设置时显示当前选中项的 text。
keystring标题列表的 key,不传时回退到下标;items 会动态增删时建议传。
disabledboolean禁用整列,标题降低不透明度且不可展开。

DropdownMenuOption

面板中的单个选项。

字段类型说明
value*DropdownMenuValue选项值,同时作为渲染 key。
text*string选项显示文本,也是标题的回退文本。
disabledboolean禁用该选项,不可点击并降低不透明度。

DropdownMenuRef

ref 暴露的命令式方法。

字段类型说明
open*(index: number) => void展开指定索引的面板,索引越界或该列禁用时忽略。
close*() => void收起当前面板,已关闭时不做任何事。