Skyroc Native UI

Grid

宫格容器,用等宽格子铺排图标入口与快捷功能

宫格(Grid)把一组入口按固定列数平铺开来,是首页九宫格、工具箱、快捷菜单这类界面的基础容器。组件由 View + Pressable 封装:items 里某项传了 onPress / onLongPress,或整体开启 clickable,该格子才渲染成 Pressable,否则退化为纯展示的 View —— 静态宫格不会凭空多出可聚焦节点。

列宽由 flexBasis: 100 / columnNum + '%' 计算,不依赖测量,首帧就是最终布局。

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

基础用法

items 是一个数组,每项至少要有 key,再按需给 icontext。默认每行四列、内容纵向排列并居中。

GridBasic.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const GridBasic = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid items={BASIC_ITEMS} />
      </View>
    </View>
  );
};

export { GridBasic };

textstringnumber 时自动包一层 Text 承接主题字号与颜色;传自定义节点则原样渲染。数字 0 也会被正确包裹 —— 组件用 isNil 判空而不是真值判断,不会出现 RN 的 Text strings must be rendered within a <Text> 报错。

何时使用

  • 首页金刚区、工具箱、支付类应用的功能入口这类「图标 + 文字」的等宽平铺。
  • 需要每格可点击时给单项传 onPress,不要在 children 里再套一层 Pressable
  • 纯展示的图表图例、数据看板也可以用,不传任何回调即可。
  • 只有一行、宽度不需要等分时用 Space;「标签 + 值」的纵向列表用 Cell

列数

columnNum 决定每行格数,默认 4。每格宽度是 100 / columnNum 的百分比,因此 items 数量不是列数整数倍时,末行左对齐排列、右侧留空。

GridColumnNum.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { GRID_ITEMS } from './shared';

const GridColumnNum = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          columnNum={3}
          items={GRID_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridColumnNum };

分隔线

border 在格子之间画 StyleSheet.hairlineWidth 粗细的分隔线,颜色取自 item slot 的 border-border,跟随主题 token。画线规则:

位置右侧竖线底部横线
每行最后一格不画按行判断
整体最后一格(末行不满时)不画不画
末行按列判断不画
其余
GridBorder.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { SEVEN_ITEMS } from './shared';

const GridBorder = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          border
          items={SEVEN_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridBorder };

示例里放了 7 项、每行 4 列,末行只有 3 个格子 —— 第 7 格右侧不会留一条悬在空白区的竖线。

Grid 自身不带外框,需要一圈边框和圆角时在外层容器上加 overflow-hidden rounded-xl border border-border

间距

gutter 以 dp 为单位设置格间距离。实现方式是「每格四周内边距 gutter / 2 + 根节点四周负外边距 gutter / 2」,而不是给格子加 paddingRight

  • 负外边距抵消最外圈留白,宫格边缘仍与相邻内容贴边对齐,不会整体内缩半个间距。
  • 间距是格子的内边距而非外边距,末列不会多出一条空白、内容也不会被挤向左侧。
GridGutter.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { GRID_ITEMS } from './shared';

const GridGutter = () => {
  return (
    <View className="bg-background p-4">
      <View>
        <Grid
          gutter={12}
          items={GRID_ITEMS}
          classNames={{ content: 'rounded-xl border border-border bg-background' }}
        />
      </View>
    </View>
  );
};

export { GridGutter };

有间距时格子的可视区域是 content 而不是 item,所以背景、圆角、描边要通过 classNames.content 传(示例里就是这么做的);写在 classNames.item 上会把间距一起圈进去。

gutter > 0 时根节点的 style 会变成 [{ margin: -gutter / 2 }, style],外部传入的 style 排在后面,仍可以覆盖这个负外边距。

间距与分隔线

bordergutter 可以同时使用。分隔线画在格子外框上、边框位于内边距之外,因此线正好落在相邻格子间距的中线,而不是贴着内容。

GridGutterBorder.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { GRID_ITEMS } from './shared';

