在我看来,无论是用 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,下面切入正题,到底如何实现像下图一样流畅的上拉动画呢?
思路:
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下面
点击Button,View2置于View1上层,并且Button位置变化
核心代码如下:
在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,今天就到这里吧,如果在阅读过程用发现什么问题,欢迎指正,共勉!