master
LiuJiaNan 2026-02-25 10:52:19 +08:00
parent 3010ec84f3
commit d1de5b0d43
3 changed files with 186 additions and 91 deletions

View File

@ -5,30 +5,40 @@ import { useEffect, useRef, useState } from "react";
* BottomUtils 组件动画 Hook * BottomUtils 组件动画 Hook
* *
* 功能说明 * 功能说明
* 1. 监听显示/隐藏状态变化 * 1. 监听显示/隐藏状态变化执行从下向上进入从上向下离开的滑动动画
* 2. 执行从下向上进入从上向下离开的动画效果 * 2. 支持模式切换港口工具 分公司工具实现平滑过渡
* 3. 支持模式切换港口工具 分公司工具 * 3. 支持首次加载动画初始化时自动进入
* 4. 支持首次加载动画 * 4. 使用延迟显示模式确保动画过渡完整可见
* *
* 动画效果 * 动画参数配置
* - 进入动画从下方 100px 处滑入同时淡入y: 100 0, opacity: 0 1 * - 进入动画y: 100 0从下向上滑入opacity: 0 1淡入
* - 离开动画向下方 100px 处滑出同时淡出y: 0 100, opacity: 1 0 * 时长0.5s缓动easeOut先快后慢营造舒适的进入体验
* - 离开动画y: 0 100从上向下滑出opacity: 1 0淡出
* 时长0.3s缓动easeIn先慢后快让用户感觉到响应迅速
* *
* 模式切换流程 * 模式切换动画流程
* 1. 旧模式执行离开动画0.3 * 1. 旧模式离开0.3s向下滑出并淡出
* 2. 离开动画完成后更新 displayedMode 为新模式 * 2. 更新 displayedMode 为新模式
* 3. 设置元素到初始位置下方 100px * 3. 新模式进入0.5s从下方滑入并淡入
* 4. 执行新模式的进入动画0.5
* *
* @param {boolean} shouldShowPort - 是否应该显示港口工具 * @param {boolean} shouldShowPort - 是否应该显示港口工具
* @param {boolean} shouldShowBranchOffice - 是否应该显示分公司工具 * @param {boolean} shouldShowBranchOffice - 是否应该显示分公司工具
* @returns {object} 返回包含以下属性的对象 * @returns {object} 返回包含以下属性的对象
* - controls: motion 动画控制器 * - controls: motion 动画控制器绑定到 motion.div animate 属性
* - displayedMode: 延迟显示的模式'port' | 'branchOffice' | null * - displayedMode: 延迟显示的模式用于决定渲染 port 还是 branchOffice 内容
* - isVisible: 元素可见性状态 * - isVisible: 元素可见性状态用于控制内容的显示/隐藏
*/ */
export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice) { export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice) {
// ==================== 状态和引用管理 ==================== // ==================== 状态管理 ====================
//
// 动画时长说明:
// - 进入动画0.5s,较慢,营造舒适的进入体验
// - 离开动画0.3s,较快,让用户感觉到响应迅速
//
// 缓动函数说明:
// - easeOut进入时使用先快后慢更自然
// - easeIn离开时使用先慢后快快速消失
// ====================
// motion 动画控制器,用于控制 motion.div 的动画 // motion 动画控制器,用于控制 motion.div 的动画
const controls = useAnimation(); const controls = useAnimation();
@ -43,16 +53,29 @@ export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice)
// 延迟显示的模式状态 // 延迟显示的模式状态
// 作用:在模式切换时,先显示旧模式,动画完成后再更新为新模式 // 作用:在模式切换时,先显示旧模式,动画完成后再更新为新模式
// 这样确保用户看到完整的离开动画和进入动画 // 这样确保用户看到完整的离开动画和进入动画,实现平滑过渡
const [displayedMode, setDisplayedMode] = useState( const [displayedMode, setDisplayedMode] = useState(
shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null, shouldShowPort ? "port" : shouldShowBranchOffice ? "branchOffice" : null,
); );
// 元素可见性状态 // 元素可见性状态
// 注意motion.div 始终挂载isVisible 只控制内容的显示/隐藏 // 注意motion.div 始终挂载isVisible 只控制内容的显示/隐藏
// 这样可以保证动画控制器始终有效 // 这样可以保证动画控制器始终有效,避免重复创建/销毁
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
// ==================== 主题:监听状态变化并执行动画 ====================
//
// useEffect 的职责:
// 1. 监听 shouldShowPort 和 shouldShowBranchOffice 的变化
// 2. 根据不同场景自动执行相应的动画
//
// 场景说明:
// - 首次渲染:初始化并执行进入动画
// - 隐藏状态:执行离开动画并隐藏元素
// - 模式切换:旧模式离开 -> 新模式进入
// - 显示状态:从隐藏切换到可见
// ====================
useEffect(() => { useEffect(() => {
// ==================== 计算当前应该显示的模式 ==================== // ==================== 计算当前应该显示的模式 ====================
// 根据 shouldShowPort 和 shouldShowBranchOffice 判断当前应该显示哪个模式 // 根据 shouldShowPort 和 shouldShowBranchOffice 判断当前应该显示哪个模式
@ -192,7 +215,6 @@ export function useBottomUtilsAnimation(shouldShowPort, shouldShowBranchOffice)
}, [shouldShowPort, shouldShowBranchOffice, controls, displayedMode, isVisible]); }, [shouldShowPort, shouldShowBranchOffice, controls, displayedMode, isVisible]);
// ==================== 返回值 ==================== // ==================== 返回值 ====================
// 返回动画控制器、延迟显示的模式和可见性状态
return { return {
controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性 controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
displayedMode, // 延迟显示的模式,用于决定渲染 port 还是 branchOffice 内容 displayedMode, // 延迟显示的模式,用于决定渲染 port 还是 branchOffice 内容

View File

@ -2,52 +2,91 @@ import { useAnimation } from "motion/react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
/** /**
* PortUtils 父子项动画 Hook * PortUtils 父子项动画 Hook港口工具栏
* *
* 功能说明 * 功能说明
* 1. 实现父级和子级的复杂交互动画 * 1. 实现父级和子级的复杂交互动画序列
* 2. 支持点击父级展开/收起子级 * 2. 支持点击父级展开/收起子级展现流畅的动画效果
* 3. 初始化时不执行动画只执行 useBottomUtilsAnimation 的容器动画 * 3. 初始化时不执行动画首次点击后执行完整的展开/收起动画序列
* 4. 首次点击后执行完整的展开/收起动画序列 * 4. 使用依次动画stagger和并行动画营造层次感
* *
* 动画流程 * 动画流程详解
* *
* 初始显示 * 初始显示
* - 使用 useBottomUtilsAnimation 的主容器动画 * - 使用 useBottomUtilsAnimation 的主容器动画
* - 不执行父级的依次显示动画 * - 不执行父级的依次显示动画保持静止
* *
* 点击父级展开子级 * 展开动画序列点击父级展开子级
* 1. 所有父级依次下落隐藏y: 0 30, opacity: 1 0, x: 0 * 1. 所有父级依次下落隐藏stagger0.05s 间隔
* 2. 选中的父级显示回来y: 30 0, opacity: 0 1, x: 0 * - y: 0 30向下移动
* 3. 选中的父级移动到最左边x: 0 -offsetLeft * - opacity: 1 0淡出
* 4. 子级展开opacity: 0 1 * - x: 0重置水平位置
* - 单个动画时长0.1s
* 2. 选中的父级单独显示回来
* - y: 30 0回到原位
* - opacity: 0 1淡入
* - x: 0保持水平位置
* - 动画时长0.1s
* 3. 选中的父级移动到最左边
* - x: 0 -offsetLeft计算目标位置
* - 缓动easeInOut平滑过渡
* - 动画时长0.1s
* 4. 子级展开
* - opacity: 0 1淡入
* - 延迟0.05s等待步骤3完成
* - 动画时长0.1s
* *
* 点击父级收起子级 * 收起动画序列点击父级收起子级
* 1. 子级收起opacity: 1 0 * 1. 子级收起
* 2. 所有父级在各自当前位置下落y: 0 30, opacity: 1 0保持 x 位置 * - opacity: 1 0淡出
* 3. 所有父级回到原位x: 当前位置 0仍在下方 * - 动画时长0.1s
* 4. 所有父级依次从下向上显示y: 30 0, opacity: 0 1, x: 0 * 2. 所有父级在各自当前位置下落并行执行
* - y: 0 30向下移动
* - opacity: 1 0淡出
* - 保持各自的 x 位置选中的在左边其他在原位
* - 动画时长0.1s
* 3. 所有父级回到原位并行执行仍在下方
* - x: 当前位置 0重置水平位置
* - 动画时长0.1s
* 4. 所有父级依次从下向上显示stagger0.05s 间隔
* - y: 30 0回到原位
* - opacity: 0 1淡入
* - x: 0保持水平位置
* - 单个动画时长0.1s
*
* 动画配置
* - duration: 0.1s单个动画的持续时间
* - staggerDelay: 0.05s每个父级的延迟时间用于依次动画
* *
* @param {number} parentCount - 父级数量 * @param {number} parentCount - 父级数量
* @param {number} currentIndex - 当前选中的父级索引-1 表示未选中 * @param {number} currentIndex - 当前选中的父级索引-1 表示未选中
* @param {boolean} isPortMode - 是否是港口模式 * @param {boolean} isPortMode - 是否是港口模式
* @returns {object} 返回包含以下属性的对象 * @returns {object} 返回包含以下属性的对象
* - parentControls: 父级动画控制器数组 * - parentControls: 父级动画控制器数组每个父级独立控制
* - childControls: 子级动画控制器 * - childControls: 子级动画控制器控制子级的展开/收起
* - optionRefs: 父级元素引用数组 * - optionRefs: 父级元素引用数组用于计算位置偏移量
* - isAnimating: 是否正在执行动画 * - isAnimating: 是否正在执行动画用于防止动画冲突
* - hasInteracted: 是否已经交互过 * - hasInteracted: 是否已经交互过首次点击后才执行完整动画
* - shouldHideInactive: 是否应该隐藏非激活的父级 * - shouldHideInactive: 是否应该隐藏非激活的父级子级展开时隐藏其他父级
* - animationConfig: 动画配置对象包含 duration staggerDelay * - animationConfig: 动画配置对象包含 duration staggerDelay
*/ */
export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) { export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) {
// ==================== 动画配置 ==================== // ==================== 动画配置 ====================
const animationConfig = { const animationConfig = {
duration: 0.1, // 单个动画的持续时间(秒) duration: 0.1, // 单个动画的持续时间(秒)
staggerDelay: 0.05, // 每个父级的延迟时间(秒),用于依次动画 staggerDelay: 0.05, // 每个父级的延迟时间(秒),用于依次动画产生波浪效果
}; };
// ==================== 状态和引用管理 ==================== // ==================== 状态管理 ====================
//
// 动画时长说明:
// - 单个动画0.1s,非常快,营造敏捷的响应感
// - stagger 延迟0.05s,相邻父级之间的时间间隔,产生波浪效果
//
// 并行 vs 依次:
// - 并行动画:多个动画同时执行(如所有父级同时回到原位)
// - 依次动画:动画按顺序执行(如父级依次下落,每个间隔 0.05s
// ====================
// 标记是否正在执行动画 // 标记是否正在执行动画
// 作用:防止在动画执行过程中触发新的动画,避免动画冲突 // 作用:防止在动画执行过程中触发新的动画,避免动画冲突
@ -58,17 +97,17 @@ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) {
const [hasInteracted, setHasInteracted] = useState(false); const [hasInteracted, setHasInteracted] = useState(false);
// 标记是否应该隐藏非激活的父级 // 标记是否应该隐藏非激活的父级
// 作用:当子级展开时,隐藏未选中的父级,避免干扰 // 作用:当子级展开时,隐藏未选中的父级,避免视觉干扰
// 在动画执行期间设置为 false让所有父级参与动画 // 在动画执行期间设置为 false让所有父级参与动画
// 动画完成后设置为 true隐藏未选中的父级 // 动画完成后设置为 true隐藏未选中的父级
const [shouldHideInactive, setShouldHideInactive] = useState(false); const [shouldHideInactive, setShouldHideInactive] = useState(false);
// 父级元素的 ref 数组 // 父级元素的 ref 数组
// 作用:获取父级 DOM 元素,用于计算位置偏移量 // 作用:获取父级 DOM 元素,用于计算位置偏移量offsetLeft
const optionRefs = useRef([]); const optionRefs = useRef([]);
// 为每个父级创建独立的动画控制器 // 为每个父级创建独立的动画控制器
// 作用:每个父级可以独立控制动画 // 作用:每个父级可以独立控制动画,实现依次动画效果
const parentControls = Array.from({ length: parentCount }, () => useAnimation()); const parentControls = Array.from({ length: parentCount }, () => useAnimation());
// 子级的动画控制器 // 子级的动画控制器
@ -76,12 +115,28 @@ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) {
const childControls = useAnimation(); const childControls = useAnimation();
// ==================== 初始化 ==================== // ==================== 初始化 ====================
// 设置子级为初始状态(隐藏),确保一开始不可见
useEffect(() => { useEffect(() => {
// 设置子级为初始状态(隐藏)
// 作用:确保子级一开始不可见
childControls.set({ opacity: 0 }); childControls.set({ opacity: 0 });
}, []); }, []);
// ==================== 主题:展开/收起动画序列 ====================
//
// 展开动画performExpandAnimation
// 1. 所有父级依次下落隐藏stagger 效果)
// 2. 选中的父级单独显示回来
// 3. 选中的父级移动到最左边
// 4. 子级展开
//
// 收起动画performCollapseAnimation
// 1. 子级收起
// 2. 所有父级在各自当前位置下落(并行)
// 3. 所有父级回到原位(并行)
// 4. 所有父级依次从下向上显示stagger 效果)
//
// 使用 useCallback 缓存函数,避免不必要的重建
// ====================
// ==================== 展开动画序列 ==================== // ==================== 展开动画序列 ====================
const performExpandAnimation = useCallback(async () => { const performExpandAnimation = useCallback(async () => {
if (isAnimating) if (isAnimating)
@ -253,14 +308,13 @@ export function usePortUtilsAnimation(parentCount, currentIndex, isPortMode) {
}, [currentIndex, isPortMode]); }, [currentIndex, isPortMode]);
// ==================== 返回值 ==================== // ==================== 返回值 ====================
// 返回动画控制器和相关状态,供组件使用
return { return {
parentControls, // 父级动画控制器数组 parentControls, // 父级动画控制器数组,每个父级独立控制
childControls, // 子级动画控制器 childControls, // 子级动画控制器,控制子级的展开/收起
optionRefs, // 父级元素引用数组 optionRefs, // 父级元素引用数组,用于计算位置偏移量
isAnimating, // 是否正在执行动画 isAnimating, // 是否正在执行动画,用于防止动画冲突
hasInteracted, // 是否已经交互过 hasInteracted, // 是否已经交互过,首次点击后才执行完整动画
shouldHideInactive, // 是否应该隐藏非激活的父级 shouldHideInactive, // 是否应该隐藏非激活的父级,子级展开时隐藏其他父级
animationConfig, // 动画配置对象(包含 duration 和 staggerDelay animationConfig, // 动画配置对象(包含 duration 和 staggerDelay
}; };
} }

View File

@ -5,37 +5,35 @@ import { useEffect, useRef, useState } from "react";
* CenterUtils 弹跳动画 Hook * CenterUtils 弹跳动画 Hook
* *
* 功能说明 * 功能说明
* 1. 监听组件的显示/隐藏状态变化 * 1. 监听组件的显示/隐藏状态变化执行炫酷的弹跳动画效果
* 2. 执行炫酷的弹跳动画效果类似 animate.css bounceIn/bounceOut * 2. 支持两种动画类型上下弹跳up-down和缩放弹跳scale
* 3. 支持多种动画类型上下弹跳缩放弹跳 * 3. 使用位置缩放透明度三重动画效果营造生动的视觉体验
* 4. 使用弹跳缩放透明度三重动画效果让动画更加生动 * 4. 延迟隐藏机制确保离开动画完整播放后再移除组件
* 5. 延迟隐藏机制确保离开动画完整播放
* *
* 动画效果详解上下弹跳模式 up-down * 动画参数配置
* - 进入动画 * - up-down 模式进入动画
* - 位置从上方 120px 处冲到下方 40px超出目标轻微上弹到 -10px最后稳定到 0 * - 位置关键帧y: [-120, 40, -10, 0]从上方120px -> 冲到下方40px -> 轻微上弹 -> 稳定
* - 缩放 0.9 倍缩小到 1.05 轻微放大轻微缩小到 0.98回到正常 1.0 * - 缩放关键帧scale: [0.9, 1.05, 0.98, 1]90% -> 105% -> 98% -> 100%
* - 透明度从透明到不透明 * - 透明度关键帧opacity: [0, 1, 1, 1]透明 -> 不透明
* - 时长0.7 * - 时长0.7s缓动[0.34, 1.56, 0.64, 1]强弹性曲线产生过冲和回弹
* - 缓动使用强弹性贝塞尔曲线 [0.34, 1.56, 0.64, 1]产生明显的过冲和回弹效果 * - 关键帧时间点times: [0, 0.5, 0.75, 1]
* *
* - 离开动画 * - up-down 模式离开动画
* - 位置从正常位置向上弹跳 50px回落到 20px然后向上离开到 -120px * - 位置关键帧y: [0, -50, 20, -120]正常 -> 向上弹跳50px -> 回落到20px -> 向上离开到-120px
* - 缩放轻微放大到 1.02然后缩小到 0.85 * - 缩放关键帧scale: [1, 1.02, 0.95, 0.85]100% -> 102% -> 95% -> 85%
* - 透明度保持可见再淡出 * - 透明度关键帧opacity: [1, 1, 0.8, 0]保持可见 -> 淡出
* - 时长0.5 * - 时长0.5s缓动[0.68, -0.6, 0.32, 1.6]超弹性曲线产生夸张反向弹跳
* - 缓动使用超弹性曲线 [0.68, -0.6, 0.32, 1.6]产生夸张的反向弹跳 * - 关键帧时间点times: [0, 0.2, 0.5, 1]
* *
* 动画效果详解缩放弹跳模式 scale * - scale 模式进入动画
* - 进入动画 * - 缩放关键帧scale: [0, 1.15, 0.92, 1]0 -> 115% -> 92% -> 100%
* - 缩放 0 放大到 1.15 超出目标缩小到 0.92回到正常 1.0 * - 时长0.7s缓动[0.34, 1.56, 0.64, 1]
* - 时长0.7 * - 关键帧时间点times: [0, 0.4, 0.7, 1]
* - 缓动使用强弹性贝塞尔曲线
* *
* - 离开动画 * - scale 模式离开动画
* - 缩放 1.0 放大到 1.1缩小到 0.85最后消失到 0 * - 缩放关键帧scale: [1, 1.1, 0.85, 0]100% -> 110% -> 85% -> 0
* - 时长0.5 * - 时长0.5s缓动[0.68, -0.6, 0.32, 1.6]
* - 缓动使用超弹性曲线 * - 关键帧时间点times: [0, 0.3, 0.6, 1]
* *
* @param {boolean} isVisible - 控制组件的显示/隐藏 * @param {boolean} isVisible - 控制组件的显示/隐藏
* - true: 组件应该显示执行进入动画 * - true: 组件应该显示执行进入动画
@ -45,23 +43,46 @@ import { useEffect, useRef, useState } from "react";
* - 'scale'缩放弹跳动画从中心放大进入缩小离开 * - 'scale'缩放弹跳动画从中心放大进入缩小离开
* @returns {object} 返回包含以下属性的对象 * @returns {object} 返回包含以下属性的对象
* - controls: motion 动画控制器绑定到 motion.div animate 属性 * - controls: motion 动画控制器绑定到 motion.div animate 属性
* - isVisible: 延迟的可见性状态用于等待离开动画完成后再隐藏组件 * - isVisible: 延迟的可见性状态用于条件渲染等待离开动画完成
*/ */
export function useCenterUtilsAnimation(isVisible, type = "up-down") { export function useCenterUtilsAnimation(isVisible, type = "up-down") {
// ==================== 状态和引用管理 ==================== // ==================== 状态管理 ====================
//
// 动画时长说明:
// - 进入动画0.7s,较慢,展示完整的弹跳效果
// - 离开动画0.5s,较快,让用户感觉到响应迅速
//
// 缓动函数说明:
// - 进入动画:[0.34, 1.56, 0.64, 1],强弹性曲线,产生明显的过冲和回弹
// - 离开动画:[0.68, -0.6, 0.32, 1.6],超弹性曲线,产生夸张的反向弹跳
//
// 三重动画效果:
// - 位置动画y/ 缩放动画scale产生弹跳的物理运动效果
// - 透明度动画opacity平滑的进入和离开
// ====================
// motion 动画控制器,用于控制 motion.div 的动画 // motion 动画控制器,用于控制 motion.div 的动画
const controls = useAnimation(); const controls = useAnimation();
// 标记是否是首次渲染 // 标记是否是首次渲染
// 作用:首次渲染时需要特殊处理,确保初始状态正确设置 // 作用:首次渲染时需要特殊处理,确保初始状态正确设置
// 首次渲染不执行离开动画,直接执行进入动画
const isFirstRender = useRef(true); const isFirstRender = useRef(true);
// 组件的实际显示状态 // 组件的实际显示状态
// 作用:延迟隐藏,确保离开动画执行完成后才将组件从 DOM 中移除 // 作用:延迟隐藏,确保离开动画执行完成后才将组件从 DOM 中移除
const [displayState, setDisplayState] = useState(isVisible); const [displayState, setDisplayState] = useState(isVisible);
// ==================== 主题:监听可见性变化并执行弹跳动画 ====================
//
// useEffect 的职责:
// 1. 监听 isVisible 和 type 的变化
// 2. 根据显示/隐藏状态执行相应的弹跳动画
//
// 场景说明:
// - 显示状态:根据 type 执行 up-down 或 scale 弹跳进入动画
// - 隐藏状态:根据 type 执行 up-down 或 scale 弹跳离开动画
// ====================
useEffect(() => { useEffect(() => {
// ==================== 显示状态:执行进入动画 ==================== // ==================== 显示状态:执行进入动画 ====================
if (isVisible) { if (isVisible) {
@ -176,11 +197,9 @@ export function useCenterUtilsAnimation(isVisible, type = "up-down") {
} }
}, [isVisible, type, controls, displayState]); }, [isVisible, type, controls, displayState]);
// 返回动画控制器和延迟的可见性状态 // ==================== 返回值 ====================
// - controls用于绑定到 motion.div 的 animate 属性
// - isVisible延迟的可见性状态用于条件渲染等待离开动画完成
return { return {
controls, controls, // motion 动画控制器,绑定到 motion.div 的 animate 属性
isVisible: displayState, isVisible: displayState, // 延迟的可见性状态,用于条件渲染(等待离开动画完成)
}; };
} }