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>