ReactNative Animated动画详解

By | 03月09日
Advertisement

最近ReactNative(以下简称RN)在前端的热度越来越高,不少同学开始在业务中尝试使用RN,这里着重介绍一下RN中动画的使用与实现原理。

举个简单的栗子

var React = require('react-native');
var {
    Animated,
    Easing,
    View,
    StyleSheet,
    Text
} = React;
 
var Demo = React.createClass({
    getInitialState() {
        return {
            fadeInOpacity: new Animated.Value(0) // 初始值
        };
    },
    componentDidMount() {
        Animated.timing(this.state.fadeInOpacity, {
            toValue: 1, // 目标值
            duration: 2500, // 动画时间
            easing: Easing.linear // 缓动函数
        }).start();
    },
    render() {
        return (
            <Animated.Viewstyle={[styles.demo, {
                    opacity: this.state.fadeInOpacity
                }]}>
                <Text style={styles.text}>悄悄的,我出现了</Text>
            </Animated.View>
        );
    }
});
 
var styles = StyleSheet.create({
    demo: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'white',
    },
    text: {
        fontSize: 30
    }
});

ReactNative Animated动画详解

是不是很简单易懂<(▰˘◡˘▰)> 和JQuery的Animation用法很类似。

步骤拆解

一个RN的动画,可以按照以下步骤进行。

  1. 使用基本的Animated组件,如Animated.View Animated.Image Animated.Text ( 重要!不加Animated的后果就是一个看不懂的报错,然后查半天动画参数,最后怀疑人生 )
  2. 使用Animated.Value设定一个或多个初始化值(透明度,位置等等)。
  3. 将初始化值绑定到动画目标的属性上(如style)
  4. 通过Animated.timing等函数设定动画参数
  5. 调用start启动动画。

栗子敢再复杂一点吗?

显然,一个简单的渐显是无法满足各位观众老爷们的好奇心的.我们试一试加上多个动画

getInitialState() {
    return (
        fadeInOpacity: new Animated.Value(0),
            rotation: new Animated.Value(0),
            fontSize: new Animated.Value(0)
    );
},
componentDidMount() {
    var timing = Animated.timing;
    Animated.parallel(['fadeInOpacity', 'rotation', 'fontSize'].map(property => {
                return timing(this.state[property], {
                toValue: 1,
                duration: 1000,
                easing: Easing.linear
            });
        })).start();
},
render() {
    return (<Animated.Viewstyle={[styles.demo, {
            opacity: this.state.fadeInOpacity,
                transform: [{
                    rotateZ: this.state.rotation.interpolate({
                        inputRange: [0,1],
                        outputRange: ['0deg', '360deg']
                    })
                }]
            }]}><Animated.Textstyle={{
                fontSize: this.state.fontSize.interpolate({
                    inputRange: [0,1],
                    outputRange: [12,26]
                })
            }}>我骑着七彩祥云出现了:smiling_imp::dash:</Animated.Text>
            </Animated.View>
    );
}

注意到我们给文字区域加上了字体增大的动画效果,相应地,也要修改Text为Animated.Text

ReactNative Animated动画详解

强大的interpolate

上面的栗子使用了interpolate函数,也就是插值函数。这个函数很强大,实现了数值大小、单位的映射转换,比如

{  
    inputRange: [0,1],
    outPutRange: ['0deg','180deg']
}

