antd Badge 动态计数变化:从官方示例到 ScrollNumber 滚轮动画原理
本篇以 ant-design(antd)官方 Badge 示例 change(展示动态变化的效果)为主体,完整拆解「徽标数字随状态变化而滚动、圆点随开关显隐」这一实战场景:既给出可直接运行的完整示例代码与参数行为说明,又结合 Badge 组件源码、ScrollNumber 与 SingleNumber 的实现,讲清数字逐位滚动的底层算法与动画样式来源,帮助你在业务中正确控制 count、dot、overflowCount 等属性的显示逻辑。
官方示例:让数字随状态动态变化
示例文档 change.md 的定位只有一句话——「展示动态变化的效果」(The count will be animated as it changes.),其配套实现位于 change.tsx。示例的核心思路是:用一个可变状态 count 驱动 <Badge count={count}>,观察徽标在数字增减时的动画表现;同时用 dot 徽标配合 Switch 验证圆点的显隐过渡。完整代码如下:
import React, { useState } from 'react';
import { MinusOutlined, PlusOutlined, QuestionOutlined } from '@ant-design/icons';
import { Avatar, Badge, Button, Space, Switch } from 'antd';
const App: React.FC = () => {
const [count, setCount] = useState(5);
const [show, setShow] = useState(true);
const increase = () => {
setCount(count + 1);
};
const decline = () => {
let newCount = count - 1;
if (newCount < 0) {
newCount = 0;
}
setCount(newCount);
};
const random = () => {
const newCount = Math.floor(Math.random() * 100);
setCount(newCount);
};
const onChange = (checked: boolean) => {
setShow(checked);
};
return (
<Space vertical>
<Space size="large">
<Badge count={count}>
<Avatar shape="square" size="large" />
</Badge>
<Space.Compact>
<Button onClick={decline} icon={<MinusOutlined />} />
<Button onClick={increase} icon={<PlusOutlined />} />
<Button onClick={random} icon={<QuestionOutlined />} />
</Space.Compact>
</Space>
<Space size="large">
<Badge dot={show}>
<Avatar shape="square" size="large" />
</Badge>
<Switch aria-label="Show badge dot" onChange={onChange} checked={show} />
</Space>
</Space>
);
};
export default App;
示例中有三处值得注意的业务细节:
decline做了下限保护:数字减到 0 后不再继续递减。这里有一个隐藏行为——count为 0 且未设置showZero时,数字徽标会整体隐藏(见下文「隐藏判定与状态缓存」),因此连续点减到最后,数字徽标会以缩放退场动画消失。random制造大幅跳变:Math.floor(Math.random() * 100)在 0–99 之间随机取值,用于观察多位数字同时滚动、以及从一位数跳到三位数时各数位的表现。dot与count是两种互斥形态:<Badge dot={show}>不显示数字,只显示红色圆点;Switch切换show时,圆点同样伴随缩放动画出现/消失,而非直接生硬增删 DOM。
与示例相关的 Badge 关键属性行为
理解示例表现的前提,是弄清楚 Badge 几个属性在源码中的默认值与判定规则(属性声明见 Badge.tsx):
| 属性 | 默认值 | 在示例中的行为 |
|---|---|---|
count |
null |
徽标要展示的数字,类型是 React.ReactNode,因此也可以传自定义节点 |
overflowCount |
99 |
超过该值显示为 99+;示例的 random 最大只能到 99,恰好落在边界 |
showZero |
false |
为 false 时 count === 0 隐藏数字徽标,这也是示例减到 0 后徽标消失的原因 |
dot |
false |
为 true 且非零时显示圆点;示例中 dot={show} 受 Switch 控制 |
size |
medium |
small 时数字更小(-count-sm),default 已废弃并会在开发环境告警 |
源码中对「显示什么、显示多少」的计算链路如下(Badge.tsx):
const numberedDisplayCount = (
(count as number) > (overflowCount as number) ? `${overflowCount}+` : count
) as string | number | null;
const isZero =
numberedDisplayCount === '0' || numberedDisplayCount === 0 || text === '0' || text === 0;
const ignoreCount = count === null || (isZero && !showZero);
也就是说,overflowCount 的截断先于一切显示判定;而 count 为 null(未传)、或为 0 且未开 showZero 时,都会进入 ignoreCount 分支,最终由 isHidden 控制整个指示器的显隐(Badge.tsx):
const showAsDot = dot && !isZero;
const mergedCount = showAsDot ? '' : numberedDisplayCount;
const isHidden = useMemo(() => {
const isEmpty = !isReactRenderable(mergedCount) && !isReactRenderable(text);
return (isEmpty || (isZero && !showZero)) && !showAsDot;
}, [mergedCount, isZero, showZero, showAsDot, text]);
显隐缓存:为什么退场动画期间数字不会「变回」
示例中从 99 减到 0,或 Switch 关闭圆点时,徽标并不会瞬间消失,而是先播放退场动画。这个过程中如果组件直接读取当前 count,动画残留的旧 DOM 可能渲染出错误内容。源码用三个 ref 做了「存活状态缓存」(Badge.tsx):
// Count should be cache in case hidden change it
const countRef = useRef(count);
if (!isHidden) {
countRef.current = count;
}
const livingCount = countRef.current;
// We need cache count since remove motion should not change count display
const displayCountRef = useRef(mergedCount);
if (!isHidden) {
displayCountRef.current = mergedCount;
}
const displayCount = displayCountRef.current;
// We will cache the dot status to avoid shaking on leaved motion
const isDotRef = useRef(showAsDot);
if (!isHidden) {
isDotRef.current = showAsDot;
}
三个缓存只在「可见」时更新:一旦进入 isHidden,count、displayCount、isDot 全部冻结在最后一帧的值上,保证退场动画期间徽标显示的是消失前的正确数字/形态,避免抖动。这对示例中「减到 0 时徽标带动画消失」的表现至关重要。
显隐过渡:CSSMotion 的 zoom 动画
数字与圆点的出现/消失统一包裹在 CSSMotion 中:
<CSSMotion
visible={!isHidden}
motionName={`${prefixCls}-zoom`}
motionAppear={false}
motionDeadline={1000}
>
motionAppear={false}:首次挂载不播入场动画,避免页面加载时的无意义闪烁;motionDeadline={1000}:1 秒兜底,防止transitionend事件丢失导致类名残留;- 对应的动画样式定义在 style/index.ts:
[`${componentCls}-zoom-appear, ${componentCls}-zoom-enter`]: {
animationName: antZoomBadgeIn,
animationDuration: token.motionDurationSlow,
animationTimingFunction: token.motionEaseOutBack,
animationFillMode: 'both',
},
[`${componentCls}-zoom-leave`]: {
animationName: antZoomBadgeOut,
// ... 同上
},
即徽标的显隐走的是「缩放 + easeOutBack 回弹」的 CSS 动画,时长与缓动由主题 token(motionDurationSlow、motionEaseOutBack)驱动,随全局 Motion 配置与暗色/定制主题自动适配。
数字滚动:ScrollNumber 的逐位渲染
示例中最核心的「数字会动」效果并不在 Badge 本体,而在 ScrollNumber——Badge 渲染数字徽标时始终通过它输出(Badge.tsx 中 count={displayCount} 传入 ScrollNumber)。其关键逻辑(ScrollNumber.tsx):
// Only integer need motion
let numberNodes: React.ReactNode = count;
if (count && Number(count) % 1 === 0) {
const numberList = String(count).split('');
numberNodes = (
<bdi>
{numberList.map((num, i) => (
<SingleNumber
prefixCls={prefixCls}
count={Number(count)}
value={num}
key={numberList.length - i}
/>
))}
</bdi>
);
}
这里有两个决定动画成败的设计:
- 只有整数才启用滚动(
Number(count) % 1 === 0)。非整数(如1.5)直接原样渲染,不做逐位动画;字符串计数同理直接展示。 - 每个数位是独立的
SingleNumber,用<bdi>包裹(配合样式中bdi { unicode-bidi: plaintext }处理 RTL 文本方向)。key={numberList.length - i}按「从右往左的位次」生成 key——也就是说,个位数永远是同一个 React 节点,十位数也是同一个。这保证了5 → 65时,个位节点从「显示 5」变为「显示 6」,而新出现的十位节点是全新挂载的,各自独立滚动互不干扰。
另外 ScrollNumber.tsx 还兼容了旧的自定义边框写法:当用户通过 style={{ borderColor }} 给徽标指定边框色时,会转换成 box-shadow: 0 0 0 1px <color> inset 来模拟边框,因为徽标本身用 box-shadow 实现了白色描边效果,普通 border 会破坏圆角形态。
单数位滚动算法:SingleNumber 如何「滚」起来
SingleNumber 实现了类似老式翻牌钟的竖向滚动:每个数位维护「当前值 + 一列可滚动的数字」,通过 transform: translateY 切换显示哪一位。核心步骤(SingleNumber.tsx):
-
记录前后状态:
prevValue/prevCount用 state 保存,直到过渡结束才同步:const onTransitionEnd: React.TransitionEventHandler<HTMLSpanElement> = () => { setPrevValue(value); setPrevCount(count); }; // Fallback if transition events are not supported React.useEffect(() => { const timer = setTimeout(onTransitionEnd, 1000); return () => clearTimeout(timer); }, [value]);除了监听
transitionend,还设了 1 秒超时兜底,浏览器不支持过渡事件时状态也能收敛。 -
无变化则直接静态渲染:
prevValue === value或出现 NaN 时只渲染单个UnitNumber,并关闭 transition,避免首帧抖动。 -
构造数字列并决定方向:
const end = value + 10; const unitNumberList: number[] = []; for (let index = value; index <= end; index += 1) { unitNumberList.push(index); } const unit = prevCount < count ? 1 : -1; // 数字变大向上滚,变小向下滚注意方向取自整体
count的变化(prevCount < count)而非单个数位。这是为了让5 → 65、9 → 10这类跨位跳变时,所有数位滚动方向一致,视觉上更协调。 -
裁剪与偏移计算:以旧值为起点向新值方向切出数字子列,每个
UnitNumber以top: ${offset}00%绝对定位(SingleNumber.tsx);容器再用translateY平移,目标位移由getOffset(SingleNumber.tsx)按 0–9 循环模运算求出「旧值到新值需要滚过几格」:offsetStyle = { transform: `translateY(${-getOffset(prevValue, value, unit)}00%)`, }; -
外层容器裁剪:滚出的数字不会溢出,因为样式里
.ant-scroll-number设置了overflow: hidden,且内层.ant-scroll-number-only的高度固定为indicatorHeight,每个数字单元等高等距排列:[numberPrefixCls]: { overflow: 'hidden', transition: `all ${token.motionDurationMid} ${token.motionEaseOutBack}`, [`${numberPrefixCls}-only`]: { height: indicatorHeight, transition: `all ${token.motionDurationSlow} ${token.motionEaseOutBack}`, WebkitTransformStyle: 'preserve-3d', WebkitBackfaceVisibility: 'hidden', }, },配合
WebkitBackfaceVisibility: hidden与preserve-3d,可缓解部分浏览器在 transform 过渡时的锯齿与闪烁。
实际使用中的边界与注意点
结合以上源码,使用示例中的写法时需要注意几个「源码级」限制:
- 减到 0 的默认行为:
showZero默认为false,count为 0 时数字徽标会整体走退场动画消失;若希望显示0,需显式传showZero。示例中decline特意 clamp 到 0,正是为了展示这一隐藏路径。 overflowCount边界:超过上限显示为字符串99+(默认上限),此时不再逐位滚动——99+是纯文本渲染。示例random的取值 0–99 刚好覆盖「一位数 ↔ 两位数」与99边界的切换。- 非整数/字符串不滚动:
count="99+"、count={1.5}这类非整数会直接静态展示,只有整数才进入逐位滚动链路。 count可以是任意节点:count的类型为React.ReactNode,传入自定义 React 元素时 Badge 会用cloneElement合并样式(Badge.tsx),此时滚动动画不生效,显隐缩放动画依然保留。- 动画时长可随主题调整:显隐 zoom 动画与数字滚动过渡分别使用
motionDurationSlow与motionDurationMid等运动 token,在 ConfigProvider 的theme.token中全局调节 Motion 即可统一影响 Badge 的动效节奏。
小结
change 示例 用最少的代码覆盖了 Badge 的两类动效:数字徽标的逐位滚动(ScrollNumber → SingleNumber 的 translateY 滚轮算法 + overflow: hidden 裁剪)与显隐的 zoom 缩放过渡(CSSMotion + badge-zoom 动画),并借 countRef/displayCountRef 等缓存机制保证退场动画期间内容不抖动。理解了这条链路,你就能在业务中准确预期 Badge 在 count 增减、归零、超上限、自定义节点等各种状态下的表现,并通过主题 Motion token 按需定制动效。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00