React Native提供了兩種動畫API LayoutAnimation
和Animated
,Animated
是較常用的一個,可以實現多種精細動畫效果。本篇將介紹Animated
。
React Native已將許多常用組件,如View
、Text
、Image
、ScrollView
、FlatList
和SectionList
,提供了封裝好的動畫組件版本。此外,如果有自己定義的組件或者需要使用到的第三方組件並希望它有動畫效果,那麼可以通過Animated.createAnimatedComponent()
方法進行封裝,使其具有動畫能力。
Animated.Value(): 對單一數值進行動畫效果
new Animated.Value(0)
Animated.ValueXY(): 同時對兩個值進行動畫效果,常用於位置的移動
new Animated.ValueXY({x:0, y:0}})
Spring(彈簧): 模擬彈簧的物理特性
friction
:阻力大小,影響彈簧的彈性和幅度。tension
:拉伸張力,主要控制彈簧的反彈速度。speed
: 動畫的執行速度bounciness
: 彈簧的彈力Timing(時機): 在指定的時間範圍內改變動畫值
duration
:設定動畫的總持續時間easing
:動畫的緩動函數,決定了值的變化節奏delay
:設定動畫開始前的等待時間Decay(衰減): 基於給定的初速度,動畫值逐漸減速直到停止
velocity
:指定動畫的初始速度,這是一個必填參數。deceleration
:描述速度的衰減率Composite(組合動畫): 組合多個動畫項目,可以實現同時或按順序執行。
sequence
: 依序執行一組動畫。parallel
: 同時執行多個動畫。stagger
: 等間隔地啟動一組動畫。delay
: 在指定的延遲後啟動動畫。其中timing是最常使用到的,例如要做一個移動一個元素的位置動畫:
import { Animated, Easing } from 'react-native';
// 假設position是一個Animated.Value初始化為0
Animated.timing(position, {
toValue: 100, // 最終位置值
easing: Easing.back(), // 緩動函數,這裡使用了反彈效果
duration: 2000, // 持續時間,即動畫從開始到結束共需2秒
delay: 500 // 延遲時間,即動畫開始前先等待0.5秒
}).start(); // 啟動動畫
插值是將一個值範圍映射到另一個值範圍。
舉例來說,假設你有一個動畫,它的進度是從0到1的變化。但是,你希望一個物件在這期間從0像素移動到100像素的位置。這時,你就可以用這一個方法將這0到1的變化“映射”成0到100的變化。這正是插值的作用。
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100]
});
首先,我們先看看React Native官網怎麼寫的
The core workflow for creating an animation is to create an Animated.Value, hook it up to one or more style attributes of an animated component, and then drive updates via animations using Animated.timing().
soruce
翻譯過來就是:核心流程是先創建一個 Animated.Value
,接著將其連接到一個或多個動畫組件的樣式屬性上,最後利用 Animated.timing()
來驅動這些動畫的更新。
簡單的說可以分為三步驟
Animated.Value
來存儲動畫的值Animated.timing()
來驅動這些動畫的更新看完以上介紹大家可能還是感覺有些抽象,沒關係,我們就來實際做一個Loading動畫吧。
在常用組件篇,有提到RN內建的ActivityIndicator組件,可供我們快速製作Loading動畫,但其依賴於原生組件,在Android/IOS上的表現不同,有時無法滿足客戶需求,若想要做出較客製的Loading動畫就可以用Animated實現。
假設我們的期望的Loading效果是:淺灰圓圈底,藍色4/3圓圈 無限轉動
Animated.Value
來存儲動畫的當前值。設定動畫值(旋轉角度),創建一個 Animated.Value 來代表這個數值。
const rotationValue = useRef(new Animated.Value(0)).current;
這裡的rotationValue即為該動畫的當前角度值。當rotationValue為0時,元素不旋轉;當其為360時,元素旋轉360度。
你或許有注意到,我們使用useRef來保存Animated.Value。這樣做的原因是:使用useRef可以確保動畫值在組件的多次渲染之間保持一致,且不受State重新渲染的影響。
接下來,我們將使用 interpolate 來連接動畫值。在本例中,我們希望 rotationValue 從 0 到 1 變化時,旋轉角度從 '0deg' 到 '360deg'。
const rotationAngle = rotationValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
最後,使用 Animated.timing() 來定義動畫的行為。
useEffect(() => {
Animated.loop(
Animated.timing(rotationValue, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
easing: Easing.linear,
})
).start();
}, [rotationValue]);
要使動畫起作用,我們要用Animated.View動畫組件,然後將 rotationAngle 動畫值放入 transform 屬性的 rotate 中,這樣當rotationAngle 變化時,圓圈就會旋轉。
<Animated.View style={{ ...customStyles.container, transform: [{ rotate: rotationAngle }] }}>
<View style={customStyles.backgroundCircle} />
<View style={customStyles.rotatingCircle} />
</Animated.View>
完成圓圈樣式
// 藍色4/3圓圈樣式
rotatingCircle: {
width: '100%',
height: '100%',
borderRadius: 40,
borderWidth: 10,
borderTopColor: primaryBorderColor,
borderRightColor: primaryBorderColor,
borderBottomColor: primaryBorderColor,
borderLeftColor: 'transparent',
},
// 淺灰色圓圈的樣式
backgroundCircle: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 40,
borderWidth: 10,
borderColor: secondaryBorderColor,
}
完整代碼
import { StyleSheet, View, Animated, Easing } from 'react-native';
import { useEffect, useRef } from 'react';
const primaryBorderColor = '#3498db';
const secondaryBorderColor = '#e0e0e0';
const customStyles = StyleSheet.create({
container: {
position: 'absolute',
left: '50%',
top: '50%',
width: 80,
height: 80,
marginLeft: -40,
marginTop: -40,
},
rotatingCircle: {
width: '100%',
height: '100%',
borderRadius: 40,
borderWidth: 10,
borderTopColor: primaryBorderColor,
borderRightColor: primaryBorderColor,
borderBottomColor: primaryBorderColor,
borderLeftColor: 'transparent',
},
backgroundCircle: { // 淺灰色圓圈的樣式
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: 40,
borderWidth: 10,
borderColor: secondaryBorderColor,
}
});
const RotatingComponent = () => {
const rotationValue = useRef(new Animated.Value(0)).current;
const rotationAngle = rotationValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
useEffect(() => {
Animated.loop(
Animated.timing(rotationValue, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
easing: Easing.linear,
})
).start();
}, [rotationValue]);
return (
<Animated.View style={{ ...customStyles.container, transform: [{ rotate: rotationAngle }] }}>
<View style={customStyles.backgroundCircle} />
<View style={customStyles.rotatingCircle} />
</Animated.View>
);
};
export default RotatingComponent;
今天介紹了動畫的基礎,並實現了一個簡易的Loading動畫。不過動畫領域博大精深,如有意深入暸解,可以再自行研究。當然,若React Native內建的動畫API無法滿足你,也可以去看看社區上的動畫套件,目前在社區中,reanimated
是較為流行的套件,下一篇也將會介紹。
https://juejin.cn/post/7255224807322615866?searchId=2023091500420349FAC813E68F9C75E02C
https://yeeflame.github.io/animate-for-RN/method/Animated.html
https://juejin.cn/post/7100068545953792030
https://reactnative.dev/docs/animations