const GridGutterBorder = () => {
  return (
    <View className="bg-background p-4">
      <View className="rounded-xl bg-muted p-2">
        <Grid
          border
          gutter={16}
          items={GRID_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridGutterBorder };

正方形

square 给内容区加 aspect-square,让每格的可视区域保持 1:1。

GridSquare.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const GridSquare = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          border
          square
          items={BASIC_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridSquare };

注意正方形约束落在 content 而不是 item 上:gutter 是外框的内边距,若把 aspect-square 画在外框上,间距会被算进正方形里,视觉上格子被压扁。

排列方向

direction 控制格子内图标与文字的排布方向,默认 'vertical'(图标在上、文字在下,间距 8dp)。传 'horizontal' 改为左右排列,图标的间距同步从 mb-2 换成 mr-2

GridHorizontal.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const GridHorizontal = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          border
          columnNum={2}
          direction="horizontal"
          items={BASIC_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridHorizontal };

direction 只影响格子内部,不影响格子本身的铺排方向 —— 宫格永远是从左到右、逐行换行。

反向排列

reverse 翻转图标与文字的顺序:纵向变成文字在上、图标在下,横向变成文字在左、图标在右。图标的外边距会跟着换边(mb-2mt-2mr-2ml-2),不会把空隙留在文字的另一侧。

GridReverse.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const GridReverse = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          reverse
          items={BASIC_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridReverse };

内容对齐

center 默认为 true,格子内容水平垂直都居中。传 false 时内容按起始位置对齐(items-start justify-start),适合文字长度不一、希望左对齐的场景。

GridAlign.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const GridAlign = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          border
          center={false}
          items={BASIC_ITEMS}
        />
      </View>
    </View>
  );
};

export { GridAlign };

交互与禁用

格子在下面任一条件成立时渲染为 Pressable

  • Grid 传了 clickable
  • 该项自己传了 onPressonLongPress

可点击的格子带 active:opacity-70 的按压反馈。disabled 让该项降到 40% 不透明度,同时给 Pressabledisabled 阻断回调 —— 并且会撤掉按压反馈类,不会出现「按下去有反应但回调不触发」的错觉。

GridPress.tsx
Loading…
import { Grid, Text } from '@skyroc/native-ui';
import type { GridItemData } from '@skyroc/native-ui';
import { useState } from 'react';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const GridPress = () => {
  const [message, setMessage] = useState('尚未触发');

  function handlePress() {
    setMessage('触发 onPress');
  }

  function handleLongPress() {
    setMessage('触发 onLongPress');
  }

  function handleDisabledPress() {
    setMessage('禁用项不应触发');
  }

  function getItems(): GridItemData[] {
    return [
      { ...BASIC_ITEMS[0], onPress: handlePress },
      { ...BASIC_ITEMS[1], onLongPress: handleLongPress },
      BASIC_ITEMS[2],
      { ...BASIC_ITEMS[3], disabled: true, onPress: handleDisabledPress }
    ];
  }

  return (
    <View className="bg-background p-4">
      <Text className="mb-3 text-sm text-muted-foreground">最近交互:{message}</Text>
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          border
          clickable
          items={getItems()}
        />
      </View>
    </View>
  );
};

export { GridPress };

不可点击的格子传 disabled 只有视觉降透明度的效果。

自定义内容

children 优先于 icontext:传了 children 时后两者被完全忽略,格子内容由你自己决定。children 仍然渲染在 content 容器内,因此 centersquaredirection 这些排布类依旧生效。

GridCustomContent.tsx
Loading…
import { Grid, Text } from '@skyroc/native-ui';
import { View } from 'react-native';
import { DemoIcon } from './shared';

const GridCustomContent = () => {
  return (
    <View className="bg-background p-4">
      <View className="overflow-hidden rounded-xl border border-border">
        <Grid
          columnNum={2}
          items={[
            {
              children: (
                <View className="w-full rounded-xl bg-primary/10 p-3">
                  <Text className="text-sm font-semibold text-primary">自定义内容</Text>
                  <Text className="mt-1 text-xs text-muted-foreground">可承载任意 ReactNode</Text>
                </View>
              ),
              key: 'custom'
            },
            {
              icon: (
                <DemoIcon
                  label="0"
                  tone="success"
                />
              ),
              key: 'zero',
              text: 0
            }
          ]}
        />
      </View>
    </View>
  );
};

export { GridCustomContent };

样式覆盖

GridclassName 作用于根节点,其余部位走 classNames;单项还可以用 item.classNames 再覆盖一层。

slot作用位置
root根节点(classNameclassNames.root 都落在这里)
item格子外框,负责列宽、间距与分隔线
content格子内的可视区域,负责内边距与图标文字排布
icon图标容器(仅传了 icon 时渲染)
text文字(仅 textstring / number 时生效)

