创建 React 动画的五种方式

简评:这篇文章将介绍五种可选方式来创建 React Web 动画,其中有一些是跨平台的(可以支持 React Native )

1. 基于 React 组件状态的 CSS 动画

对于我来说最基础也是最显然的来创建动画就是使用 CSS 类的属性并通过添加或删除他们来展现动画。如果在你的应用中已经使用了 CSS,这是种很好的方式来实现基础动画。

缺点:不是跨平台的(不支持 React Native),依赖于 CSS 和 DOM,如果需要实现复杂的效果,这种方式会变得难以控制。

优点:高性能。关于 CSS 动画,有一条已知的规则:除了透明度和变换意外,不要改变任何属性,通常会有很棒的性能。基于状态更新这些值非常简单,而且只要简单地重新渲染我们的组件就能达到平滑变换的效果。

看个例子:我们将会基于 React 组件使用 CSS 动画来动画化一个 input 组件。

首先我们要创建两个类关连上我们的 input:

.input {
  transition: width .35s linear;
  outline: none;
  border: none;
  border-radius: 4px;
  padding: 10px;
  font-size: 20px;
  width: 150px;
  background-color: #dddddd;
}

.input-focused {
  width: 240px;
}

我们有一些基础的属性,并且我们设置了 width .35 linear 的变换,给动画一些属性。

同时 input-focused 类将把宽度从 150 px 改动到 240 px。

现在在我们的 React 应用中把他们用起来:

class App extends Component {
  state = {
    focused: false
  }
  componentDidMount() {
    this.input.addEventListener('focus', this.focus);
    this.input.addEventListener('blur', this.focus);
  }
  focus = () => {
    this.setState((state) => ({ focused: !state.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>
    );
  }
}
  1. 我们创建了一个 focused 状态并设为 false。我们将用这个状态出发更新我们动画化的组件。

  2. componentDidMount 中,我们添加了两个监听器,一个监听 blur,一个监听 focus。两个监听器都能够调用 focus 方法。注意到我们正在引用 this.input,这是因为我们使用 ref 方法创建了一个引用,然后把它设置为一个类属性。我们在 componentDidMount 中做这些因为在 componentWillMount 时我们还没有进入 dom。

  3. focus 方法会检查上个 focused 状态的值,并基于他的值来触发。

  4. 在 render 中,主要注意的是我们给 input 设置了 classNames。我们检查 this.state.focused 是否为 true,如果是,我们会加入 input-focused 类。我们创建了一个数组,并调用 .join('') 作为一个可用的 className。

2. 基于 React 组件状态的 JS 样式动画

用 JS 样式来创建动画的方式和用 CSS 类有点相似。好处是你可以获得相同的性能,但你不用依赖 CSS 类,你可以在 JS 文件中写上所有的逻辑。

优点:像 CSS 动画,好处是性能杠杠的。同样也是种很好的方式,因为你不需要依赖于任何 CSS 文件。

缺点:同样和 CSS 动画一样,不是跨平台的(不支持 React Native),依赖于 CSS 和 DOM,如果要创造复杂的动画,会变得难以控制。

这个例子中,我们会创建一个输入框,当用户输入时,会变成可点击和不可点击的状态,给予用户反馈。

class App extends Component {
  state = {
    disabled: true,
  }
  onChange = (e) => {
    const length = e.target.value.length;
    if (length >= 4) {
      this.setState(() => ({ disabled: false }))
    } else if (!this.state.disabled) {
      this.setState(() => ({ disabled: true }))
    }
  }
  render() {
    const label = this.state.disabled ? 'Disabled' : 'Submit';
    return (
      <div className="App">
        <button
          style={Object.assign({}, styles.button, !this.state.disabled && styles.buttonEnabled)}
          disabled={this.state.disabled}
        >{label}</button>
        <input
          style={styles.input}
          onChange={this.onChange}
        />
      </div>
    );
  }
}

const styles = {
  input: {
    width: 200,
    outline: 'none',
    fontSize: 20,
    padding: 10,
    border: 'none',
    backgroundColor: '#ddd',
    marginTop: 10,
  },
  button: {
    width: 180,
    height: 50,
    border: 'none',
    borderRadius: 4,
    fontSize: 20,
    cursor: 'pointer',
    transition: '.25s all',
  },
  buttonEnabled: {
    backgroundColor: '#ffc107',
    width: 220,
  }
}
  1. 初始化一个 disabled 状态,设为 true

  2. onChange 方法绑定了 input,我们会检查输入了多少个字符。如果有 4 个或以上,我们将 disabled 设为 false,否则它还没被设为 true 的话那就设为 true

  3. 按钮元素的样式属性将会决定添加动画类 buttonEnabled 与否,取决于 this.state.disabled的值。

  4. 按钮的样式有一个 .25s all 的变换,因为我们想让 backgroundColorwidth 属性同时动画化。

3. React Motion

React MotionCheng Lou(华裔 FB 大神,不确定国籍)写的很棒的库,他在动画方面工作超过 2 年了,包括 React Web 和 React Native。他在 2015 年的 React Europe 上发表了一个很棒的关于讨论动画的演讲

React Motion 背后的思想是它将 API 引用的内容作为 “Spring”,这是一个非常稳定的基础动画配置,在大多数情况下工作良好,同时也是可配置的。它不依赖于时间,所以当你想要取消/停止/撤销一个动画或者在你的应用中使用可变维度的时候会更好用。

React Motion 的用法是你在一个 React Motion 组件中设置一个样式配置,然后你会收到一个包含这些样式值的回调函数。基础的例子看起来是这样的:

<Motion style={{ x: spring(this.state.x) }}>
  {
    ({ x }) =>
      <div style={{ transform: `translateX(${x}px)` }} />
  }
</Motion>

优点:React Motion 是跨平台的。spring 的概念一开始觉得很奇怪,但在真正使用后会觉得它是个天才的想法,并且将所有的东西都处理得非常好。同时 API 设计的也很棒!

缺点:我注意到在某些情况下它的性能不如纯 CSS/JS 样式动画。尽管 API 很容易上手,但你还是要花时间去学习。

要使用这个库,你可以通过 npm 或者 yarn 安装:
yarn add react-motion

这个例子中,我们将创建一个下拉菜单,按钮按下会触发菜单展开动画。

import React, { Component } from 'react';

import {Motion, spring} from 'react-motion';

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: {
    overflow: 'hidden',
    border: '2px solid #ddd',
    width: 300,
    marginTop: 20,
  },
  selection: {
    padding: 10,
    margin: 0,
    borderBottom: '1px solid #ededed'
  },
  button: {
    justifyContent: 'center',
    alignItems: 'center',
    display: 'flex',
    cursor: 'pointer',
    width: 200,
    height: 45,
    border: 'none',
    borderRadius: 4,
    backgroundColor: '#ffc107',
  },
}
  1. 我们从 react-motion 中导入了 Motionspring

