摘要:以前一直投入在 React Native 中,写动画的时候不是用 CSS 中的 transitions / animations,就是依赖像 GreenSock 这样的库,最近转向 Web,在 Tweet 得到很多大佬关于React We…

以下便是本文要分享的创建 React 动画 的几种方式

  • CSS animation
  • JS Style
  • React Motion
  • Animated
  • Velocity React

下面,勒次个特斯大特一特

CSS animation

给元素添加 class 是最简单,最常见的书写方式。如果你的 app 正在使用 CSS,那么这将是你最愉快的选择

赞同者: 我们只需修改 opacity 和 transform 这样的属性,就可构建基本的动画,而且,在组件中,我们可以非常容易地通过 state 去更新这些值

反对者:这种方式并不跨平台,在 React Native 中就不适用,而且,对于较复杂的动画,这种方式难以控制

接下来,我们通过一个实例来体验一下这种创建方式:当 input focus 的时候,我们增加它的宽度

首先,我们要创建两个 input 要用到的 class

  1. .input {
  2.  width: 150px;
  3.  padding: 10px;
  4.  font-size: 20px;
  5.  border: none;
  6.  border-radius: 4px;
  7.  background-color: #dddddd;
  8.  transition: width .35s linear;
  9.  outline: none;
  10. }
  11. .input-focused {
  12.  width: 240px;
  13. }

一个是它原始的样式,一个是它 focus 后的样式

下面,我们就开始书写我们的 React 组件

在此,推荐一个 在线的 React VS Code IDE,真的很强大,读者不想构建自己的 React app,可以在其中检验以下代码的正确性

React Web 动画的 5 种创建方式,每一种都不简单-爱 • 范儿
  1. class App extends Component {
  2.  state = {
  3.    focused: false,
  4.  }
  5.  componentDidMount() {
  6.    this._input.addEventListener('focus', this.focus);
  7.    this._input.addEventListener('blur', this.focus);
  8.  }
  9.  focus = () => {
  10.    this.setState(prevState => ({
  11.      focused: !prevState.focused,
  12.    }));
  13.  }
  14.  render() {
  15.    return (
  16.      <div className="App">
  17.        <div className="container">
  18.          <input
  19.            ref={input => this._input = input}
  20.            className={['input', this.state.focused && 'input-focused'].join(' ')}
  21.          />
  22.        </div>
  23.      </div>
  24.    );
  25.  }
  26. }
  • 我们有一个 focused 的 state,初始值为 false,我们通过更新该值来创建我们的动画
  • 在 componentDidMount 中,我们添加两个监听器,一个 focus,一个 blur,指定的回调函数都是 focus
  • focus 方法会获取之前 focused 的值,并负责切换该值
  • 在 render 中,我们通过 state 来改变 input 的 classNames,从而实现我们的动画

JS Style

JavaScipt styles 跟 CSS 中的 classes 类似,在 JS 文件中,我们就可以拥有所有逻辑

赞同者:跟 CSS动画 一样,且它的表现更加清晰。它也不失为一个好方法,可以不必依赖任何 CSS

反对者:跟 CSS动画 一样,也是不跨平台的,且动画一旦复杂,也难以控制

在下面的实例中,我们将创建一个 input,当用户输入时,我们将一个 button 从 disable 转变为 enable

