簡體   English   中英

使用 requestAnimationFrame 展開動畫,React 有時不起作用

[英]Expand animation with requestAnimationFrame and React doesn't work sometimes

我正在嘗試在進入/退出編輯模式時實現一些帶有簡單“擴展”動畫的單一輸入表單。

基本上我創建了一個包含值的幽靈元素,該元素旁邊是用作編輯/保存的圖標按鈕。 當您單擊編輯按鈕時,應出現帶有值的輸入而不是幻影元素,並且輸入的寬度應擴展/減少到定義的常量。

到目前為止,我有這段代碼,它大部分工作正常,但是對於展開它有時沒有動畫,我不知道為什么。

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) ,它開始工作,但我根本不喜歡代碼。

這個答案詳細解釋了發生了什么以及如何解決它。 但是,我實際上並不建議實施它。

自定義動畫很雜亂,並且有很棒的庫可以為您處理繁瑣的工作。 它們封裝了refrequestAnimationFrame代碼,並為您提供了一個聲明式 API。 我過去使用過react-spring ,它對我來說效果很好,但Framer Motion看起來也不錯。

但是,如果您想了解示例中發生的情況,請繼續閱讀。

發生了什么

requestAnimationFrame是一種告訴瀏覽器在每次渲染幀時運行一些代碼的方法。 requestAnimationFrame一個保證是瀏覽器將始終等待您的代碼在瀏覽器呈現下一幀之前完成,即使這意味着丟棄一些幀。

那么為什么這似乎不像它應該的那樣工作呢?

setState觸發的更新是異步的。 setState被調用時,React 不保證重新渲染; setState只是一個重新評估虛擬 DOM 樹的請求,React 異步執行。 這意味着setState可以並且通常會在不立即更改 DOM 的情況下完成,並且實際的 DOM 更新可能在瀏覽器渲染下一幀之后才會發生。

這也允許 React 將多個setState調用捆綁到一個重新渲染中, 它有時會這樣做,因此在動畫完成之前 DOM 可能不會更新。

如果您想保證requestAnimationFrame的 DOM 更改,則必須使用 React ref自己執行:

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>
  );
};

這是一個工作示例。

這段代碼使用了鈎子,但同樣的概念也適用於類; 只需將useEffect替換為componentDidUpdate ,將useState為組件狀態,並將useRefReact.createRef

在您的組件中使用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;
}

它可以讓您避免使用setTimeoutrequestAnimationFrame並使代碼更清晰。

Codesandbox: https ://codesandbox.io/s/csstransition-component-forked-3o4x3 ? file =/ index.js

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM