news 2026/9/8 20:28:14

Ant Design Masonry 组件完全指南:瀑布流布局 API、响应式列数与定位算法源码解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ant Design Masonry 组件完全指南:瀑布流布局 API、响应式列数与定位算法源码解析

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.tsrequestAnimationFrame合并(节流)测量回调
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.06.0.0
columns列数,可为固定值或响应式配置number{ xs?: number; sm?: number; md?: number }3-×
fresh是否持续监听子项尺寸变化booleanfalse-×
gutter间距,可为固定值、响应式配置或“水平/垂直间距”配置Gap[Gap, Gap]0-×
items瀑布流条目数据MasonryItem[]--×
itemRender自定义条目渲染函数(item: MasonryItem) => React.ReactNode--×
styles为组件内部各语义结构自定义内联样式,支持对象或函数形式Record<SemanticDOM, CSSProperties>(info: { props }) => Record<SemanticDOM, CSSProperties>-6.0.06.0.0
onLayoutChange条目列归属发生变化时的回调({ key: React.Key; column: number }[]) => void--×

MasonryItem(items数组中的单个对象)

属性说明类型默认值
children自定义展示内容,优先于itemRenderReact.ReactNode-
column指定条目归属的列number-
data自定义数据存储,会作为itemRender回调参数传入T-
height条目高度number-
key条目的唯一标识stringnumber-

需要说明,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> )} /> ); };

该示例中columnsgutter都使用响应式对象,随着窗口从手机宽度放大到桌面宽度,瀑布流会依次从单列切换为 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共有三种写法,官方文档全部支持:

  1. 固定数值,如gutter={16},表示行列间距均为 16px;
  2. 二元组,如gutter={[16, 24]},分别表示水平(列)间距与垂直(行)间距;
  3. 响应式对象,如gutter={{ xs: 8, sm: 12, md: 16 }}

源码中 gutter 与 antd 栅格共用同一套解析逻辑:useGutter(gutter, screens)先解析出水平/垂直两组值,再按[horizontalGutter = 0, verticalGutter = horizontalGutter]展开(Masonry.tsx),因此只给单个值时垂直间距会复用水平间距,缺省整体为0verticalGutter会被传入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时才调用itemRenderitemRender收到的回调参数是展开后的完整 item(包含keydatacolumnchildren等),并额外附带该条目的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; }

要点有三:

  1. 初始化一个长度等于列数的“列高数组”,全部为 0;
  2. 每个条目若自身带column则钉到该列(并对列下标做Math.min(columnCount - 1)越界保护);否则落在当前列高最小的那一列(indexOf(Math.min(...))保证多列等高时取最左侧列,行为确定);
  3. 落位后把该列高度加上“条目高 + 垂直间距”,直到所有条目处理完;容器总高度取各列高度的最大值(再减去末尾多余的一个垂直间距)。

条目高度数据则来自真实 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/motionCSSMotionList内(Masonry.tsx),开启motionAppearmotionLeave,动效名称为${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 暴露了rootitem两个语义化 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 的实现看,MasonryComponentToken当前为空接口(export interface ComponentToken {}),即没有定义组件专属的样式 Token;其样式主要复用 antd 主题的通用运动 TokenmotionDurationSlowmotionDurationFastmotionEaseOut)来控制淡入淡出与位移过渡的节奏。因此:

  • 若想调整动画快慢,可以通过主题 Token(motion 相关全局 Token)整体生效;
  • 若想精细定制列宽、圆角、边框、悬浮态等外观,更推荐直接使用上文介绍的classNames/styles(针对 root/item 两个语义节点),或对.ant-masonry/.ant-masonry-item类名书写覆盖样式(源码中的前缀类名与genStyleHooks('Masonry', …)接入方式一致)。

小结

把官方文档、官方示例与源码放在一起看,Masonry 的使用模型其实非常清晰:

  • 数据驱动items(带keydata的数组)+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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 20:27:01

Codex CLI 完全指南:从安装配置到终端 AI 编程实战

最近 Codex CLI 在开发者圈子里火得很快&#xff0c;很多人的时间线都被它刷屏。作为一个常年泡在终端里干活、能不开 IDE 就绝不开 IDE 的老用户&#xff0c;我也第一时间装上了这玩意儿试了试&#xff0c;结果一用就回不去了。如果你平时主要用命令行工作&#xff0c;又想让 …

作者头像 李华
网站建设 2026/9/8 20:24:00

『Hello アルゴリズム』スタック・キュー章 総まとめ:LIFO/FIFO の核心 5 要点と配列・連結リスト実装の比較、章末 QA をソースコードで徹底解説

『Hello アルゴリズム』スタック・キュー章 総まとめ&#xff1a;LIFO/FIFO の核心 5 要点と配列・連結リスト実装の比較、章末 Q&A をソースコードで徹底解説 【免费下载链接】hello-algo 《Hello 算法》&#xff1a;动画图解、一键运行的数据结构与算法教程。支持简中、繁…

作者头像 李华
网站建设 2026/9/8 20:20:33

极空间NAS部署道理鱼:音乐/MV/有声书全栈媒体库完整指南

一直在折腾家里的极空间NAS&#xff0c;从最开始的纯文件存储&#xff0c;到后来跑Jellyfin看剧、部署各种自动化工具&#xff0c;慢慢感觉这台机器的功能越挖越深。前两天为了给车上的音乐库和跑步时候听的有声书找一个统一入口&#xff0c;盯上了一个叫『道理鱼』&#xff08…

作者头像 李华