浅谈 React Native 动画

在我看来,无论是用 OC还是 RN 动画无疑都是我的痛点。但是在开发3.13.0版本的时候,有一个界面必须要使用动画,并且这个界面还要用RN开发,一开始我的拒绝的,但是为了产品交互,给用户带来更好的体验,只能硬着头皮上了。其实该动画说难也不难,如果用OC就是分分钟钟的事,但是关于RN动画方面之前根本没有接触过,所以就没怎么研究。因此我花了两天才把这个动画搞出来,也是给自己点个赞。

如果你不也不太了解react-native中的动画,不妨先看看官方文档

下面我对用到的属性及方法做一个简要概述:

Animation

使用范围:

在react-native中有且仅有三个组件支持animation,它们分别是:Image,View,Text,用的最多的可能是View。

执行动画函数:

  • start():开始动画

  • stop(): 结束动画

Value

在Animation中,设置一种类型的动画后,也要声明Value,就是动画的变化值。一般会将Value在this.state中声明,通过改变改value来实现动效,官网上给的例子就是给spring类型的动画设置bounceValue,有兴趣的小伙伴可以去官网上看,这里不做赘述。

动画类型:

  • spring: 弹跳动画,它包括两个参数

    friction:摩擦力默认值为7

    tension:张力,默认值为40

  • decay: 以一个初始值开始逐渐减慢至停止,它亦包括两个参数

    velocity:起始速度,不可缺省哦!

    deceleration:速度递减比例,默认值为0.997。

  • timing: 渐变动画,它有三个可配置参数

    duration:动画持续的时间,默认值为500毫秒

    easing: 定义曲线渐变函数。iOS中默认为Easing.inOut(Easing.ease)

    delay: 延迟多少毫秒后执行,默认为0

组合动画

就像在OC中有组动画一样,react-native也提供了类似组动画的函数,即组合动画。你可以将多个动画通过,parallel, sequence, stagger和delay组合使用,三种方式来组织多个动画。它们所接受的参数都是一个动画数组。

插值 interpolate

插值函数是 Animation 中相对比较重要且强大的函数,如果你想实现比较流畅炫酷的动画,那么插值函数是非用不可的。在接下来我给大家展示的例子中就多次用到interpolate
它主要通过接受一个输入区间inputRange ,然后将其映射到一个输出区间outputRange,通过这种方法来改变不同区间值上的不同动效。

以上介绍的都是Animation中比较常用的API,还有诸如跟踪动态值,输入事件,响应当前动画值,LayoutAnimation 等灯,这里先不做总结,以后再做讲解。

OK,下面切入正题,到底如何实现像下图一样流畅的上拉动画呢?

动效.gif

思路:
1.先将View1布局好,将View2布局到View1下方

2.点击FlipButton时,改变View2的top坐标,并改变 this.state.pullUp,标记FlipButton的状态

3.改变View2的top坐标时改变View1的透明度

4.将FlipButton旋转180度

5.一定要将FlipButton提至Z轴的最顶端,也就是说要高于 View1 和 View2,在它们的上层,这样才能保证,无论是View1面向于用户面还是View2面向于用户,FlipButton都还是那个最初的FlipButton,并永远面向用户,不会被任何视图覆盖。

如图:
未点击Button时 View1 面向于用户,view2在view1下面

上拉前.png

点击Button,View2置于View1上层,并且Button位置变化


上拉后.png

核心代码如下:

在constructor方法中声明我们需要的 pullUpAnim & fadeAnim 并为其赋予初始Value
其中pullUpAnim是当点击FlipButton按钮时上滑View2,在这个动画中将插入改变透明度的插值器,来改变View1的透明度,后面会看到相应代码

export default class Voice extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
              pullUp:false,
            pullUpAnim: {
                pullUp: new Animated.Value(0),
            },
            fadeAnim: {
                descriptionAlpha: new Animated.Value(0),
            },
        };
        this.onFlipButtonPress = this.onFlipButtonPress.bind(this);
    }

