如何实现SpeedDial悬浮时按钮依次平滑显示的过渡动画
条件渲染{hovered && children}会直接在状态切换时增删DOM元素,元素没有过渡执行的时间窗口,是动画无法生效的根本原因。纯靠visibility: hidden也无法实现过渡,因为visibility是离散属性,只有显示/隐藏两个状态,没有中间过渡值。
实现方案
要实现按钮依次平滑显现的效果,核心逻辑是:
- 移除条件渲染逻辑,让操作按钮始终挂载在DOM中,通过CSS类控制显隐状态
- 组合
opacity(透明度)、transform: scale()(缩放)两个可连续过渡的属性实现平滑显现效果,搭配visibility和pointer-events保证按钮隐藏时不响应交互、不触发误触 - 给每个操作按钮设置递增的动画延迟,实现从靠近主按钮到远离主按钮依次出现的错落效果
具体代码修改
1. 修改SpeedDial.js
移除子元素的条件渲染,将展开状态透传给所有子按钮,给每个按钮传入索引值计算动画延迟,同时修复原代码中加号旋转样式的语法错误:
import React, { useState } from "react"; import Icon from "./Icon"; import "./SpeedDial.css"; export default function SpeedDial({ children, direction, style }) { const [hovered, setHovered] = useState(false); return ( <div className={`speed-dial ${direction}`} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} onClick={() => setHovered(!hovered)} style={style} > <button className="main-btn"> <Icon name="plus" style={{ transform: hovered ? "rotate(45deg)" : "none", transition: "transform ease .25s" }} /> </button> <div className={`action-wrapper ${direction} ${hovered ? 'open' : ''}`}> {React.Children.map(children, (child, index) => { return React.cloneElement(child, { index: index, dialOpen: hovered }) })} </div> </div> ); } SpeedDial.defaultProps = { direction: "right", onClick: () => {} };
2. 修改SpeedDialAction.js
接收传入的索引和展开状态,移除标签的条件渲染,通过CSS变量绑定每个按钮的动画延迟:
import React from "react"; import Icon from "./Icon"; import "./SpeedDialAction.css"; export default function SpeedDialAction({ icon, label, hovered, setActiveAction, direction, onClick, index, dialOpen }) { return ( <div className={`speed-dial-action ${dialOpen ? 'open' : ''}`} onClick={onClick} style={{'--delay': `${index * 50}ms`}} > <div className={`label ${direction} ${hovered ? 'show' : ''}`}>{label}</div> <button onMouseEnter={() => setActiveAction(label)} onMouseLeave={() => setActiveAction("")} className={direction} > <Icon name={icon} /> </button> </div> ); }
注:50ms的单步延迟和官方组件的动画节奏完全一致,可根据需要自行调整数值。
3. 补全CSS过渡样式
首先在SpeedDial.css末尾追加left方向的容器布局样式:
.speed-dial > .action-wrapper.left { flex-direction: row-reverse; align-items: center; }
然后替换SpeedDialAction.css中的重复样式,追加过渡效果:
.speed-dial-action { display: flex; flex-direction: column; align-items: center; position: relative; /* 初始隐藏状态 */ opacity: 0; transform: scale(0.4); visibility: hidden; pointer-events: none; /* 过渡配置,采用Material Design标准缓动曲线 */ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) var(--delay, 0ms); } /* 展开时的显示状态 */ .speed-dial-action.open { opacity: 1; transform: scale(1); visibility: visible; pointer-events: auto; } .speed-dial-action button { box-shadow: rgb(0 0 0 / 20%) 0px 3px 5px -1px, rgb(0 0 0 / 14%) 0px 6px 10px 0px, rgb(0 0 0 / 12%) 0px 1px 18px 0px; background: white; height: 45px; width: 45px; border-radius: 50%; font-size: 20px; cursor: pointer; } .speed-dial-action button:hover { background: #d8d8d8; } .speed-dial-action button.left { margin-right: 15px; } .speed-dial-action button.right { margin-left: 15px; } .speed-dial-action button.bottom { margin-top: 15px; } .speed-dial-action button.top { margin-bottom: 15px; } /* 标签淡入效果 */ .speed-dial-action .label { background: #565656; color: white; padding: 5px 8px; position: absolute; border-radius: 4px; text-transform: capitalize; opacity: 0; pointer-events: none; transition: opacity 0.15s ease; } .speed-dial-action .label.show { opacity: 1; } .speed-dial-action .label.left { top: -40px; } .speed-dial-action .label.right { top: -40px; }
最终效果
修改完成后即可实现和官方组件一致的交互:
- 悬浮主按钮时,操作按钮从靠近主按钮的位置开始,每隔50ms依次完成淡入+缩放显现,无滑出效果
- 鼠标移出时,按钮按反向顺序依次隐藏,隐藏状态下不会触发误触
- 主按钮加号旋转效果、操作按钮悬浮标签淡入效果均正常生效
内容的提问来源于stack exchange,提问作者Dan Zuzevich
相关产品推荐
相关产品推荐

