Ant Design Masonry 组件完全指南:瀑布流布局 API、响应式列数与定位算法源码解析
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
Masonry 是 Ant Design 6.0.0 起在Layout分组中新增的瀑布流布局容器组件,用于按“最短列优先”的规则把高度参差不齐的图片、卡片等内容均匀、紧凑地排列进多列网格。本文以 组件文档 为骨架,结合组件源码(Masonry.tsx、usePositions.ts)与全部官方示例,系统讲解其 API、响应式列数规则、内容测量/重排机制与语义化定制,读完即可在真实项目中落地瀑布流场景。
Masonry 是什么:为“高度不齐”的内容而生
瀑布流(Masonry,又称砖石布局)与普通Row/Col栅格最大的区别在于:每一列是**独立“堆叠”**的,新条目总是被放进当前最矮的那一列,从而让整体高度差最小、视觉上最紧凑。文档给出了三类典型的使用场景:
- 展示图片、卡片等高度不规则的内容(如瀑布流相册);
- 需要内容在列方向上均匀分布(避免某一列明显偏长);
- 需要列数随屏幕宽度自动响应。
从组件目录看,Masonry 主要由容器与子项两层实现组成,并通过三个 hooks 协作完成测量与排布:
| 文件 | 职责 |
|---|---|
| Masonry.tsx | 主容器:断点计算、尺寸收集、位置计算、条目渲染编排 |
| MasonryItem.tsx | 单个条目(含children/itemRender优先级与按需ResizeObserver) |
| usePositions.ts | 核心算法:把“条目高度数组”排布为“列 + top”坐标 |
| useRefs.ts | 以 key 为索引的子项 ref 管理 |
| useDelay.ts | 用requestAnimationFrame合并(节流)测量回调 |
| style/index.ts | 组件样式(相对定位容器、透明度与位移动画) |
组件通过 components/index.ts 以Masonry名义导出,类型MasonryProps/MasonryRef一并导出,入口见 masonry/index.tsx。
快速上手:固定列数的基础用法
最直接的用法是:通过items传入数据数组,通过itemRender决定每个数据渲染成什么内容,用columns固定列数、gutter设置间距。官方 basic 示例 完整代码如下:
import React from 'react'; import { Card, Masonry } from 'antd'; import type { MasonryProps } from 'antd'; type MasonryItemType = NonNullable<MasonryProps<number>['items']>[number]; const heights = [150, 50, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 60, 50, 80].map( (height, index) => { const item: MasonryItemType = { key: `item-${index}`, data: height, }; // 某一项可以直接塞 children,优先级高于 itemRender if (index === 4) { item.children = ( <Card size="small" cover={<img alt="food" src="..." />}> <Card.Meta title="I'm Special" description="Let's have a meal" /> </Card> ); } return item; }, ); const App: React.FC = () => ( <Masonry columns={4} gutter={16} items={heights} itemRender={({ data, index }) => ( <Card size="small" style={{ height: data }}> {index + 1} </Card> )} /> ); export default App;需要注意:布局中每个条目虽然最终表现为绝对定位的格子,但子项的真实高度由渲染后的 DOM 实测得到,因此你完全可以只给每张卡片一个不同的固定高度(如示例中 150/50/90… 的数字),或者放一张width: 100%、高度自然变化的图片,组件会自动收敛列高。
API 一览
组件通用属性说明可参考仓库内 Common Props 文档(中文版)。
Masonry 主组件
以下 API 表完整继承自 组件文档:
| 属性 | 说明 | 类型 | 默认值 | 版本 | 全局配置 |
|---|---|---|---|---|---|
| classNames | 为组件内部各语义结构自定义类名,支持对象或函数形式 | Record<SemanticDOM, string>或(info: { props }) => Record<SemanticDOM, string> | - | 6.0.0 | 6.0.0 |
| columns | 列数,可为固定值或响应式配置 | number或{ xs?: number; sm?: number; md?: number } | 3 | - | × |
| fresh | 是否持续监听子项尺寸变化 | boolean | false | - | × |
| gutter | 间距,可为固定值、响应式配置或“水平/垂直间距”配置 | Gap或[Gap, Gap] | 0 | - | × |
| items | 瀑布流条目数据 | MasonryItem[] | - | - | × |
| itemRender | 自定义条目渲染函数 | (item: MasonryItem) => React.ReactNode | - | - | × |
| styles | 为组件内部各语义结构自定义内联样式,支持对象或函数形式 | Record<SemanticDOM, CSSProperties>或(info: { props }) => Record<SemanticDOM, CSSProperties> | - | 6.0.0 | 6.0.0 |
| onLayoutChange | 条目列归属发生变化时的回调 | ({ key: React.Key; column: number }[]) => void | - | - | × |
MasonryItem(items数组中的单个对象)
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| children | 自定义展示内容,优先于itemRender | React.ReactNode | - |
| column | 指定条目归属的列 | number | - |
| data | 自定义数据存储,会作为itemRender回调参数传入 | T | - |
| height | 条目高度 | number | - |
| key | 条目的唯一标识 | string或number | - |
需要说明,Masonry的类型是泛型的:MasonryProps<ItemDataType>。给每个数据指定key是必须的(源码中key: React.Key为必填),它同时作为 DOM 测量的索引与动画 diff 的标识;data则用于承载业务数据。
Gap 类型
Gap表示条目间的间距,可以是一个固定值,也可以是响应式配置:
type Gap = undefined | number | Partial<Record<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl', number>>;若把gutter写成[水平间距, 垂直间距]的二元组,则可以分别控制列间距与行间距。
columns:固定列数与响应式断点匹配规则
columns不传时默认为3(源码 Masonry.tsx 中if (!columns) return 3);传入纯数字则直接使用。
当传入响应式对象时,组件借助useBreakpoint()(实现见 useBreakpoint.tsx,底层媒体查询由 _util/responsiveObserver.ts 维护)收集当前命中的屏幕断点,随后按从大到小的顺序(xxxl → xxl → xl → lg → md → sm → xs)查找第一个“当前已命中且在columns中显式配置了列数”的断点;一个都没命中时回退到columns.xs ?? 1。
该匹配规则决定了书写习惯:给大屏配置的断点要在小屏配置之前被“命中”,因此在对象中只需写出关键档位即可。官方 responsive 示例:
const heights = [120, 55, 85, 160, 95, 140, 75, 110, 65, 130, 90, 145, 55, 100, 80]; const App: React.FC = () => { const items = heights.map((height, index) => ({ key: `item-${index}`, data: height, index, })); return ( <Masonry columns={{ xs: 1, sm: 2, md: 3, lg: 4 }} gutter={{ xs: 8, sm: 12, md: 16 }} items={items} itemRender={(item) => ( <Card size="small" style={{ height: item.data }}> {item.index + 1} </Card> )} /> ); };该示例中columns与gutter都使用响应式对象,随着窗口从手机宽度放大到桌面宽度,瀑布流会依次从单列切换为 2、3、4 列,间距也随之增大。由于断点来自useBreakpoint订阅(实现见 responsiveObserver.ts),窗口变化时组件会自动重算并触发条目重新落位,无需手动刷新。
定位样式与总高度的计算
容器内部每个条目都是绝对定位的。源码中每个条目的位置与尺寸由一组计算表达式得出(Masonry.tsx):
const itemStyle: CSSProperties = { [`--ant-masonry-item-width`]: `calc((100% + ${horizontalGutter}px) / ${columnCount})`, insetInlineStart: `calc(var(--ant-masonry-item-width) * ${columnIndex})`, width: `calc(var(--ant-masonry-item-width) - ${horizontalGutter}px)`, top: position.top, position: 'absolute', };也就是说:每条宽度 = “(容器总宽 + 间距)÷ 列数 − 间距”,水平起点 = 该列下标 × 单列宽度(含间距),垂直起点top则由定位算法给出;容器的height会由算法直接写成“各列中最高的那一列的总高度”(见下节usePositions返回值totalHeight),从而撑起外层布局。另外在 RTL 环境下会自动追加${prefixCls}-rtl样式类(Masonry.tsx)。
gutter:固定值、响应式与双向间距
gutter共有三种写法,官方文档全部支持:
- 固定数值,如
gutter={16},表示行列间距均为 16px; - 二元组,如
gutter={[16, 24]},分别表示水平(列)间距与垂直(行)间距; - 响应式对象,如
gutter={{ xs: 8, sm: 12, md: 16 }}。
源码中 gutter 与 antd 栅格共用同一套解析逻辑:useGutter(gutter, screens)先解析出水平/垂直两组值,再按[horizontalGutter = 0, verticalGutter = horizontalGutter]展开(Masonry.tsx),因此只给单个值时垂直间距会复用水平间距,缺省整体为0。verticalGutter会被传入usePositions参与列高的累加(每放一个条目列高增加height + verticalGutter),以保证行间距真实生效。
内容渲染:children 优先,itemRender 兜底
每个条目到底渲染成什么,由 MasonryItem.tsx 决定:
const renderNode = useMemo(() => { return item.children ?? itemRender?.({ ...item, index, column }); }, [item, itemRender, column, index]);即:item.children优先(适合个别条目的特殊展示,例如上面的“Special”卡片);只有未提供children时才调用itemRender。itemRender收到的回调参数是展开后的完整 item(包含key、data、column、children等),并额外附带该条目的index(数据下标)与column(当前所在列),因此你可以同时拿到业务数据和位置信息。这与文档 API 表里itemRender的类型(item: MasonryItem) => React.ReactNode一致(更完整的 TS 定义为MasonryItem & { index: number },见 Masonry.tsx)。
item.column 钉列与 onLayoutChange:动态增删的“受控回流”
瀑布流默认是“自动落位”的:新条目塞进当前最矮的列。但有些场景(如图片墙里删除某一张)你希望条目保持相对顺序稳定、不至于整体乱跳。官方 dynamic 示例 演示了完整闭环:
<Masonry columns={4} gutter={16} items={items} itemRender={({ data, key }) => ( <Card size="small" style={{ height: data }}> {Number(key) + 1} <Button style={{ position: 'absolute', insetBlockStart: token.paddingSM, insetInlineEnd: token.paddingSM }} size="small" icon={<CloseOutlined />} onClick={() => removeItem(key)} /> </Card> )} onLayoutChange={(sortedItems) => { setItems((prevItems) => prevItems.map((item) => { const matchItem = sortedItems.find((sortedItem) => sortedItem.key === item.key); return matchItem ? { ...item, column: matchItem.column } : item; }), ); }} />点击右上角关闭按钮删除任意条目,点击底部按钮追加一条随机高度数据。这里的关键机制是:
- 写入钉列:初始数据里可为每个 item 附带
column(示例中为index % 4),把条目“钉”到指定列; - 回调回写:当布局因增删而重新排布后,
onLayoutChange会把每个条目的最新key → column关系回传给业务方;业务方据此更新自身 state 中的column字段,再回流给组件,从而让其余条目尽量停在原来的列里,只“补位”而非“全员乱序”。
这个“稳定优先”的设计可以从 usePositions.ts 的注释中读到原作者的意图:
Always get stable positions by order instead of dynamic adjust for next item height.(始终按顺序获得稳定位置,而不是针对下一个条目的高度做动态调整。)
定位算法:选“当前最矮的列”
usePositions.ts 用一个很精简的“贪心”算法完成排布:
const columnHeights = new Array(columnCount).fill(0) as number[]; for (let i = 0; i < itemHeights.length; i += 1) { const [itemKey, itemHeight, itemColumn] = itemHeights[i]; let targetColumnIndex = itemColumn ?? columnHeights.indexOf(Math.min(...columnHeights)); targetColumnIndex = Math.min(targetColumnIndex, columnCount - 1); const top = columnHeights[targetColumnIndex]; itemPositions.set(itemKey, { column: targetColumnIndex, top }); columnHeights[targetColumnIndex] += itemHeight + verticalGutter; }要点有三:
- 初始化一个长度等于列数的“列高数组”,全部为 0;
- 每个条目若自身带
column则钉到该列(并对列下标做Math.min(columnCount - 1)越界保护);否则落在当前列高最小的那一列(indexOf(Math.min(...))保证多列等高时取最左侧列,行为确定); - 落位后把该列高度加上“条目高 + 垂直间距”,直到所有条目处理完;容器总高度取各列高度的最大值(再减去末尾多余的一个垂直间距)。
条目高度数据则来自真实 DOM:collectItemSize遍历每个 ref,用getBoundingClientRect()实测渲染高度(Masonry.tsx),这解释了为什么示例里只要给卡片height样式即可,而不必手动上报高度。
fresh:持续监听尺寸变化及其代价
默认情况下,Masonry 只在“条目集合或列数变化”“容器尺寸变化”“子项图片的 load/error 事件”等时机触发一次重测(容器外层包裹了ResizeObserver,并注册了onLoad/onError事件,见 Masonry.tsx),因此资源开销较小。
但如果你的子项高度会动态变化(例如点击卡片后高度发生过渡动画),就需要开启fresh——此时组件会给每个条目额外套一个ResizeObserver持续监听(MasonryItem.tsx 中onResize ? <ResizeObserver onResize={onResize}>…),并在条目尺寸每次变化后都重新收集高度与落位。
官方 fresh 示例 中的卡片可点击随机改变自身高度:
<Masonry fresh columns={4} gutter={16} items={heights} itemRender={({ data, index }) => <RandomHeightCard index={index} defaultHeight={data} />} /><Card size="small" style={{ height, transition: 'height 0.3s' }} onClick={() => setHeight(...)}>文档示例的中文注释给出了明确提醒:“通过fresh持续监听尺寸变化,会有性能损耗”(见 fresh.md)。因此fresh默认值为false,仅当子项高度确实会运行时变化(动画、折叠、懒加载等)时才需要开启;反之条目较多时应尽量保持关闭,或把高度变化收敛为条目自身的过渡动画,避免每个条目的监听器高频触发全量重排。
测量为何“延迟”?
即使触发了重测,也并非同步执行:collectItemSize通过 useDelay.ts 的raf(callback)包裹,同一帧内多次触发只会合并执行最后一次,避免布局抖动期间产生中间态测量;且只有当新测量结果与上一次不同(isEqual比较)时才会触发 state 更新,减少无谓重渲染。
变更动效:出现 / 离场 / 位移
条目发生移动、新增、删除时,Masonry 会呈现平滑过渡。实现分为两层:
- 动效编排:条目渲染在
@rc-component/motion的CSSMotionList内(Masonry.tsx),开启motionAppear与motionLeave,动效名称为${prefixCls}-item-fade; - 样式定义:style/index.ts 中定义了入场/离场透明度动画(
opacity过渡),以及对“非动效态”条目(即位移中的常规条目)的left/right/top三个方向的位移过渡,时长取自全局motionDurationSlow/motionDurationFast。
因此批量增删条目、或者窗口断点切换导致大量条目换列时,界面会看到淡入淡出与平滑滑动,而不是瞬间跳动。容器本身则使用相对定位 + flex 列换行模型(style/index.ts),其中 flex 主要服务于文档流语义,条目的实际网格坐标仍由内联绝对定位样式驱动。
classNames / styles:按语义结构精准定制
与 antd 5.x/6.x 其他组件一致,Masonry 暴露了root与item两个语义化 DOM 节点(演示见 _semantic.tsx):
- root:根容器元素,承担相对定位、flex 布局与瀑布流容器样式;
- item:单个条目元素,承担绝对定位、宽度计算、过渡动画与瀑布流项目样式。
classNames/styles支持对象与函数两种写法。函数形式会收到{ props }(其中props.columns已被解析为当前生效的具体列数,见 Masonry.tsx 的mergedProps),因此可以依据运行时状态做条件定制。官方 style-class 示例 给出了完整的对象与函数对照:
// 函数形式:根据当前列数动态决定边框颜色 const stylesFn: MasonryProps['styles'] = (info) => { const { props } = info; return { root: { border: `2px solid ${ typeof props.columns === 'number' && props.columns > 2 ? '#1890ff' : '#52c41a' }`, padding: 20, height: 280, backgroundColor: 'rgba(240,248,255,.6)', }, item: { boxShadow: '0 2px 8px rgba(0,0,0,0.1)', border: '1px solid #1890ff', }, }; };- 想定制类名与内联样式的优先级关系(context → prop → root style)与合并顺序,可参考该组件的 semantic 测试,其中验证了
classNames/styles的对象键与函数签名类型,以及语义样式优先级; classNames/styles两个属性自6.0.0起可用,且支持通过ConfigProvider的组件级全局配置(Global Config,同版本 6.0.0)统一注入。
图片瀑布流实战:懒加载友好写法
对“图片高度未知”的典型场景,官方 image 示例 给出了一种零手工测高写法——图片直接以width: 100%渲染、高度由图片自身纵横比自然撑开,再由组件自动测量:
const App = () => ( <Masonry columns={4} gutter={16} items={imageList.map((img, index) => ({ key: `item-${index}`, data: img, }))} itemRender={({ data }) => ( <img src={`${data}?w=523&auto=format`} alt="sample" style={{ width: '100%' }} /> )} /> );由于容器监听了子项图片资源的onLoad/onError,即使图片加载有快有慢,每当有图片加载完成都会触发一次重测并把后续条目平滑下移,最终仍能收敛成紧凑的瀑布流,无需为每张图片预写占位高度。
Design Token 与主题定制说明
组件文档中以<ComponentTokenTable component="Masonry" />挂载了该组件的 Token 表。从 style/index.ts 的实现看,Masonry的ComponentToken当前为空接口(export interface ComponentToken {}),即没有定义组件专属的样式 Token;其样式主要复用 antd 主题的通用运动 Token(motionDurationSlow、motionDurationFast、motionEaseOut)来控制淡入淡出与位移过渡的节奏。因此:
- 若想调整动画快慢,可以通过主题 Token(motion 相关全局 Token)整体生效;
- 若想精细定制列宽、圆角、边框、悬浮态等外观,更推荐直接使用上文介绍的
classNames/styles(针对 root/item 两个语义节点),或对.ant-masonry/.ant-masonry-item类名书写覆盖样式(源码中的前缀类名与genStyleHooks('Masonry', …)接入方式一致)。
小结
把官方文档、官方示例与源码放在一起看,Masonry 的使用模型其实非常清晰:
- 数据驱动:
items(带key与data的数组)+itemRender/children决定“渲染什么”,高度一律交给真实 DOM 测量; - 两套排布模式:不给
column时自动选择“当前最矮列”(见 usePositions.ts);给了column并配合onLayoutChange回写后,可保持相对顺序稳定,适合图片墙类的增删场景; - 三种粒度控制:
columns/gutter支持固定值与响应式断点(大断点优先匹配),fresh决定是否对子项尺寸做持续监听(有性能代价),classNames/styles支持按语义节点做对象/函数式样式定制; - 动画与测量自动收敛:重测经由 rAF 节流,出现/离场/位移均有内置过渡,图片 load/error 也会自动触发重排。
对于需要在 Ant Design 项目中实现相册瀑布流、高度不一的卡片墙或响应式内容墙的开发者,Masonry 是开箱即用的选择;若想进一步深挖,推荐继续阅读 组件源码、定位算法、组件测试 以及其余 官方示例。
【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考