下面代码都是View1的布局。其中当执行pullUp动画时,插入改变View1背景透明度的动画,其中inputRange为[0,1],outputRange为[1,0],就是,当pullUp.pullUp的value为0时,View1的opacity为1,不透明;而当pullUp.pullUp的value变为1的时候,View1的opacity为0 ,完全透明,用户将看不到View1。

   render(){
    return (
    <Animated.View style={[styles.container,
    {
    opacity:
    this.state.pullUpAnim.pullUp.interpolate({
    inputRange: [0, 1],
    outputRange: [1, 0],
    })
    }
    ]}>
    <View style={styles.navBar}>
    <Image style={[styles.navBarImage,
    { resizeMode: 'stretch' }]}
    source={App.Image.bg.voiceHeaderShadow} />
    </View>
    
    <View style={styles.navButtonContainer}>
    <TouchableOpacity
    style={styles.returnBtn}
    onPress={this.onReturnButtonPress}>
    <Image source={App.Image.btn.navBackWhite} />
    </TouchableOpacity>
    
    <TouchableOpacity
    style={styles.shareBtn}
    onPress={this.onShareButtonPress}>
    <Image source={App.Image.btn.navShare} />
    </TouchableOpacity>
    </View>
    
    <View style={styles.titleContainer}>
    <Text style={styles.title}>
    {title}
    </Text>
    </View>
    {this.state.voiceData &&
    <NativeComponent.RCTVoicePlayView
    voiceData={this.state.voiceData}
    fromScanQR={this.state.fromScanQR}
    style={{
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        width: App.Constant.screenWidth,
        height: App.Constant.screenWidth === 320 ? App.Constant.screenWidth : App.Constant.screenWidth * 1.1,
        marginTop: 10,
    }}
    />}
{this.state.voiceData &&
    <Animated.View style={styles.functionContainer}>
        <TouchableOpacity
            style={styles.downloadBtn}
            onPress={this.onVoiceDownloadBtnPress}>
            {this.state.isDownload ?
                <Image source={App.Image.btn.voiceDownloaded} />
                :
                <Image source={App.Image.btn.voiceDownload} />
            }
        </TouchableOpacity>
        <TouchableOpacity
            style={styles.bookmarkBtn}
            onPress={this.onVoiceLikeBtnPress}>
            <Image source={this.state.isBookMark ? App.Image.btn.voiceLiked : App.Image.btn.voiceLike} />
        </TouchableOpacity>

        <View style={styles.voicestarBtnContainer}>
            <TouchableOpacity
                style={styles.voicestarBtn}
                onPress={this.onVoiceStarBtnPress}>
                <Image source={this.state.isCommented ? App.Image.btn.voiceStared : App.Image.btn.voiceStar} />
            </TouchableOpacity>
            {this.state.isCommented ?
                <Text style={styles.score}>
                                        {this.state.score.toFixed(1)}
                                    </Text> : null
                                }
                            </View>
                        </Animated.View>
                    }
                </Animated.View >

这里的Comment是我自定义的组件,这里可以理解成View2。从style中可以看出,我将View2的position设为绝对布局,也就是它的位置是固定的,不想对于任何其他控件的位置,不随上下左右控价坐标的改变而改变。而View2的动画效果是从View1的底部逐渐移动到手机屏幕的顶部,同样的,我们给pullUpAnim.pullUp设置再一个插值器,这个插值器主要是针对top属性做修改了,当pullUp为0时,view2的top为屏幕高度,也就是View2距屏幕顶部的距离为screenHeight,当pullUp为1时,View2距屏幕顶部距离为0。

{this.state.voiceData &&
    <Comment voiceID={this.state.voiceID}
        voiceData={this.state.voiceData}
        style={{
            position: 'absolute',
            width: App.Constant.screenWidth,
            height: App.Constant.screenHeight,
            top: this.state.pullUpAnim.pullUp.interpolate({
                inputRange: [0, 1],
                outputRange: [App.Constant.screenHeight, 0]
            }),
        }}
      displayAnim={this.state.pullUpAnim.pullUp} />
 }

下面这段代码就是对FlipButton的布局 ,上面提到过,FlipButton必须在View1 和 View2的上面,在Z轴的最上面,因此我将它放在View1和View2布局的后面,这种方法比较笨,但是我还没找到如何轻易的将一个组件提到Z轴最顶层。
其中FlipButton是自己封装的一个组件,里面主要实现背景色的变化和透明度的变化以及将按钮反转180度。

{this.state.voiceData &&
    <Animated.View style={{
        position: 'absolute',
        marginLeft: 20,
        top: this.state.pullUpAnim.pullUp.interpolate({
            inputRange: [0, 1],
            outputRange: [App.Constant.screenHeight - 40, 30],
        }),
        opacity: this.state.fadeAnim.descriptionAlpha.interpolate({
            inputRange: [0, 1],
            outputRange: [1, 0]
        }),
    }}>
        <FlipButton
            flip={this.state.pullUp}
            style={'white'}
            onPress={this.onFlipButtonPress}
            />
    </Animated.View>
        )
   }   
}

上面代码中在onFlipButtonPress方法中,使用到了渐变动画 timing 执行时间为180毫秒,并为toValue设置新的pullUp,因为上文提到的插值器会根据改值的变化而进行不同的响应,实现不同的透明度变化或top变化。this.state.pullUp的值为 bool 值,false 时为0,true时为1。之所以定义这个值,是因为在自定义的FlipButton中需要使用这个值来配置FlipButton的timing动画。

 // 点击FlipButton事件
     onFlipButtonPress() {
        const pullUp = !this.state.pullUp;
        Animated.timing(
            this.state.pullUpAnim.pullUp,
            {
                duration: 180,
                toValue: pullUp
            }
        ).start(() => {
            this.setState({
                pullUp,
            });
        });
    }

看到这里,是不是有一种感觉,其实this.state.pullUpAnim.pullUp动画并没有去实现任何动画,而是提供了一个容器而已,供其他插值器有容器可以依附,因为需求中的动画,需要我们在点击按钮时不仅改变View1的透明度,还要改变View2距顶部的位置,所以用基本的动画是无法实现的,必须使用插值器在不同的情况下来实现不同的动画效果。这下知道插值器的强大之处了吧,随时随地有需要就给容器动画加插值器就好啦!

OK,今天就到这里吧,如果在阅读过程用发现什么问题,欢迎指正,共勉!

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

推荐阅读更多精彩内容