ReactNative Animated详解(转)

作者: 滕的世界 | 来源:发表于2016-10-13 14:40 被阅读2722次

    参考链接:
    React Native开发之动画(Animations)

    最近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.View style={[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
        }
    });
    

    demo1
    是不是很简单易懂<(▰˘◡˘▰)> 和JQuery的Animation用法很类似。
    步骤拆解
    一个RN的动画,可以按照以下步骤进行。
    使用基本的Animated组件,如Animated.View Animated.Image Animated.Text (重要!不加Animated的后果就是一个看不懂的报错,然后查半天动画参数,最后怀疑人生
    使用Animated.Value设定一个或多个初始化值(透明度,位置等等)。
    将初始化值绑定到动画目标的属性上(如style)
    通过Animated.timing等函数设定动画参数
    调用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.View style={[styles.demo, {
                opacity: this.state.fadeInOpacity,
                    transform: [{
                        rotateZ: this.state.rotation.interpolate({
                            inputRange: [0,1],
                            outputRange: ['0deg', '360deg']
                        })
                    }]
                }]}><Animated.Text style={{
                    fontSize: this.state.fontSize.interpolate({
                        inputRange: [0,1],
                        outputRange: [12,26]
                    })
                }}>我骑着七彩祥云出现了😈💨</Animated.Text>
                </Animated.View>
        );
    }
    

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

    demo2
    强大的interpolate
    上面的栗子使用了interpolate函数,也就是插值函数。这个函数很强大,实现了数值大小、单位的映射转换,比如
    {   
        inputRange: [0,1],
        outPutRange: ['0deg','180deg']
    }
    

    当setValue(0.5)时,会自动映射成90deg。 inputRange并不局限于[0,1]区间,可以画出多段。 interpolate一般用于多个动画共用一个Animated.Value,只需要在每个属性里面映射好对应的值,就可以用一个变量控制多个动画。 事实上,上例中的fadeInOpacityfontSizerotation用一个变量来声明就可以了。(那你写那么多变量逗我吗(╯‵□′)╯︵┻━┻) (因为我要强行使用parallel �┬─┬ ノ( ' – 'ノ))
    流程控制
    在刚才的栗子中,我们使用了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 <View style={styles.container}>
                   <Text>sequence/delay/stagger/parallel演示</Text>
                   {views}
               </View>;
    }
    

    demo3
    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代码
    [图片上传中。。。(4)]
    动画循环
    Animated的start方法是支持回调函数的,在动画或某个流程结束的时候执行,这样子就可以很简单地实现循环动画了。

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

    demo5
    是不是很魔性?[doge]
    原理篇
    首先感谢能看到这里的小伙伴们:)
    在上面的文章中,我们已经基本掌握了RN Animated的各种常用API,接下来我们来了解一下这些API是如何设计出来的。
    声明: 以下内容参考自Animated原作者的分享视频
    首先,从React的生命周期来编程的话,一个动画大概是这样子写:
    getInitialState() {
        return {left: 0};
    }
     
    render(){
        return (
            <div style={{left: this.state.left}}>
                <Child />
            </div>
        );
    }
     
    onChange(value) {
        this.setState({left: value});
    }
    

    只需要通过requestAnimationFrame调用onChange,输入对应的value,动画就简单粗暴地跑起来了。◕‿◕,全剧终。
    然而事实总是没那么简单,问题在哪?
    我们看到,上述动画基本是以毫秒级的频率在调用setState,而React的每次setState都会重新调用render方法,并切遍历子元素进行渲染,即使有Dom Diff也可能扛不住这么大的计算量和UI渲染。

    demo6
    那么该如何优化呢?
    关键词:

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

    ShouldComponentUpdate
    学过React的都知道,ShouldComponentUpdate是性能优化利器,只需要在子组件的shouldComponentUpdate返回false,分分钟渲染性能爆表。
    [图片上传中。。。(7)]
    然而并非所有的子元素都是一成不变的,粗暴地返回false的话子元素就变成一滩死水了。而且组件间应该是独立的,子组件很可能是其他人写的,父元素不能依赖于子元素的实现。
    <StaticContainer>(静态容器)
    这时候可以考虑封装一个容器,管理ShouldCompontUpdate,如图示:
    [图片上传中。。。(8)]
    小明和老王再也不用关心父元素的动画实现啦。
    一个简单的<StaticContainer>实现如下:

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

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

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

    缓存之后,每次setState时,React通过DOM Diff就不再渲染子元素了。
    上面的方法都有弊端,就是条件竞争。当动画在进行的时候,子元素恰好获得了新的state,而这时候动画无视了这个更新,最后就会导致状态不一致,或者动画结束的时候子元素发生了闪动,这些都是影响用户操作的问题。
    Raw DOM Mutation 原生DOM操作
    刚刚都是在React的生命周期里实现动画,事实上,我们只想要变更这个元素的left值,并不希望各种重新渲染、DOM DIFF等等发生。
    “React,我知道自己要干啥,你一边凉快去“
    如果我们跳出这个生命周期,直接找到元素进行变更,是不是更简单呢?

    demo9
    简单易懂,性能彪悍,有木有?!
    然而弊端也很明显,比如这个组件unmount之后,动画就报错了。
    Uncaught Exception: Cannot call ‘style’ of null
    而且这种方法照样避不开条件竞争——动画值改变的时候,有可能发生setState之后,left又回到初始值之类的情况。
    再者,我们使用React,就是因为不想去关心dom元素的操作,而是交给React管理,直接使用Dom操作显然违背了初衷。
    ↑↑↓↓←→←→BA (秘籍)
    唠叨了这么多,这也不行,那也不行,什么才是真理?
    我们既想要原生DOM操作的高性能,又想要React完善的生命周期管理,如何把两者优势结合到一起呢?答案就是Data Binding(数据绑定)
    render(){
        return(
            <Animated.div style={{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} />;
        }
    }
    

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

    综上,通过封装一个Animated的元素,内部通过数据绑定和DOM操作变更元素,结合React的生命周期完善内存管理,解决条件竞争问题,对外表现则与原生组件相同,实现了高效流畅的动画效果。
    读到这里,应该知道为什么ImageText等做动画一定要使用Animated加持过的元素了吧?
    参考资料

    React Addons Update
    React Component Lifecycle
    Christopher Chedeau – Animated

    1846.743
    好书推荐 React Native开发指南》

    原创文章转载请注明:
    转载自AlloyTeam:http://www.alloyteam.com/2016/01/reactnative-animated/

    相关文章

      网友评论

        本文标题:ReactNative Animated详解(转)

        本文链接:https://www.haomeiwen.com/subject/jevwyttx.html