我正在尝试用简单的"展开";进入/退出编辑模式时的动画。
基本上,我创建了一个包含值的重影元素,这个元素旁边是图标按钮,可以编辑/保存。当您单击编辑按钮时,应显示带有值的输入,而不是重影元素,并且输入的宽度应扩展/减小到定义的常量。
到目前为止,我已经有了这段代码,它大部分都很好用,但对于扩展,它有时不会产生动画,我不知道为什么。
toggleEditMode = () => {
const { editMode } = this.state
if (editMode) {
this.setState(
{
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
setTimeout(() => {
this.setState({
editMode: false
})
}, 150)
})
}
)
} else {
this.setState(
{
editMode: true,
inputWidth: this.ghostRef.current.clientWidth
},
() => {
requestAnimationFrame(() => {
this.setState({
inputWidth: INPUT_WIDTH
})
})
}
)
}
}
你可以看看这里的例子。有人能解释一下出了什么问题,或者帮我找到解决方案吗?如果我在代码中添加另一个setTimeout(() => {...expand requestAnimationFrame here...}, 0)
,它就会开始工作,但我根本不喜欢这个代码。
这个答案详细解释了发生了什么以及如何修复它。然而,我实际上并不建议实现它。
自定义动画很混乱,而且有一些很棒的库可以帮你处理这些肮脏的工作。它们包装ref
和requestAnimationFrame
代码,并为您提供一个声明性的API。我过去用过react spring,它对我来说效果很好,但Framer Motion看起来也不错。
然而,如果你想了解你的例子中发生了什么,请继续阅读
发生了什么
requestAnimationFrame
是告诉浏览器在每次渲染帧时运行一些代码的一种方式。requestAnimationFrame
的一个保证是,浏览器将始终等待代码完成,然后再渲染下一帧,即使这意味着要删除一些帧。
那么,为什么这似乎不起作用呢?
setState
触发的更新是异步的。React不保证在调用setState
时重新渲染;setState
只是请求重新评估虚拟DOM树,React异步执行。这意味着setState
可以并且通常在不立即更改DOM的情况下完成,并且实际的DOM更新可能直到浏览器渲染下一帧之后才发生。
这也允许React将多个setState
调用绑定到一个重新渲染中,有时会这样做,因此DOM可能在动画完成之前不会更新。
如果您想保证requestAnimationFrame
中的DOM更改,您必须使用Reactref
:自己执行
const App = () => {
const divRef = useRef(null);
const callbackKeyRef = useRef(-1);
// State variable, can be updated using setTarget()
const [target, setTarget] = useState(100);
const valueRef = useRef(target);
// This code is run every time the component is rendered.
useEffect(() => {
cancelAnimationFrame(callbackKeyRef.current);
const update = () => {
// Higher is faster
const speed = 0.15;
// Exponential easing
valueRef.current
+= (target - valueRef.current) * speed;
// Update the div in the DOM
divRef.current.style.width = `${valueRef.current}px`;
// Update the callback key
callbackKeyRef.current = requestAnimationFrame(update);
};
// Start the animation loop
update();
});
return (
<div className="box">
<div
className="expand"
ref={divRef}
onClick={() => setTarget(target === 100 ? 260 : 100)}
>
{target === 100 ? "Click to expand" : "Click to collapse"}
</div>
</div>
);
};
下面是一个工作示例。
这段代码使用钩子,但类也使用相同的概念;只需用componentDidUpdate
替换useEffect
,用组件状态替换useState
,用React.createRef
替换useRef
。
在组件中从react-transition-group
使用CSSTransition
似乎是一个更好的方向
function Example() {
const [tr, setIn] = useState(false);
return (
<div>
<CSSTransition in={tr} classNames="x" timeout={500}>
<input
className="x"
onBlur={() => setIn(false)}
onFocus={() => setIn(true)}
/>
</CSSTransition>
</div>
);
}
并且在您的css模块中:
.x {
transition: all 500ms;
width: 100px;
}
.x-enter,
.x-enter-done {
width: 400px;
}
它可以避免使用setTimeout
s和requestAnimationFrame
,并使代码更干净。
Codesandbox:https://codesandbox.io/s/csstransition-component-forked-3o4x3?file=/index.js