合并顺序为「变体类 → Grid.classNamesitem.classNames」,后者优先。item.classNames 的可选 slot 是 GridItemSlots,比 GridSlots 少一个 root —— 根节点是整个宫格共有的,不该由某一项覆盖。

GridStyles.tsx
Loading…
import { Grid } from '@skyroc/native-ui';
import type { GridItemData } from '@skyroc/native-ui';
import { View } from 'react-native';
import { BASIC_ITEMS } from './shared';

const STYLE_ITEMS: GridItemData[] = [
  BASIC_ITEMS[0],
  {
    ...BASIC_ITEMS[1],
    classNames: {
      content: 'bg-warning/10',
      icon: 'opacity-70',
      text: 'text-warning'
    }
  }
];

const GridStyles = () => {
  return (
    <View className="bg-background p-4">
      <Grid
        className="rounded-xl bg-primary/5 p-2"
        classNames={{
          content: 'rounded-xl bg-background p-3',
          icon: 'opacity-80',
          item: 'rounded-xl border border-primary/20',
          text: 'font-medium text-primary'
        }}
        columnNum={2}
        gutter={8}
        items={STYLE_ITEMS}
      />
    </View>
  );
};

export { GridStyles };

列宽、gutter 内边距、分隔线粗细依赖运行时的 columnNum / gutter,无法用类名表达,因此写在 itemstyle 上。用 classNames.itemp-*w-*border-* 会被这些内联样式覆盖,改间距请用 gutter

无障碍

可点击的格子设置 accessibilityRole="button",并把 disabled 映射到 accessibilityState.disabled;不可点击时不设置 role,读屏器按普通内容播报。

每项都可以传 accessibilityLabel,未提供时读屏器朗读格子内的文本。图标是纯装饰节点、或用 children 自定义内容且里面没有文字时,建议显式传 accessibilityLabel

组件没有为格子额外扩大热区:可点击区域就是整个格子外框(含 gutter 的内边距),在默认 p-4 内边距下已远超 44dp 的推荐尺寸。

API

Grid

除下表外,Grid 透传 ViewProps 的其余属性(children 除外)到根节点。

属性说明类型默认值
items*宫格数据项GridItemData[]-
columnNum每行列数,格宽为 100 / columnNum 的百分比number4
border是否在格子之间显示分隔线;有 gutter 时落在间距中线booleanfalse
gutter格子之间的间距(dp),由格子内边距 + 容器负外边距实现number0
direction格子内图标与文字的排列方向'horizontal' | 'vertical''vertical'
reverse是否翻转图标与文字的顺序,并同步换边图标间距booleanfalse
center内容是否在格子内居中,false 时按起始位置对齐booleantrue
square是否将格子内容区固定为正方形(不含 gutter)booleanfalse
clickable是否让所有格子可点击并带按压反馈;单项有回调时无需开启booleanfalse
classNameUniwind className,作用于根节点string-
classNames覆盖各 slot 的类名SlotClassNames<GridSlots>-
style根节点内联样式;gutter > 0 时排在负外边距之后,可覆盖它StyleProp<ViewStyle>-
ref根节点的 ref,用于 measure / 滚动定位等命令式操作Ref<View>-

类型

import type { GridDirection, GridItemData, GridItemSlots, GridProps, GridSlots } from '@skyroc/native-ui';

GridDirection

格子内图标与文字的排列方向。

'horizontal' | 'vertical'

GridSlots

Grid 可通过 classNames 覆盖的 slot 名称。

'content' | 'icon' | 'item' | 'root' | 'text'

GridItemSlots

单项可通过 item.classNames 覆盖的 slot 名称,根节点只能由 Grid 自身的 classNames 覆盖。

Exclude<GridSlots, 'root'>

SlotClassNames

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

Partial<Record<Slots, string>>

GridItemData

单个宫格项的数据结构。

字段类型说明
key*string唯一标识,用作列表 key;增删重排时靠它保持节点身份
iconReactNode图标区域内容
textReactNode文字内容,string / number 自动包裹 Text
childrenReactNode自定义子元素,提供后忽略 icon 与 text
onPress() => void点击回调,传入后该格子渲染为 Pressable
onLongPress() => void长按回调,同样会让该格子变成可点击的 Pressable
disabledboolean禁用交互并整体降低透明度;可点击时同时阻断回调
classNamesSlotClassNames<GridItemSlots>覆盖该项各 slot 的类名
accessibilityLabelstring无障碍标签,未提供时读屏朗读子节点文本
testIDstring测试标识