React Web 动画的 5 种创建方式,每一种都不简单-爱 • 范儿
  1. class App extends Component {
  2.  state = {
  3.    disabled: true,
  4.  }
  5.  onChange = (e) => {
  6.    const length = e.target.value.length;
  7.    if (length > 0) {
  8.      this.setState({ disabled: false });
  9.    } else {
  10.      this.setState({ disabled: true });
  11.    }
  12.  }
  13.  render() {
  14.    const { disabled } = this.state;
  15.    const label = disabled ? 'Disabled' : 'Submit';
  16.    return (
  17.      <div style={styles.App}>
  18.        <input
  19.          style={styles.input}
  20.          onChange={this.onChange}
  21.        />
  22.        <button
  23.          style={Object.assign({},
  24.            styles.button,
  25.            !this.state.disabled && styles.buttonEnabled
  26.          )}
  27.          disabled={disabled}
  28.        >
  29.          {label}
  30.        </button>
  31.      </div>
  32.    );
  33.  }
  34. }
  35. const styles = {
  36.  App: {
  37.    display: 'flex',
  38.    justifyContent: 'left',
  39.  },
  40.  input: {
  41.    marginRight: 10,
  42.    padding: 10,
  43.    width: 190,
  44.    fontSize: 20,
  45.    border: 'none',
  46.    backgroundColor: '#ddd',
  47.    outline: 'none',
  48.  },
  49.  button: {
  50.    width: 90,
  51.    height: 43,
  52.    fontSize: 17,
  53.    border: 'none',
  54.    borderRadius: 4,
  55.    transition: '.25s all',
  56.    cursor: 'pointer',
  57.  },
  58.  buttonEnabled: {
  59.    width: 120,
  60.    backgroundColor: '#ffc107',
  61.  }
  62. }
  • 我们有一个 disabled 的 state,初始值为 true
  • onChange 方法会获取用户的输入,当输入非空时,就切换 disabled 的值
  • 根据 disabled 的值,确定是否将 buttonEnabled 添加到 button 中

React Motion

React Motion 是 Cheng Lou 书写的一个非常不错的开源项目。它的思想是你可以对Motion 组件 进行简单的样式设置,然后你就可以在回调函数中通过这些值,享受动画带来的乐趣

对于绝大多数的动画组件,我们往往不希望对动画属性(宽高、颜色等)的变化时间做硬编码处理,react-motion 提供的 spring 函数就是用来解决这一需求的,它可以逼真地模仿真实的物理效果,也就是我们常见的各类缓动效果

下面是一个森破的示例

  1. <Motion style={{ x: spring(this.state.x) }}>
  2.  {
  3.    ({ x }) =>
  4.      <div style={{ transform: `translateX(${x}px)` }} />
  5.  }
  6. </Motion>

这是官方提供的几个 demo,真的可以是不看不知道,一看吓一跳

  • Chat Heads
  • Draggable Balls
  • TodoMVC List Transition
  • Water Ripples
  • Draggable List

赞同者:React Motion 可以在 React Web 中使用,也可以在 React Native 中使用,因为它是跨平台的。其中的 spring 概念最开始对我来说感觉挺陌生,然而上手之后,发现它真的很神奇,并且,它有很详细的 API

反对者:在某些情况下,他不如纯 CSS / JS 动画,虽然它有不错的 API,容易上手,但也需要学习成本

为了使用它,首先我们要用 yarn 或 npm 安装它

  1. yarn add react-motion

在下面的实例中,我们将创建一个 dropdown 菜单,当点击按钮时,下拉菜单友好展开