  2. height 状态初始化为 38. 我们将会用它来动画化菜单的高度。

  3. animate 方法会检查当前高度值,如果是 38 就改为 250,否则将它重置为 38.

  4. render 中,我们使用 Motion 组件包裹了一个 p 标签列表。我们设置了 Motion 样式属性,传递了 this.state.height 作为高度值。现在,高度将在 Motion 组件的回调中返回。我们可以在回调中用这个高度来设置包裹着列表的 div 样式。

  5. 当按钮点击时,调用了 this.animate 触发高度属性变化。

4. Animated

Animated 库基于在 React Native 中使用的同名动画库。

Animated 的基本思想是你可以创建声明式动画,并传递配置对象来控制在动画中发生的事情。

优点:跨平台。在 React Native 中也非常稳定,所以如果你在 Web 中学习了就不用再学一次了。Animated 允许我们通过 interpolate 方法插入一个单一的值到多个样式中。我们还可以利用多个 Easing 属性的优势,开箱即用。

缺点:根据我通过 Twitter 的交流,看起来这个库在 Web 上还没有达到 100% 稳定,像为老版本浏览器自动添加前缀的问题及一些性能问题。如果你还没有从 React Native 中学过,同样需要花费时间学习。

可以通过 npm 或 yarn 安装:
yarn add animated

在这个例子中,我们将模仿点击订阅后弹出一条消息。

import Animated from 'animated/lib/targets/react-dom';
import Easing from 'animated/lib/Easing';

class App 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>
    );
  }
}
  1. animated 中导入 AnimatedEasing。注意到我们没有直接导入整个库,但我们实际上直接引入了 react-domEasing APIs。

  2. 创建了一个 animatedValue 类属性,通过调用 *new Animated.Value(0) *设为 0.

  3. 创建了一个 animated 方法。这个方法控制动画的发生,我们稍后将使用这个动画值并使用 interpolate 方法创建其他动画值。在这个方法中,我们通过调用 this.animatedValue.setValue(0) 将动画值设为 0,这样每次这个函数被调用时都能触发动画。然后调用了 Animated.timing, 传递动画值作为第一个参数(this.animatedValued),第二个参数是一个配置对象。这个配置对象有个 toValue 属性,将成为最终的动画值。duration 是动画的时长,easing 属性将声明动画的类型(我们选择了 Elastic)。

  4. 在我们的 render 方法中,我们首先通过使用 interpolate 方法创建了一个可动画化的值叫 marginLeftinterpolate 接受一个配置对象包含 inputRange 数组和一个 outputRange 数组,将会基于输入和输出创建一个新值。我们用这个值来设置 UI 中消息的 marginLeft 属性。

  5. Animated.div 取代常规的 div。

  6. 我们用 animatedValuemarginLeft 属性为* Animated.div* 添加样式,用 animatedValue 设置 opacitymarginLeft 设置 marginLeft

