React-Native之跃首页投稿(暂停使用,暂停投稿)手机移动程序开发

浅谈 React Native 动画

2017-01-16  本文已影响1611人  Jeavil_Tang

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

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

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

Animation

使用范围:

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

执行动画函数:

Value

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

动画类型:

组合动画

就像在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,今天就到这里吧,如果在阅读过程用发现什么问题,欢迎指正,共勉!

上一篇 下一篇

猜你喜欢

热点阅读