React Web 动画的 5 种创建方式,每一种都不简单-爱 • 范儿
  1. class App extends Component {
  2.  state = {
  3.    height: 38,
  4.  }
  5.  animate = () => {
  6.    this.setState((state) => ({ height: state.height === 233 ? 38 : 233 }));
  7.  }
  8.  render() {
  9.    return (
  10.      <div className="App">
  11.        <div style={styles.button} onClick={this.animate}>Animate</div>
  12.        <Motion
  13.          style={{ height: spring(this.state.height) }}
  14.        >
  15.          {
  16.            ({ height }) =>
  17.            <div style={Object.assign({}, styles.menu, { height } )}>
  18.              <p style={styles.selection}>Selection 1</p>
  19.              <p style={styles.selection}>Selection 2</p>
  20.              <p style={styles.selection}>Selection 3</p>
  21.              <p style={styles.selection}>Selection 4</p>
  22.              <p style={styles.selection}>Selection 5</p>
  23.              <p style={styles.selection}>Selection 6</p>
  24.            </div>
  25.          }
  26.        </Motion>
  27.      </div>
  28.    );
  29.  }
  30. }
  31. const styles = {
  32.  menu: {
  33.    marginTop: 20,
  34.    width: 300,
  35.    border: '2px solid #ddd',
  36.    overflow: 'hidden',
  37.  },
  38.  button: {
  39.    display: 'flex',
  40.    width: 200,
  41.    height: 45,
  42.    justifyContent: 'center',
  43.    alignItems: 'center',
  44.    border: 'none',
  45.    borderRadius: 4,
  46.    backgroundColor: '#ffc107',
  47.    cursor: 'pointer',
  48.  },
  49.  selection: {
  50.    margin: 0,
  51.    padding: 10,
  52.    borderBottom: '1px solid #ededed',
  53.  },
  54. }
  • 我们从 react-motion 中 import Motion 和 spring
  • 我们有一个 height 的 state,初始值为 38,代表 menu 的高度
  • animate 方法设置 menu 的 height,如果 原 height 为 38,则设置新 height 为 233,如果 原 height 为 233,则设置 新 height 为 38
  • 在 render 中,我们使用 Motion 组件 包装整个 p 标签 列表,将 this.state.height 的当前值设为组件的 height,然后在组件的回调函数中使用该值作为整个下拉的高度
  • 当按钮被点击时,我们通过 this.animate 切换下拉的高度

Animated

Animated 是基于 React Native 使用的同一个动画库建立起来的

它背后的思想是创建声明式动画,通过传递配置对象来控制动画

赞同者:跨平台,它在 React Native 中已经非常稳定,如果你在 React Native 中使用过,那么你将不用再重复学习。其中的 interpolate 是一个神奇的插值函数,我们将在下面看到

反对者:基于 Twitter 的交流,它目前貌似不是 100% 的稳定,在老的浏览器中的,存在前缀和性能的问题,而且,它也有学习成本

为了使用 Animated,我们首先还是要用 yarn 或 npm 安装它

  1. yarn add animated

在下面的实例中,我们将模拟在提交表单成功后显示的动画 message

React Web 动画的 5 种创建方式,每一种都不简单-爱 • 范儿
  1. import Animated from 'animated/lib/targets/react-dom';
  2. import Easing from 'animated/lib/Easing';
  3. class AnimatedApp extends Component {
  4.  animatedValue = new Animated.Value(0);
  5.  animate = () => {
  6.    this.animatedValue.setValue(0);
  7.    Animated.timing(
  8.      this.animatedValue,
  9.      {
  10.        toValue: 1,
  11.        duration: 1000,
  12.        easing: Easing.elastic(1),
  13.      }
  14.    ).start();
  15.  }
  16.  render() {
  17.    const marginLeft = this.animatedValue.interpolate({
  18.      inputRange: [0, 1],
  19.      outputRange: [-120, 0],
  20.    });
  21.    return (
  22.      <div className="App">
  23.          <div style={styles.button} onClick={this.animate}>Animate</div>
  24.          <Animated.div
  25.            style={
  26.              Object.assign(
  27.                {},
  28.                styles.box,
  29.                { opacity: this.animatedValue, marginLeft })}
  30.          >
  31.            <p>Thanks for your submission!</p>
  32.          </Animated.div>
  33.      </div>- 我们将 `animatedValue`
  34. `marginLeft`  作为 `Animated.div ` `style` 属性,  );
  35.  }
  36. }
  37. const styles = {
  38.  button: {
  39.    display: 'flex',
  40.    width: 125,
  41.    height: 50,
  42.    justifyContent: 'center',
  43.    alignItems: 'center',
  44.    border: 'none',
  45.    borderRadius: 4,
  46.    backgroundColor: '#ffc107',
  47.    cursor: 'pointer',
  48.  },
  49.  box: {
  50.    display: 'inline-block',
  51.    marginTop: 10,
  52.    padding: '0.6rem 2rem',
  53.    fontSize:'0.8rem',
  54.    border: '1px #eee solid',
  55.    borderRadius: 4,
  56.    boxShadow: '0 2px 8px rgba(0,0,0,.2)',
  57.  },
  58. }
  • 从 animated 中 import Animated 和 Easing
  • 用 new Animated.Value(0) 创建一个值为 0 的类属性 - animatedValue
  • 创建 animate 方法,处理所有的动画,首先通过 this.animatedValue.setValue(0) 初始化动画值,实现的效果就是每次重新执行该动画,然后调用 Animated.timing,animatedValue 作为第一个参数传递,配置对象 作为第二个参数,一个设置最终动画值,一个设置持续时间,一个设置缓动效果
  • 在 render 中,我们用 interpolate 方法创建 marginLeft 对象,包含 inputRange 和 outputRange 数组,我们使用此对象作为 UI 中 message 的 style 属性
  • 我们使用 Animated.div 替代默认的 div
  • 我们将 animatedValue 和 marginLeft 作为 Animated.div 的 style 属性