当setValue(0.5)时,会自动映射成90deg。 inputRange并不局限于[0,1]区间,可以画出多段。 interpolate一般用于多个动画共用一个Animated.Value,只需要在每个属性里面映射好对应的值,就可以用一个变量控制多个动画。 事实上,上例中的fadeInOpacityfontSizerotation用一个变量来声明就可以了。(那你写那么多变量逗我吗(╯‵□′)╯︵┻━┻) (因为我要强行使用parallel &#27;┬─┬ ノ( ' – 'ノ))

流程控制

在刚才的栗子中,我们使用了Parallel来实现多个动画的并行渲染,其它用于流程控制的API还有:

  • sequence接受一系列动画数组为参数,并依次执行
  • stagger接受一系列动画数组和一个延迟时间,按照序列,每隔一个延迟时间后执行下一个动画(其实就是插入了delay的parrllel)
  • delay生成一个延迟时间(基于timing的delay参数生成)

例3

getInitialState() {
    return (
        anim: [1,2,3].map(() => new Animated.Value(0)) // 初始化3个值
    );
},
 
componentDidMount() {
    var timing = Animated.timing;
    Animated.sequence([
        Animated.stagger(200, this.state.anim.map(left => {
            return timing(left, {
                toValue: 1,
              });
            }).concat(
                this.state.anim.map(left => {
                    return timing(left, {
                        toValue: 0,
                    });
                })
            )), // 三个view滚到右边再还原,每个动作间隔200ms
            Animated.delay(400), // 延迟400ms,配合sequence使用
            timing(this.state.anim[0], {
                toValue: 1
            }),
            timing(this.state.anim[1], {
                toValue: -1
            }),
            timing(this.state.anim[2], {
                toValue: 0.5
            }),
            Animated.delay(400),
            Animated.parallel(this.state.anim.map((anim) => timing(anim, {
                toValue: 0
            }))) // 同时回到原位置
        ]
    ).start();
},
render() {
    var views = this.state.anim.map(function(value, i) {
        return (
            <Animated.View
                key={i}
                style={[styles.demo, styles['demo' + i], {
                    left: value.interpolate({
                        inputRange: [0,1],
                        outputRange: [0,200]
                    })
                }]}>
                <Text style={styles.text}>我是第{i + 1}个View</Text>
 
            </Animated.View>
        );
    });
    return <Viewstyle={styles.container}>
              <Text>sequence/delay/stagger/parallel演示</Text>
              {views}
          </View>;
}

ReactNative Animated动画详解

Spring/Decay/Timing

前面的几个动画都是基于时间实现的,事实上,在日常的手势操作中,基于时间的动画往往难以满足复杂的交互动画。对此,RN还提供了另外两种动画模式。

  • Spring 弹簧效果
    • friction 摩擦系数,默认40
    • tension 张力系数,默认7
    • bounciness
    • speed
  • Decay 衰变效果
    • velocity 初速率
    • deceleration 衰减系数 默认0.997

Spring支持 friction与tension 或者 bounciness与speed 两种组合模式,这两种模式不能并存。 其中friction与tension模型来源于 origami ,一款F家自制的动画原型设计工具,而bounciness与speed则是传统的弹簧模型参数。

Track && Event

RN动画支持跟踪功能,这也是日常交互中很常见的需求,比如跟踪用户的手势变化,跟踪另一个动画。而跟踪的用法也很简单,只需要指定toValue到另一个Animated.Value就可以了。 交互动画需要跟踪用户的手势操作,Animated也很贴心地提供了事件接口的封装,示例:

// Animated.event 封装手势事件等值映射到对应的Animated.Value
onPanResponderMove: Animated.event(
    [null, {dx: this.state.x, dy: this.state.y}] // map gesture to leader
)

在官方的demo上改了一下,加了一张费玉污的图,效果图如下 代码太长,就不贴出来了,可以参考 官方Github代码

ReactNative Animated动画详解

动画循环

Animated的start方法是支持回调函数的,在动画或某个流程结束的时候执行,这样子就可以很简单地实现循环动画了。

startAnimation() {
    this.state.rotateValue.setValue(0);
    Animated.timing(this.state.rotateValue, {
        toValue: 1,
        duration: 800,
        easing: Easing.linear
    }).start(() => this.startAnimation());
}

ReactNative Animated动画详解

是不是很魔性?[doge]

首先感谢能看到这里的小伙伴们:)

在上面的文章中,我们已经基本掌握了RN Animated的各种常用API,接下来我们来了解一下这些API是如何设计出来的。

声明: 以下内容参考自Animated原作者的 分享视频

首先,从React的生命周期来编程的话,一个动画大概是这样子写:

getInitialState() {
    return {left: 0};
}
 
render(){
    return (
        <divstyle={{left: this.state.left}}>
            <Child />
        </div>
    );
}
 
onChange(value) {
    this.setState({left: value});
}

只需要通过requestAnimationFrame调用onChange,输入对应的value,动画就简单粗暴地跑起来了。◕‿◕,全剧终。

然而事实总是没那么简单,问题在哪?

我们看到,上述动画基本是以毫秒级的频率在调用setState,而React的每次setState都会重新调用render方法,并切遍历子元素进行渲染,即使有Dom Diff也可能扛不住这么大的计算量和UI渲染。

ReactNative Animated动画详解

那么该如何优化呢?

  • 关键词:
    • ShouldComponentUpdate
    • <StaticContainer>(静态容器)
    • Element Caching (元素缓存)
    • Raw DOM Mutation (原生DOM操作)
    • ↑↑↓↓←→←→BA (秘籍)

ShouldComponentUpdate

学过React的都知道,ShouldComponentUpdate是性能优化利器,只需要在子组件的shouldComponentUpdate返回false,分分钟渲染性能爆表。

ReactNative Animated动画详解

然而并非所有的子元素都是一成不变的,粗暴地返回false的话子元素就变成一滩死水了。而且组件间应该是独立的,子组件很可能是其他人写的,父元素不能依赖于子元素的实现。

<StaticContainer>(静态容器)

