动效
动效只用来说明三件事:东西从哪来、到哪去,以及状态变了。不做纯装饰的循环动画;计时器的呼吸点是唯一的例外,它表示「正在计时」。
时长和曲线按场景取,不在组件里写死数字:
- Web 用 CSS 变量
--motion-<场景>,它是「时长 + 曲线」,可以直接写进transition和animation; - Flutter 用
BiTransitions.of(context).<场景>,它会按当前风格取值,系统要求减少动态效果时自动退化; - 其他代码读
@bostonivy/design的motion[风格][场景]。
基础 token
| Token | 值 | 用在 |
|---|---|---|
duration-instant |
80ms | 像素风格的状态切换、复选框。 |
duration-fast |
120ms | 悬停、按下、标签出现。 |
duration-base |
200ms | 面板展开、卡片移动。 |
duration-slow |
320ms | 模态和侧滑面板进入。 |
easing-standard |
cubic-bezier(0.2, 0, 0, 1) |
青藤:默认缓动。 |
easing-enter |
cubic-bezier(0, 0, 0, 1) |
元素进入。 |
easing-exit |
cubic-bezier(0.3, 0, 1, 1) |
元素离开。 |
easing-pop |
cubic-bezier(0.34, 1.56, 0.64, 1) |
贴纸:带一点回弹,用于完成勾选和贴纸落下。 |
easing-linear |
cubic-bezier(0, 0, 1, 1) |
像素:不做缓动,只用 instant 时长。 |
场景
每格是「时长 曲线,位移,起始缩放」。CSS 变量是 --motion-<场景>(位移、缩放、逐行间隔另有 -shift、-scale、-stagger),Flutter 是 BiTransitions.<风格>.<场景>。
| 场景 | 名字 | 用在 | 青藤 | 萌虎 | 像素 | 减少动态效果时 |
|---|---|---|---|---|---|---|
| 悬停和按下 | hover |
悬停、按下、选中时的颜色、底色和描边变化。 | 120ms standard |
120ms standard |
80ms linear |
照旧 |
| 菜单出现 | popover-enter |
菜单、日期选择、工具提示出现:淡入,朝打开方向移入 4px,缩放 0.97 → 1。 | 120ms enter,4px,0.97 |
120ms enter,4px,0.97 |
80ms linear |
只淡入淡出 |
| 菜单消失 | popover-exit |
菜单、日期选择、工具提示消失:淡出,不位移。 | 80ms exit |
80ms exit |
80ms linear |
只淡入淡出 |
| 对话框出现 | dialog-enter |
对话框出现:遮罩淡入,面板淡入并从 0.97 放大到 1。 | 200ms enter,0.97 |
200ms enter,0.97 |
80ms linear |
只淡入淡出 |
| 对话框关闭 | dialog-exit |
对话框关闭:遮罩和面板一起淡出。 | 120ms exit |
120ms exit |
80ms linear |
只淡入淡出 |
| 侧栏滑入 | panel-enter |
侧栏和任务详情面板滑入:从所在的一边移入整个宽度。 | 320ms enter,整宽 |
320ms enter,整宽 |
80ms linear |
只淡入淡出 |
| 侧栏滑出 | panel-exit |
侧栏和任务详情面板滑出。 | 200ms exit,整宽 |
200ms exit,整宽 |
80ms linear |
只淡入淡出 |
| 列表新增 | list-insert |
列表新增一行(新建任务、Agent 建的一批任务到达):高度展开,同时淡入并下移 4px。一批超过 6 行时,第 7 行起不再逐行出现,和第 6 行一起出现。 | 200ms standard,4px,逐行 24ms |
200ms standard,4px,逐行 24ms |
80ms linear |
只淡入淡出 |
| 列表删除 | list-remove |
列表删除一行:先淡出,再用 list-move 的时长收起高度。 | 120ms exit |
120ms exit |
80ms linear |
只淡入淡出 |
| 换位置 | list-move |
行或卡片换位置:排序、拖到另一列、完成后移到「已完成」分组。从旧位置移到新位置。 | 200ms standard |
200ms pop |
80ms linear |
直接切换 |
| 展开和收起 | expand |
展开和收起:Agent 动态的改动列表、计划预览的子任务、评论的回复。高度变化,内容淡入;箭头旋转用 hover 的时长。 | 200ms standard |
200ms standard |
80ms linear |
直接切换 |
| 轻提示出现 | toast-enter |
轻提示出现:从底部上移 8px 并淡入。 | 200ms enter,8px |
200ms enter,8px |
80ms linear |
只淡入淡出 |
| 轻提示消失 | toast-exit |
轻提示消失:淡出。 | 120ms exit |
120ms exit |
80ms linear |
只淡入淡出 |
| 完成任务 | complete |
完成任务:勾选框填色并画勾,标题加删除线;停 200ms 后用 list-move 移到「已完成」。 | 120ms standard |
120ms pop |
80ms linear |
照旧 |
进入和退出
- 退出比进入快:对话框进 200ms、出 120ms,菜单进 120ms、出 80ms。人在等东西出现,不在等它消失。
- 进入用
easing-enter(先快后慢,落定时柔和),退出用easing-exit(先慢后快,干脆离开),原地变化(颜色、展开、换位)用easing-standard。 - 移动的方向说明来处:菜单、日期选择从触发按钮的方向移入 4px;轻提示从底部上移 8px;侧栏和任务详情从所在的一边整宽移入(从右往左时方向对调);对话框不移动,只从 0.97 放大。
- Web 参考实现里的出现动画都用同一个关键帧
bi-appear,它读--bi-shift(位移)和--bi-scale(起始缩放):
.my-popover { animation: bi-appear var(--motion-popover-enter) both;
--bi-shift: calc(-1 * var(--motion-popover-enter-shift)); --bi-scale: var(--motion-popover-enter-scale); }
- Flutter:
showBiDialog和showBiToast已经按对话框和轻提示的场景出入;其他地方用BiTransitions.of(context)取时长和曲线:
final t = BiTransitions.of(context).popoverEnter;
AnimatedSlide(duration: t.duration, curve: t.curve, offset: Offset(0, open ? 0 : -t.shift / height), child: menu);
列表:新增、删除、移动
- 新增一行(新建任务、Agent 建好的一批任务到达):高度展开,同时淡入并下移 4px。一批里逐行出现,每行间隔 24ms,第 7 行起和第 6 行一起出现,免得一批 30 个任务要等上一秒。Web 给新行加
bi-anim-insert并设--bi-index(批内序号);Flutter 用listInsert.delayFor(index)作为每行的延迟。 - 删除一行:先用 120ms 淡出,再用 200ms 收起高度,下面的行跟着上移。
- 换位置(排序、拖到另一列、完成后移到「已完成」):从旧位置移到新位置,200ms。拖动中的卡片抬起,用
shadow-raised;萌虎倾斜 3°,像素改成琥珀色描边。 - 完成任务:勾选框填色并画勾,标题加删除线(
complete,120ms),停 200ms,再用list-move移到「已完成」分组。萌虎的勾选框额外旋转 −6° 并轻微放大。
展开和收起
计划预览的子任务、Agent 动态的改动列表、评论的回复,都用 expand:高度在 200ms 内变化,内容同时淡入;箭头图标的切换用 hover 的时长。Web 的 AgentActivity 和 Flutter 的 BiAgentActivity 已经这样做;客户端给计划预览加折叠时照同样的写法。
轻提示
出现时从底部上移 8px 并淡入(200ms),消失时淡出(120ms)。停留 5 秒,带操作(如「撤销」)时 8 秒;Web 在鼠标停留或获得焦点时暂停计时,Flutter 在读屏开着时不自动关掉带操作的轻提示。同一时间最多两条。
减少动态效果
系统打开「减少动态效果」(Web 读 prefers-reduced-motion,Flutter 读 MediaQuery.disableAnimations,对应 macOS 的「减弱动态效果」和 Android 的「移除动画」)时,每个场景按上表最后一列退化:
- 照旧:只有颜色、底色、描边在变,没有位置变化,保留原来的时长。
- 只淡入淡出:去掉位移、缩放和逐行间隔,只留不超过 120ms 的淡入淡出。淡入淡出能让人看出东西出现或消失了,不会引起不适。
- 直接切换:展开、收起和换位置直接跳到结果,不做中间过程。
另外:计时器圆点不再闪烁,改为常亮;萌虎完成任务时的旋转和放大去掉;拖动卡片不再倾斜。这些都由变量和 BiTransitions.of 处理,组件里不要再自己判断系统设置、另写一套时长。
三种风格
| 风格 | 气质 | 写法 |
|---|---|---|
| 青藤 | 平稳、几乎察觉不到 | 按上表的时长和曲线 |
| 萌虎 | 有一点弹性和仪式感 | 完成任务、卡片落位用带回弹的 easing-pop,其余同青藤 |
| 像素 | 像终端一样干脆 | 所有场景 80ms 线性,不位移、不缩放、不逐行出现;计时器圆点用 steps(2) 闪烁 |