5. Velocity React

Velocity React 基于已有的 Velocity DOM 库。

用过之后,我的感觉是它的 API 像 Animated 和 React Motion 的结合体。总体来说,他看起来是一个有趣的库,我会在 web 上做动画的时候想到它,但我想的比较多的是 React Motion 和 Animated。

优点:非常容易上手。API 相当简单明了,比 React Motion 更容易掌握。

缺点:学它的时候有几个瑕疵必须要克服,包括不在 componentDidMount 中运行动画,而是必须声明 runOnMount 属性。同样不是跨平台的。

基础的 API 看起来像这样:

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

可以通过 npm 或 yarn 来安装:
yarn add velocity-react

在这个例子中我们会创建一个很酷的输入动画:

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 App 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: {
    height: 40,
    backgroundColor: '#ddd',
    width: 200,
    border: 'none',
    outline: 'none',
    marginBottom: 20,
    fontSize: 22,
    padding: 8,
  },
  letters: {
    display: 'flex',
    height: 140,
  },
  letter: {
    opacity: 0,
    marginTop: 100,
    fontSize: 22,
    whiteSpace: 'pre',
  }
}
  1. velocity-react 中导入 VelocityComponent

  2. 我们创建了一个可以重用的组件来保存每个要动画化的字符。

  3. 在这个组件中,我们设置动画的 opacity 为 1,marginTop 为 0. 子组件会根据我们传入的值重写这些值。这个例子中,<p> 的初始 opacity 为 0, marginTop 为 100. 当组件被创建时,我们将 opacity 从 0 设为 1,将 marginTop 从 100 设为 0. 我们同时设置了时长为 500 毫秒,以及一个 runOnMount 属性,声明我们想让动画在组件被安装或者创建时运行。

  4. renderinput 元素回调了一个 onChange 方法。onChange 将会从 input 中得到每个字符,并使用上面的 VelocityLetter 组件创建了一个新的数组。

  5. render 中,我们用这个数组来渲染字符到 UI 中。

总结

总体来说,我会适应 JS 样式动画来做基础动画,React Motion 来做任何 Web 上疯狂的东西。至于 React Native,我坚持使用 Animated。尽管我现在正在开始享受使用 React Motion,一旦 Animated 变得更加成熟,我可能在 web 上也会切换到 Animated!

原文链接:React Animations in Depth
推荐阅读:教你用 Web Speech API 和 Node.js 来创建一个简单的 AI 聊天机器人

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 以前一直投入在 React Native 中,写动画的时候不是用 CSS 中的 transitions / ani...
    枫上雾棋阅读 926评论 0 8
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 记忆中第一次上网是在初中。当时,只听闻某年级的学生半夜翻墙跑到网吧通宵或者某某同学沉迷于网吧荒废学业,却未曾到网吧...
    万卷无书阅读 262评论 0 0
  • 杨柳岸 晓风月 自古情多是伤离别 灞桥边 难眠夜 望尽红尘悲歌 知是故人远踏雪 深夜煎茶共邀月 遥忆当年事 天寒心...
    书云公子阅读 532评论 0 1