这时候可以考虑封装一个容器,管理ShouldCompontUpdate,如图示:

ReactNative Animated动画详解

小明和老王再也不用关心父元素的动画实现啦。

一个简单的\<StaticContainer\>实现如下:

class StaticContainerextends React.Component {
    render(){
        return this.props.children;
    }
    shouldComponentUpdate(nextProps){
        return nextProps.shouldUpdate; // 父元素控制是否更新
    }
}
 
// 父元素嵌入StaticContainer
render() {
    return (
        <divstyle={{left: this.state.left}}>
            <StaticContainer
            shouldUpdate={!this.state.isAnimating}>
                <ExpensiveChild />
            </StaticContainer>
        </div>
    );
}

Element Caching 缓存元素

还有另一种思路优化子元素的渲染,那就是缓存子元素的渲染结果到局地变量。

render(){
    this._child = this._child || <ExpensiveChild />;
    return (
        <divstyle={{left:this.state.left}}>
            {this._child}
        </div>
    );
}

缓存之后,每次setState时,React通过DOM Diff就不再渲染子元素了。

上面的方法都有弊端,就是 条件竞争 。当动画在进行的时候,子元素恰好获得了新的state,而这时候动画无视了这个更新,最后就会导致状态不一致,或者动画结束的时候子元素发生了闪动,这些都是影响用户操作的问题。

Raw DOM Mutation 原生DOM操作

刚刚都是在React的生命周期里实现动画,事实上,我们只想要变更这个元素的left值,并不希望各种重新渲染、DOM DIFF等等发生。

“React,我知道自己要干啥,你一边凉快去“

如果我们跳出这个生命周期,直接找到元素进行变更,是不是更简单呢?

ReactNative Animated动画详解

简单易懂,性能彪悍,有木有?!

然而弊端也很明显,比如这个组件unmount之后,动画就报错了。

Uncaught Exception: Cannot call ‘style’ of null

而且这种方法照样避不开 条件竞争 ——动画值改变的时候,有可能发生setState之后,left又回到初始值之类的情况。

再者,我们使用React,就是因为不想去关心dom元素的操作,而是交给React管理,直接使用Dom操作显然违背了初衷。

↑↑↓↓←→←→BA (秘籍)

唠叨了这么多,这也不行,那也不行,什么才是真理?

我们既想要原生DOM操作的高性能,又想要React完善的生命周期管理,如何把两者优势结合到一起呢?答案就是 Data Binding(数据绑定)

render(){
    return(
        <Animated.divstyle={{left: this.state.left}}>
            <ExpensiveChild />
        </Animated.div>
    );
}
 
getInitialState(){
    return {left: new Animated.Value(0)}; // 实现了数据绑定的类
}
 
onUpdate(value){
    this.state.left.setValue(value); // 不是setState
}

首先,需要实现一个具有数据绑定功能的类Animated.Value,提供setValueonChange等接口。 其次,由于原生的组件并不能识别Value,需要将动画元素用Animated包裹起来,在内部处理数据变更与DOM操作。

一个简单的动画组件实现如下:

Animated.div = class extends React.Component{
    componentWillUnmount() {
        nextProps.style.left.removeAllListeners();
    },
    // componentWillMount需要完成与componentWillReceiveProps同样的操作,此处略
    componentWillReceiveProps(nextProps) {
        nextProps.style.left.removeAllListeners();
        nextProps.style.left.onChange(value => {
            React.findDOMNode(this).style.left = value + 'px';
        });
        
        // 将动画值解析为普通数值传给原生div
        this._props = React.addons.update(
            nextProps,
            {style:{left:{$set: nextProps.style.left.getValue()}}}
        );
    },
    render() {
        return <div ...{this._props} />;
    }
}

代码很简短,做的事情有:

  1. 遍历传入的props,查找是否有Animated.Value的实例,并绑定相应的DOM操作。
  2. 每次props变更或者组件unmount的时候,停止监听数据绑定事件,避免了条件竞争和内存泄露问题。
  3. 将初始传入的Animated.Value值逐个转化为普通数值,再交给原生的React组件进行渲染。

综上,通过封装一个Animated的元素,内部通过数据绑定和DOM操作变更元素,结合React的生命周期完善内存管理,解决条件竞争问题,对外表现则与原生组件相同,实现了高效流畅的动画效果。

读到这里,应该知道为什么ImageText等做动画一定要使用Animated加持过的元素了吧?

