React Web 动画的 5 种创建方式,每一种都不简单

你是我的 7年前
   <p>以前一直投入在 React Native 中,写动画的时候不是用 CSS 中的 transitions / animations ,就是依赖像 GreenSock 这样的库,最近转向 Web ,在 Tweet 得到很多大佬关于 React Web 动画 的回应,于是决定分享给大家。</p>    <p>以下便是本文要分享的创建 React 动画 的几种方式</p>    <ul>     <li>CSS animation</li>     <li>JS Style</li>     <li><a href="/misc/goto?guid=4959751352269886581" rel="nofollow,noindex">React Motion</a></li>     <li><a href="/misc/goto?guid=4959751352350151623" rel="nofollow,noindex">Animated</a></li>     <li><a href="/misc/goto?guid=4959751352445590921" rel="nofollow,noindex">Velocity React</a></li>    </ul>    <p>下面,勒次个特斯大特一特</p>    <h2>CSS animation</h2>    <p>给元素添加 class 是最简单,最常见的书写方式。如果你的 app 正在使用 CSS ,那么这将是你最愉快的选择</p>    <p>赞同者 : 我们只需修改 opacity 和 transform 这样的属性,就可构建基本的动画,而且,在组件中,我们可以非常容易地通过 state 去更新这些值</p>    <p>反对者 :这种方式并 不跨平台 ,在 React Native 中就不适用,而且,对于较复杂的动画,这种方式 难以控制</p>    <p>接下来,我们通过一个实例来体验一下这种创建方式: 当 input focus 的时候,我们增加它的宽度</p>    <p>首先,我们要创建两个 input 要用到的 class</p>    <pre>  <code class="language-java">.input {    width: 150px;    padding: 10px;    font-size: 20px;    border: none;    border-radius: 4px;    background-color: #dddddd;    transition: width .35s linear;    outline: none;  }    .input-focused {    width: 240px;  }</code></pre>    <p>一个是它 原始 的样式,一个是它 focus 后的样式</p>    <p>下面,我们就开始书写我们的 React 组件</p>    <p>在此,推荐一个 <a href="/misc/goto?guid=4959751352529703867" rel="nofollow,noindex">在线的 React VS Code IDE</a> ,真的很强大,读者不想构建自己的 React app ,可以在其中检验以下代码的正确性</p>    <p><img src="https://simg.open-open.com/show/34e12b363e28584285837b5b9fbaca03.gif"></p>    <pre>  <code class="language-java">class App extends Component {    state = {      focused: false,    }      componentDidMount() {      this._input.addEventListener('focus', this.focus);      this._input.addEventListener('blur', this.focus);    }      focus = () => {      this.setState(prevState => ({        focused: !prevState.focused,      }));    }      render() {      return (        <div className="App">          <div className="container">            <input              ref={input => this._input = input}              className={['input', this.state.focused && 'input-focused'].join(' ')}            />          </div>        </div>      );    }  }</code></pre>    <ul>     <li>我们有一个 focused 的 state ,初始值为 false ,我们通过 更新该值 来创建我们的动画</li>     <li>在 componentDidMount 中,我们添加两个 监听器 ,一个 focus ,一个 blur ,指定的 回调函数都 是 focus</li>     <li>focus 方法会获取之前 focused 的值,并负责 切换 该值</li>     <li>在 render 中,我们通过 state 来改变 input 的 classNames ,从而实现我们的动画</li>    </ul>    <h2>JS Style</h2>    <p>JavaScipt styles 跟 CSS 中的 classes 类似,在 JS 文件中,我们就可以拥有所有逻辑</p>    <p>赞同者 :跟 CSS 动画 一样,且它的表现更加清晰。它也不失为一个好方法,可以不必依赖任何 CSS</p>    <p>反对者 :跟 CSS 动画 一样,也是 不跨平台 的,且动画一旦复杂,也 难以控制</p>    <p>在下面的实例中,我们将创建一个 input ,当用户输入时,我们将一个 button 从 disable 转变为 enable</p>    <p><img src="https://simg.open-open.com/show/204b6a2d062a79cb40464757c4080aec.gif"></p>    <pre>  <code class="language-java">class App extends Component {    state = {      disabled: true,    }      onChange = (e) => {      const length = e.target.value.length;        if (length > 0) {        this.setState({ disabled: false });      } else {        this.setState({ disabled: true });      }    }    render() {      const { disabled } = this.state;      const label = disabled ? 'Disabled' : 'Submit';        return (        <div style={styles.App}>          <input            style={styles.input}            onChange={this.onChange}          />          <button            style={Object.assign({},              styles.button,              !this.state.disabled && styles.buttonEnabled            )}            disabled={disabled}          >            {label}          </button>        </div>      );    }  }      const styles = {    App: {      display: 'flex',      justifyContent: 'left',    },    input: {      marginRight: 10,      padding: 10,      width: 190,      fontSize: 20,      border: 'none',      backgroundColor: '#ddd',      outline: 'none',    },    button: {      width: 90,      height: 43,      fontSize: 17,      border: 'none',      borderRadius: 4,      transition: '.25s all',      cursor: 'pointer',    },    buttonEnabled: {      width: 120,      backgroundColor: '#ffc107',    }  }</code></pre>    <ul>     <li>我们有一个 disabled 的 state ,初始值为 true</li>     <li>onChange 方法会获取用户的输入,当输入非空时,就切换 disabled 的值</li>     <li>根据 disabled 的值,确定是否将 buttonEnabled 添加到 button 中</li>    </ul>    <h2>React Motion</h2>    <p>React Motion 是 <a href="/misc/goto?guid=4959751352608868134" rel="nofollow,noindex">Cheng Lou</a> 书写的一个非常不错的开源项目。它的思想是你可以对 Motion 组件 进行简单的 样式设置 ,然后你就可以在 回调函数 中通过这些值,享受动画带来的乐趣</p>    <p>对于绝大多数的动画组件,我们往往不希望对 动画属性 (宽高、颜色等)的变化时间做 硬编码 处理, react-motion 提供的 spring 函数就是用来解决这一需求的,它可以逼真地模仿真实的物理效果,也就是我们常见的各类 缓动效果</p>    <p>下面是一个 森破 的示例</p>    <pre>  <code class="language-java"><Motion style={{ x: spring(this.state.x) }}>    {      ({ x }) =>        <div style={{ transform: `translateX(${x}px)` }} />    }  </Motion></code></pre>    <p>这是官方提供的几个 demo ,真的可以是 不看不知道,一看吓一跳</p>    <ul>     <li><a href="/misc/goto?guid=4959751352685758187" rel="nofollow,noindex">Chat Heads</a></li>     <li><a href="/misc/goto?guid=4959751352771233584" rel="nofollow,noindex">Draggable Balls</a></li>     <li><a href="/misc/goto?guid=4959751352856930270" rel="nofollow,noindex">TodoMVC List Transition</a></li>     <li><a href="/misc/goto?guid=4959751352941354056" rel="nofollow,noindex">Water Ripples</a></li>     <li><a href="/misc/goto?guid=4959751353024178366" rel="nofollow,noindex">Draggable List</a></li>    </ul>    <p>赞同者 : React Motion 可以在 React Web 中使用,也可以在 React Native 中使用,因为它是跨平台的。其中的 spring 概念最开始对我来说感觉挺陌生,然而上手之后,发现它真的很 神奇 ,并且,它有很详细的 API</p>    <p>反对者 :在某些情况下,他不如 纯 CSS / JS 动画 ,虽然它有不错的 API ,容易上手,但也需要学习成本</p>    <p>为了使用它,首先我们要用 yarn 或 npm 安装它</p>    <pre>  <code class="language-java">yarn add react-motion</code></pre>    <p>在下面的实例中,我们将创建一个 dropdown 菜单 ,当点击按钮时,下拉菜单友好展开</p>    <p><img src="https://simg.open-open.com/show/e49f3a4bf698d2c8ad341252be5fd4d6.gif"></p>    <pre>  <code class="language-java">class App extends Component {    state = {      height: 38,    }      animate = () => {      this.setState((state) => ({ height: state.height === 233 ? 38 : 233 }));    }      render() {      return (        <div className="App">          <div style={styles.button} onClick={this.animate}>Animate</div>          <Motion            style={{ height: spring(this.state.height) }}          >            {              ({ height }) =>              <div style={Object.assign({}, styles.menu, { height } )}>                <p style={styles.selection}>Selection 1</p>                <p style={styles.selection}>Selection 2</p>                <p style={styles.selection}>Selection 3</p>                <p style={styles.selection}>Selection 4</p>                <p style={styles.selection}>Selection 5</p>                <p style={styles.selection}>Selection 6</p>              </div>            }          </Motion>        </div>      );    }  }    const styles = {    menu: {      marginTop: 20,      width: 300,      border: '2px solid #ddd',      overflow: 'hidden',    },    button: {      display: 'flex',      width: 200,      height: 45,      justifyContent: 'center',      alignItems: 'center',      border: 'none',      borderRadius: 4,      backgroundColor: '#ffc107',      cursor: 'pointer',    },    selection: {      margin: 0,      padding: 10,      borderBottom: '1px solid #ededed',    },  }</code></pre>    <p>``</p>    <ul>     <li>我们从 react-motion 中 import Motion 和 spring</li>     <li>我们有一个 height 的 state ,初始值为 38 ,代表 menu 的高度</li>     <li>animate 方法设置 menu 的 height ,如果 原 height 为 38 ,则设置 新 height 为 233 ,如果 原 height 为 233 ,则设置 新 height 为 38</li>     <li>在 render 中,我们使用 Motion 组件 包装整个 p 标签 列表,将 this.state.height 的当前值设为组件的 height ,然后在组件的 回调函数 中使用该值作为整个下拉的高度</li>     <li>当按钮被点击时,我们通过 this.animate 切换下拉的高度</li>    </ul>    <h2>Animated</h2>    <p>Animated 是基于 React Native 使用的同一个动画库建立起来的</p>    <p>它背后的思想是创建 声明式动画 ,通过 传递配置对象来控制动画</p>    <p>赞同者 : 跨平台 ,它在 React Native 中已经非常稳定,如果你在 React Native 中使用过,那么你将不用再重复学习。其中的 interpolate 是一个神奇的插值函数,我们将在下面看到</p>    <p>反对者 :基于 推ter 的交流,它目前貌似不是 100% 的稳定,在老的浏览器中的,存在 前缀 和 性能 的问题,而且,它也有学习成本</p>    <p>为了使用 Animated ,我们首先还是要用 yarn 或 npm 安装它</p>    <pre>  <code class="language-java">yarn add animated</code></pre>    <p>在下面的实例中,我们将模拟在提交表单成功后显示的动画 message</p>    <p><img src="https://simg.open-open.com/show/bfcec915be7c2143a50deaeaa3edd1f0.gif"></p>    <pre>  <code class="language-java">import Animated from 'animated/lib/targets/react-dom';  import Easing from 'animated/lib/Easing';    class AnimatedApp extends Component {    animatedValue = new Animated.Value(0);      animate = () => {      this.animatedValue.setValue(0);        Animated.timing(        this.animatedValue,        {          toValue: 1,          duration: 1000,          easing: Easing.elastic(1),        }      ).start();    }      render() {      const marginLeft = this.animatedValue.interpolate({        inputRange: [0, 1],        outputRange: [-120, 0],      });        return (        <div className="App">            <div style={styles.button} onClick={this.animate}>Animate</div>            <Animated.div              style={                Object.assign(                  {},                  styles.box,                  { opacity: this.animatedValue, marginLeft })}            >              <p>Thanks for your submission!</p>            </Animated.div>        </div>- 我们将 `animatedValue`   和 `marginLeft`  作为 `Animated.div ` 的 `style` 属性,  );    }  }    const styles = {    button: {      display: 'flex',      width: 125,      height: 50,      justifyContent: 'center',      alignItems: 'center',      border: 'none',      borderRadius: 4,      backgroundColor: '#ffc107',      cursor: 'pointer',    },    box: {      display: 'inline-block',      marginTop: 10,      padding: '0.6rem 2rem',      fontSize:'0.8rem',      border: '1px #eee solid',      borderRadius: 4,      boxShadow: '0 2px 8px rgba(0,0,0,.2)',    },  }</code></pre>    <ul>     <li>从 animated 中 import Animated 和 Easing</li>     <li>用 new Animated.Value(0) 创建一个值为 0 的类属性 - animatedValue</li>     <li>创建 animate 方法,处理所有的动画,首先通过 this.animatedValue.setValue(0) 初始化动画值,实现的效果就是每次 重新执行 该动画,然后调用 Animated.timing , animatedValue 作为第一个参数传递, 配置对象 作为第二个参数,一个设置 最终动画值 ,一个设置 持续时间 ,一个设置 缓动效果</li>     <li>在 render 中,我们用 interpolate 方法创建 marginLeft 对象,包含 inputRange 和 outputRange 数组,我们使用此对象作为 UI 中 message 的 style 属性</li>     <li>我们使用 Animated.div 替代默认的 div</li>     <li>我们将 animatedValue 和 marginLeft 作为 Animated.div 的 style 属性</li>    </ul>    <h2>Velocity React</h2>    <p>Velocity React 是基于已经存在的 Velocity 建立起来的</p>    <p>赞同者 :上手容易, API 简单明了,相对其他库更易于掌握</p>    <p>反对者 :有些不得不克服的问题,比如 componentDidMount 后动画并没有真正地起作用等,而且,它 不跨平台</p>    <p>下面是一个 森破 的示例</p>    <pre>  <code class="language-java"><VelocityComponent    animation={{ opacity: this.state.showSubComponent ? 1 : 0 }}          duration={500}  >    <MySubComponent/>  </VelocityComponent></code></pre>    <p>首先还是要用 yarn 或 npm 安装它</p>    <pre>  <code class="language-java">yarn add velocity-react</code></pre>    <p>在下面的实例中,我们将创建一个很酷的 动画输入</p>    <p><img src="https://simg.open-open.com/show/15dcf4b0275a48bd9eb19f73a58132a8.gif"></p>    <pre>  <code class="language-java">import { VelocityComponent } from 'velocity-react';    const VelocityLetter = ({ letter }) => (    <VelocityComponent      runOnMount      animation={{ opacity: 1, marginTop: 0 }}      duration={500}    >      <p style={styles.letter}>{letter}</p>    </VelocityComponent>  )    class VelocityApp extends Component {    state = {      letters: [],    }      onChange = (e) => {      const letters = e.target.value.split('');      const arr = [];        letters.forEach((l, i) => {        arr.push(<VelocityLetter letter={l} />)      });      this.setState({ letters: arr });    }      render() {      return (        <div className="App">          <div className="container">            <input onChange={this.onChange} style={styles.input} />            <div style={styles.letters}>              {                this.state.letters              }            </div>          </div>        </div>      );    }  }    const styles = {    input: {      marginBottom: 20,      padding: 8,      width: 200,      height: 40,      fontSize: 22,      backgroundColor: '#ddd',      border: 'none',      outline: 'none',    },    letters: {      display: 'flex',      height: 140,    },    letter: {      marginTop: 100,      fontSize: 22,      whiteSpace: 'pre',      opacity: 0,    }  }</code></pre>    <ul>     <li>从 velocity-react 中 import VelocityComponent</li>     <li>我们要创建一个 可重复 使用的组件来满足每个 letter 的动画</li>     <li>在这个组件中,我们将 animation 的 opacity 设为 1 , marginTop 设为 0 ,这些值代表着传入子组件的 重写值 ,即当组件被创建时,组件的 opacity 会由初始的 0 变为 1 , marginTop 会由初始的 100 变为 0 ,我们还设置了 500 ms 的持续时间,最后值得一提的是 runOnMount 属性,它的意思是在组件 挂载 或 创建 完后执行该动画</li>     <li>其中的 onChange 方法会获取用户的每次输入,并创建一个由 VelocityLetter 组成的新数组</li>     <li>在 render 中,我们就使用该数组在 UI 中渲染 letters</li>    </ul>    <h2>总结</h2>    <p>总的来说,基本的动画,我会选择 JS style ,复杂的动画,我更偏向 React Motion 。而对于 React Native ,我还是坚持使用 Animated ,一旦 Animated 成熟,在 Web 中可能也会投入使用,目前,我真的很享受 React Motion</p>    <p>原文链接 : <a href="/misc/goto?guid=4959751353107191965" rel="nofollow,noindex">React Animations in Depth</a></p>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000010645631</p>    <p> </p>