Velocity React

Velocity React 是基于已经存在的 Velocity 建立起来的

赞同者:上手容易,API 简单明了,相对其他库更易于掌握

反对者:有些不得不克服的问题,比如 componentDidMount 后动画并没有真正地起作用等,而且,它不跨平台

下面是一个森破的示例

  1. <VelocityComponent
  2.  animation={{ opacity: this.state.showSubComponent ? 1 : 0 }}      
  3.  duration={500}
  4. >
  5.  <MySubComponent/>
  6. </VelocityComponent>

首先还是要用 yarn 或 npm 安装它

  1. yarn add velocity-react

在下面的实例中,我们将创建一个很酷的动画输入

  1. import { VelocityComponent } from 'velocity-react';
  2. const VelocityLetter = ({ letter }) => (
  3.  <VelocityComponent
  4.    runOnMount
  5.    animation={{ opacity: 1, marginTop: 0 }}
  6.    duration={500}
  7.  >
  8.    <p style={styles.letter}>{letter}</p>
  9.  </VelocityComponent>
  10. )
  11. class VelocityApp extends Component {
  12.  state = {
  13.    letters: [],
  14.  }
  15.  onChange = (e) => {
  16.    const letters = e.target.value.split('');
  17.    const arr = [];
  18.    letters.forEach((l, i) => {
  19.      arr.push(<VelocityLetter letter={l} />)
  20.    });
  21.    this.setState({ letters: arr });
  22.  }
  23.  render() {
  24.    return (
  25.      <div className="App">
  26.        <div className="container">
  27.          <input onChange={this.onChange} style={styles.input} />
  28.          <div style={styles.letters}>
  29.            {
  30.              this.state.letters
  31.            }
  32.          </div>
  33.        </div>
  34.      </div>
  35.    );
  36.  }
  37. }
  38. const styles = {
  39.  input: {
  40.    marginBottom: 20,
  41.    padding: 8,
  42.    width: 200,
  43.    height: 40,
  44.    fontSize: 22,
  45.    backgroundColor: '#ddd',
  46.    border: 'none',
  47.    outline: 'none',
  48.  },
  49.  letters: {
  50.    display: 'flex',
  51.    height: 140,
  52.  },
  53.  letter: {
  54.    marginTop: 100,
  55.    fontSize: 22,
  56.    whiteSpace: 'pre',
  57.    opacity: 0,
  58.  }
  59. }
  • 从 velocity-react 中 import VelocityComponent
  • 我们要创建一个可重复使用的组件来满足每个 letter 的动画
  • 在这个组件中,我们将 animation 的 opacity 设为 1,marginTop 设为 0,这些值代表着传入子组件的重写值,即当组件被创建时,组件的 opacity 会由初始的 0 变为 1,marginTop 会由初始的 100 变为 0,我们还设置了 500 ms 的持续时间,最后值得一提的是 runOnMount 属性,它的意思是在组件 挂载 或 创建 完后执行该动画
  • 其中的 onChange 方法会获取用户的每次输入,并创建一个由 VelocityLetter 组成的新数组
  • 在 render 中,我们就使用该数组在 UI 中渲染 letters