Similar Posts:

  • [置顶] android动画详解五 layout,插值与评估器

    · 动画监听器 你可以使用下述监听器监听动画过程中的重要事件们: · Animator.AnimatorListener · onAnimationStart() - 动画开始时调用. · onAnimationEnd() - 当动画结束时调用. · onAnimationRepeat() - 当动画开始重复时调用. · onAnimationCancel() - 当动画被取消时调用.一个取消的动画还调用onAnimationEnd(),不论他是怎样结束的. · ValueAnimator.An

  • Tween Animation动画详解

    文章转自:http://blog.csdn.net/harvic880925/article/details/40117115 相关文章:<Android自定义控件三部曲文章索引> 一.概述 前两篇,我为大家讲述了利用XML来定义动画及插值器,但在代码中,我们常常是动态生成动画的,所以,这篇将为大家讲述如何用代码生成动态生成动画及插值器. 先简单写出各个标签对应的类,方便大家理解: scale -- ScaleAnimation alpha -- AlphaAnimation rotate -

  • Android 属性动画详解

    前言 The property animation system is a robust framework that allows you to animate almost anything. You can define an animation to change any object property over time, regardless of whether it draws to the screen or not. A property animation changes

  • SVG 动画详解

    转载地址:http://www.zhangxinxu.com/wordpress/?p=4333 扫我分享 一.SVG SMIL animation概览 1. SMIL是什么? SMIL不是指「水蜜梨」,而是Synchronized Multimedia Integration Language(同步多媒体集成语言)的首字母缩写简称,是有标准的.本文所要介绍的SVG动画就是基于这种语言. SMIL允许你做下面这些事情: 动画元素的数值属性(X, Y, -) 动画属性变换(平移或旋转) 动画颜色属

  • Android 开发之动画详解

    一.动画类型 Android的animation由四种类型组成:alpha.scale.translate.rotate XML配置文件中 alpha 渐变透明度动画效果 scale 渐变尺寸伸缩动画效果 translate 画面转换位置移动动画效果 rotate 画面转移旋转动画效果 Java Code代码中 AlphaAnimation 渐变透明度动画效果 ScaleAnimation 渐变尺寸伸缩动画效果 TranslateAnimation 画面转换位置移动动画效果 RotateAnim

  • ios 动画详解

    实现iphone漂亮的动画效果主要有两种方法,一种是UIView层面的,一种是使用CATransition进行更低层次的控制, 第一种是UIView,UIView方式可能在低层也是使用CATransition进行了封装,它只能用于一些简单的.常用的效果展现,这里写一个常用的示例代码,供大家参考. 1.使用UIView类函数实现: //UIViewAnimationTransitionFlipFromLeft, 向左转动 //UIViewAnimationTransitionFlipFromRig

  • [置顶] Core Animation(三)动画详解

    一.概要 前两篇Core Animation(一)iOS图形和动画的初步认识和Core Animation(二)动画基础部分从动画的核心库架构.基本用法.效果.分类等几方面简单的描述了iOS动画,并举了几个简单的例子,通过例子来看,iOS动画学习貌似也挺容易的,所以这一篇我们深入且系统的了解下Core Animation. 在iOS中核心动画分为几类:基本动画.关键帧动画.动画组.过度动画,如下图: CAAnimation:核心动画的基础类,不能直接使用,负责动画运行时间.速度的控制,本身实现了

  • Android 动画详解(转载)

    Contents: Animations Tween Animations AnimationSet Interpolator Frame-By-Frame Animations LayoutAnimationsController AnimationListener Animations 一.Animations介绍 Animations是一个实现android UI界面动画效果的API,Animations提供了一系列的动画效果,可以进行旋转.缩放.淡入淡出等,这些效果可以应用在绝大多数的控

  • android旋转动画和平移动画详解,补充说一下如果制作gif动画放到csdn博客上

    先上效果图: 我这里用的是GifCam来制作的gif动画,可以在http://download.csdn.net/detail/baidu_nod/7628461下载, 制作过程是先起一个模拟器,然后把GifCam的框拖到模拟器上面,点击Rec的new先,然后点击Rec,然后就save到本地成gif文件 这里做一个左右旋转,上下旋转,和左右移动的动画,先自己建立一个View的类,作为操作的对象: public class MyView extends View { private Paint m

  • Android 模仿iPhone列表数据View刷新动画详解

    因为我本人很喜欢在不同的页面之间跳转时加点好玩的动画,今天无意间看到一个动画效果感觉不错,几种效果图如下:既然好玩就写在博客中,直接说就是:该效果类似于iPhone中View的切换动画效果,今天就只介绍上面展示的效果. 废话不多说,先上效果,再看代码!! 效果一: 效果二: 效果三: 效果四:(犯错的效果): 效果五(回旋效果一): 效果六(回旋效果二): 效果看完了,就来看下上面效果实现的具体代码吧, 中间会把我自己试验的.犯的错误都以注释的形式写下来的, 大家使用的时候别出错就行了!先来看下

Tags: