You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React钢琴可视化:切换CSS动画后如何保留元素高度状态?

解决React钢琴可视化工具的动画切换高度重置问题

核心问题分析

  1. CSS动画执行完毕后会回到初始状态,切换move_up时没有把expand_up过程中累积的高度固化为元素的实际样式
  2. 直接操作DOM修改style后,React虚拟DOM因为没有对应的state更新,会覆盖掉手动设置的样式
  3. visuals状态中存储的是JSX元素,而非数据,导致更新state时无法触发对应元素的重新渲染

具体解决方案

1. 重构状态存储,用数据代替JSX

把visuals从存储JSX改为存储每个视觉实例的状态数据,这样React能正确感知变化并重新渲染:

// 初始化状态
const [visuals, setVisuals] = useState([]);
const [counter, setCounter] = useState(0);
const [keyDown, setKeyDown] = useState(false);
const visualRefs = useRef({});

2. 修改按键逻辑,通过state管理视觉实例

更新useEffect中的逻辑,操作数据而非直接创建JSX:

useEffect(() => {
  if (this_pressed) {
    setKeyDown(true);
    play_sound();
    // 添加新的视觉实例数据
    const newVisual = {
      id: counter,
      height: 0,
      isMoving: false
    };
    setVisuals(prev => [...prev, newVisual]);
  } else {
    if (keyDown) {
      const currVisual = visualRefs.current[counter];
      if (!currVisual) return;
      
      // 获取当前动画中的实际高度
      const { height } = currVisual.getBoundingClientRect();
      
      // 更新对应实例的状态:固化高度并标记为移动中
      setVisuals(prev => prev.map(v => 
        v.id === counter ? {...v, height, isMoving: true} : v
      ));

      // 监听动画结束后移除实例
      const handleAnimationEnd = () => {
        setVisuals(prev => prev.filter(v => v.id !== counter));
        delete visualRefs.current[counter];
        currVisual.removeEventListener('animationend', handleAnimationEnd);
      };
      currVisual.addEventListener('animationend', handleAnimationEnd);

      setCounter(prev => prev + 1);
      setKeyDown(false);
    }
  }
}, [this_pressed]);

3. 在render中动态生成视觉元素

根据visuals数据渲染div,用state控制样式和动画class:

return (
  <div className={`piano-key-wrapper ${color}-wrapper`}>
    <div className={`vis-path`}>
      {visuals.map(visual => (
        <div
          key={visual.id}
          ref={ref => visualRefs.current[visual.id] = ref}
          className={`visualizer-instance ${visual.isMoving ? 'move_up' : 'expand_up'}`}
          style={{ height: `${visual.height}px` || 'auto' }}
        />
      ))}
    </div>
    <div 
      ref={innerRef} 
      className={`piano-key ${color}-key ${keyDown ? 'pressed' : ''}`}
      onMouseUp={() => set_this_pressed(false)}
      onMouseDown={() => set_this_pressed(true)}
      onMouseLeave={() => set_this_pressed(false)}
    >
      {keyboard_key}
    </div>
  </div>
);

4. 优化CSS动画

去掉动画中重复的position设置,避免冲突:

@keyframes expand_up {
    0% {
        height: 0;
        bottom: 0;
    }
    100% {
        bottom: 0;
        height: 300000px;
    }
}

@keyframes move_up {
    0% {
        bottom: 0;
    }
    100% {
        bottom: 300000px;
    }
}

.visualizer-instance {
    background-color: var(--ten-color);
    position: absolute;
    width: 100%;
    /* 初始高度设为0,配合动画 */
    height: 0;
}
.expand_up {
    animation: expand_up 1000s linear forwards; /* 添加forwards保持动画结束状态 */
}

.move_up {
    animation: move_up 1000s linear forwards;
}

关键改动说明

  • 用数据驱动渲染:React对状态数据的变更敏感度远高于直接操作JSX,确保每次状态更新都能触发正确的重渲染
  • 固化动画高度:通过state存储当前高度,让React负责把高度应用到DOM上,避免虚拟DOM覆盖手动修改的样式
  • 添加forwards动画属性:让动画结束后保持最后一帧的状态,避免切换时的状态回退

内容的提问来源于stack exchange,提问作者The Canadian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 18:51:17