前言
任何一款應用無疑都或多或少的使用到動畫效果,它對於提升使用者體驗有著無比重要的作用。React Native同樣提供了豐富的動畫API供開發者呼叫,而對於此部分知識的掌握無疑是RN進階的必經之路,本文通過案例帶大家實踐掌握Animated、ART等動畫及繪圖知識。
Animated
ART
手勢系統
實現的效果
動畫基礎
RN目前已更新至v0.56,其動畫API也在不斷的豐富,其中以Animated
為主要核心,集中了動畫建立、執行(組合)、運算(插值)、事件等功能。
建立動畫值物件
this.anim1 = new Animated.Value(0) // 用於單個值
this.anim2 = new Animated.ValueXY({x: 0, y: 0}) // 用於向量值
複製程式碼
執行與組合
- 所謂動畫的執行,實質是改變動畫物件的值,我們可以通過
this.anim1.setValue(1)
方法直接賦值,也可以利用Animated.timing
Animated.spring
Animated.decay
等方法以動畫的方式執行,以timing為例:
Animated.timing(
this.anim1, // 定義的動畫值物件
{
toValue: 1, // 執行到的動畫值
duration: 300, // 持續時間
easing: Easing.bounce, // Easing類提供了多種動畫效果, 須注意屬性和函式的區別
isInteraction: true, // 是否在InteractionManager建立一個interaction handle,此動畫可以加入同步佇列,完成之後會執行runAfterInteractions函式
useNativeDriver:true // 使用原生動畫驅動,啟動動畫前將所有配置資訊傳送至原生端,之後利用原生程式碼在UI執行緒執行動畫,無需兩端溝通,脫離JS執行緒,更加流暢。
}
).start() // 呼叫start方法開始執行
複製程式碼
- 同時也可以通過parallel(同時執行)、sequence(順序執行)、loop(迴圈執行)、stagger和delay等方法組合動畫,例如:
Animated.sequence([ // 首先執行decay動畫,結束後同時執行spring和twirl動畫
Animated.decay(position, { // 滑行一段距離後停止
velocity: {x: gestureState.vx, y: gestureState.vy}, // 根據使用者的手勢設定速度
deceleration: 0.997,
}),
Animated.parallel([ // 在decay之後並行執行:
Animated.spring(position, {
toValue: {x: 0, y: 0} // 返回到起始點開始
}),
Animated.timing(twirl, { // 同時開始旋轉
toValue: 360,
}),
]),
]).start(); // 執行這一整套動畫序列
複製程式碼
運算與插值
- Animated提供了對動畫值的加、減、乘、除、取模等運算。
Animated.add(a, b) // 加
Animated.subtract(a, b) // 減 v0.56新增
Animated.multiply(a, b) // 乘
Animated.divide(a, b) // 除
Animated.modulo(a, modulus) // 取模
Animated.diffClamp(a, min, max) // 返回一個介於min和max之間動畫值
複製程式碼
- 插值也屬於運算的一種方式,可以使用動畫值物件的
interpolate
方法,進行輸入與輸出的對映. 例如:
this.anim1.interpolate({
inputRange: [0, 1],
outputRange: [0, 100], // 將動畫0-1值對映為0-100進行輸出
});
複製程式碼
元件
動畫必須作用在特定元件之上,目前Animated封裝了Image、ScrollView、Text、View元件並匯出,可以直接通過Animated.XXX
的形式使用。對於自定義元件,可以通過createAnimatedComponent
方法建立,具體使用見案例部分。
事件
對於連續呼叫的事件,比如使用手勢進行滾動、平移、縮放等操作,可以通過Animated.event
進行結構化對映,從複雜的事件中提取值並對映到動畫值物件,自動完成setValue
方法的呼叫,例如:
onScroll={Animated.event(
[{ nativeEvent: {
contentOffset: {
x: this.scrollAnimX // scrollAnimX為建立的動畫物件, 對映到e.nativeEvent.contentOffset.x的值
}
}
}]
)}
複製程式碼
繪圖基礎
ART
ART(iOS端需預先引入)是RN提供的繪圖API,主要涵蓋了以下內容:
const {
Surface,
Shape,
Group,
Text,
Path,
ClippingRectangle,
LinearGradient,
RadialGradient,
Pattern,
Transform
} = React.ART
複製程式碼
同普通元件使用一致,通過設定屬性的方式繪製圖形,重點掌握Path類對路徑的繪製,這裡不再贅述,點選瞭解更多
一些外掛
- d3-shape 更加方便的繪製路徑
- d3-scale 實現抽象的資料對映
- react-native-svg 對於熟悉Svg開發的同學推薦使用
案例1
接下來開始我們的編碼工作,首先實現抖音的雙擊點贊特效
原理解析
利用手勢系統監聽雙擊事件,拿到當前觸控點的座標值並建立心形元件,元件內部執行放大並降低透明度的動畫,動畫完畢後移除元件。
實現程式碼
- 單獨建立一個心形元件,其內部完成動畫效果
const ROTATE_ANGLE = ['-35deg','-25deg','0deg','25deg','35deg']
class AnimHeart extends Component{
constructor(props){
super(props)
// 建立一個動畫值物件,並使用插值運算實現透明度和縮放的效果
this.anim = new Animated.Value(0)
// 設定隨機旋轉角度
this.rotateValue = ROTATE_ANGLE[Math.floor(Math.random()*4)]
}
render(){
const {x, y} = this.props
return <Animated.Image
style={{
position:'absolute',
left: x,
top: y,
opacity: this.anim.interpolate({
inputRange:[0, 1, 2],
outputRange:[1, 1, 0] // 根據動畫值0-1-2的變化,調整透明度
}),
transform: [{
scale: this.anim.interpolate({
inputRange: [0, 1, 2],
outputRange: [1, 0.8, 2] // 根據動畫值0-1-2的變化,調整縮放比例
})
},{
rotate: this.rotateValue
}]
}}
source={require('./cc-heart.png')}
/>
}
componentDidMount(){
// 使用順序執行動畫函式
Animated.sequence([
// 使用彈簧動畫函式
Animated.spring(
this.anim,
{
toValue: 1,
useNativeDriver: true, // 使用原生驅動
bounciness: 5 // 設定彈簧比例
}
),
// 使用定時動畫函式
Animated.timing(
this.anim,
{
toValue: 2,
useNativeDriver: true
}
)
]).start(()=>{
// 動畫完成後回撥
this.props.onEnd && this.props.onEnd()
})
}
componentWillUnmount(){
//console.warn('unmount')
}
// 禁止該元件重新渲染,提升效能
shouldComponentUpdate(){
return false
}
}
/*
注意:
動畫序列中,如果第一個動畫中的useNativeDriver設定為true,
此時動畫便交於原生端進行執行,不可再切換為JS驅動,後續動畫的useNativeDriver也必須設定為true
*/
複製程式碼
- 建立手勢,檢測雙擊事件,根據座標點渲染
AnimHeart
class App extends Component {
constructor(props){
super(props)
this.state = {
heartList: []
}
this.tapStartTime = null
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: this._onPanResponderGrant
})
}
_onPanResponderGrant = (ev)=>{
if(!this.isDoubleTap()) return
const { pageX, pageY } = ev.nativeEvent
// 設定位置資料,渲染AnimHeart元件
this.setState(({heartList})=>{
heartList.push({
x: pageX - 60,
y: pageY - 60,
key: shortid.generate() // 使用shortid生成唯一的key值
})
return {
heartList
}
})
}
// 檢測是否為雙擊
isDoubleTap(){
const curTime = +new Date()
if(!this.tapStartTime || curTime - this.tapStartTime > 300) {
this.tapStartTime = curTime
return false
}
this.tapStartTime = null
return true
}
render() {
return (
<View
{...this._panResponder.panHandlers}
style={styles.container}>
{
this.state.heartList.map(({x, y, key}, index)=>{
return <AnimHeart
onEnd={()=>{
// 動畫完成後銷燬元件
this.setState(({heartList})=>{
heartList.splice(index,1)
return {
heartList
}
})
}}
key={key} // 不要使用index作為key值
x={x}
y={y}
/>
})
}
</View>
);
}
}
複製程式碼
- 最終效果如下