React實現動畫效果
流暢、有意義的動畫對於移動應用使用者體驗來說是非常必要的。和React Native的其他部分一樣,動畫API也還在積極開發中,不過我們已經可以聯合使用兩個互補的系統:用於全域性的佈局動畫LayoutAnimation
,和用於建立更精細的互動控制的動畫Animated
。
Animated
Animated
庫使得開發者可以非常容易地實現各種各樣的動畫和互動方式,並且具備極高的效能。Animated
僅關注動畫的輸入與輸出宣告,在其中建立一個可配置的變化函式,然後使用簡單的start/stop
方法來控制動畫按順序執行。下面是一個在載入時帶有簡單的彈跳動畫的元件示例:
class Playground extends React.Component {
constructor(props: any) {
super(props);
this.state = {
bounceValue: new Animated.Value(0),
};
}
render(): ReactElement {
return (
<Animated.Image // 可選的基本元件型別: Image, Text, View
source={{uri: `http://i.imgur.com/XMKOH81.jpg`}}
style={{
flex: 1,
transform: [ // `transform`是一個有序陣列(動畫按順序執行)
{scale: this.state.bounceValue}, // 將`bounceValue`賦值給 `scale`
]
}}
/>
);
}
componentDidMount() {
this.state.bounceValue.setValue(1.5); // 設定一個較大的初始值
Animated.spring( // 可選的基本動畫型別: spring, decay, timing
this.state.bounceValue, // 將`bounceValue`值動畫化
{
toValue: 0.8, // 將其值以動畫的形式改到一個較小值
friction: 1, // Bouncier spring
}
).start(); // 開始執行動畫
}
}
bounceValue
在建構函式中初始化為state
的一部分,然後和圖片的縮放比例進行繫結。在動畫執行的背後,其數值會被不斷的計算並用於設定縮放比例。當元件剛剛掛載的時候,縮放比例被設定到1.5。然後緊跟著在bounceValue
上執行了一個彈跳動畫(spring),會逐幀重新整理數值,並同步更新所有依賴本數值的繫結(在這個例子裡,就是圖片的縮放比例)。比起呼叫setState
然後重新渲染,這一執行過程要快得多。因為整個配置都是宣告式的,我們可以實現更進一步的優化,只要序列化好配置,然後我們可以在一個高優先順序的執行緒執行動畫。
核心API
大部分你需要的東西都來自Animated
模組。它包括兩個值型別,Value
用於單個的值,而ValueXY
用於向量值;還包括三種動畫型別,spring
,decay
,還有timing
,以及三種元件型別,View
,Text
和Image
。你可以使用Animated.createAnimatedComponent
方法來對其它型別的元件建立動畫。
這三種動畫型別可以用來建立幾乎任何你需要的動畫曲線,因為它們每一個都可以被自定義:
-
spring
: 基礎的單次彈跳物理模型,符合Origami設計標準-
friction
: 摩擦力,預設為7. -
tension
: 張力,預設40。
-
-
decay
: 以一個初始速度開始並且逐漸減慢停止。-
velocity
: 起始速度,必填引數。 -
deceleration
: 速度衰減比例,預設為0.997。
-
-
timing
: 從時間範圍對映到漸變的值。-
duration
: 動畫持續的時間(單位是毫秒),預設為500。 -
easing
:一個用於定義曲線的漸變函式。閱讀Easing
模組可以找到許多預定義的函式。iOS預設為Easing.inOut(Easing.ease)
。 -
delay
: 在一段時間之後開始動畫(單位是毫秒),預設為0。
-
動畫可以通過呼叫start
方法來開始。start
接受一個回撥函式,當動畫結束的時候會呼叫此回撥函式。如果動畫是因為正常播放完成而結束的,回撥函式被呼叫時的引數為{finished: true}
,但若動畫是在結束之前被呼叫了stop
而結束(可能是被一個手勢或者其它的動畫打斷),它會收到引數{finished: false}
。
組合動畫
多個動畫可以通過parallel
(同時執行)、sequence
(順序執行)、stagger
和delay
來組合使用。它們中的每一個都接受一個要執行的動畫陣列,並且自動在適當的時候呼叫start/stop。舉個例子:
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(); // 執行這一整套動畫序列
預設情況下,如果任何一個動畫被停止或中斷了,組內所有其它的動畫也會被停止。Parallel有一個stopTogether
屬性,如果設定為false
,可以禁用自動停止。
插值(interpolate)
Animated
API還有一個很強大的部分就是interpolate
插值函式。它可以接受一個輸入區間,然後將其對映到另一個的輸出區間。下面是一個一個簡單的從0-1區間到0-100區間的對映示例:
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});
interpolate
還支援定義多個區間段落,常用來定義靜止區間等。舉個例子,要讓輸入在接近-300時取相反值,然後在輸入接近-100時到達0,然後在輸入接近0時又回到1,接著一直到輸入到100的過程中逐步回到0,最後形成一個始終為0的靜止區間,對於任何大於100的輸入都返回0。具體寫法如下:
value.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});
它的最終對映結果如下:
輸入 | 輸出 |
---|---|
-400 | 450 |
-300 | 300 |
-200 | 150 |
-100 | 0 |
-50 | 0.5 |
0 | 1 |
50 | 0.5 |
100 | 0 |
101 | 0 |
200 | 0 |
interpolation
還支援任意的漸變函式,其中有很多已經在Easing
類中定義了,包括二次、指數、貝塞爾等曲線以及step、bounce等方法。interpolation
還支援限制輸出區間outputRange
。你可以通過設定extrapolate
、extrapolateLeft
或extrapolateRight
屬性來限制輸出區間。預設值是extend
(允許超出),不過你可以使用clamp
選項來阻止輸出值超過outputRange
。
跟蹤動態值
動畫中所設的值還可以通過跟蹤別的值得到。你只要把toValue設定成另一個動態值而不是一個普通數字就行了。比如我們可以用彈跳動畫來實現聊天頭像的閃動,又比如通過timing
設定duration:0
來實現快速的跟隨。他們還可以使用插值來進行組合:
Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
toValue: pan.x.interpolate({
inputRange: [0, 300],
outputRange: [1, 0],
}),
}).start();
ValueXY
是一個方便的處理2D互動的辦法,譬如旋轉或拖拽。它是一個簡單的包含了兩個Animated.Value
例項的包裝,然後提供了一系列輔助函式,使得ValueXY
在許多時候可以替代Value
來使用。比如在上面的程式碼片段中,leader
和follower
可以同時為valueXY
型別,這樣x和y的值都會被跟蹤。
輸入事件
Animated.event
是Animated API中與輸入有關的部分,允許手勢或其它事件直接繫結到動態值上。它通過一個結構化的對映語法來完成,使得複雜事件物件中的值可以被正確的解開。第一層是一個陣列,允許同時對映多個值,然後陣列的每一個元素是一個巢狀的物件。在下面的例子裡,你可以發現scrollX
被對映到了event.nativeEvent.contentOffset.x
(event
通常是回撥函式的第一個引數),並且pan.x
和pan.y
分別對映到gestureState.dx
和gestureState.dy
(gestureState
是傳遞給PanResponder
回撥函式的第二個引數)。
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}] // scrollX = e.nativeEvent.contentOffset.x
)}
onPanResponderMove={Animated.event([
null, // 忽略原生事件
{dx: pan.x, dy: pan.y} // 從gestureState中解析出dx和dy的值
]);
響應當前的動畫值
你可能會注意到這裡沒有一個明顯的方法來在動畫的過程中讀取當前的值——這是出於優化的角度考慮,有些值只有在原生程式碼執行階段中才知道。如果你需要在JavaScript中響應當前的值,有兩種可能的辦法:
-
spring.stopAnimation(callback)
會停止動畫並且把最終的值作為引數傳遞給回撥函式callback
——這在處理手勢動畫的時候非常有用。 -
spring.addListener(callback)
會在動畫的執行過程中持續非同步呼叫callback
回撥函式,提供一個最近的值作為引數。這在用於觸發狀態切換的時候非常有用,譬如當使用者拖拽一個東西靠近的時候彈出一個新的氣泡選項。不過這個狀態切換可能並不會十分靈敏,因為它不像許多連續手勢操作(如旋轉)那樣在60fps下執行。
後續工作
如前面所述,我們計劃繼續優化Animated,以進一步提升效能。我們還想嘗試一些宣告式的手勢響應和觸發動畫,譬如垂直或者水平的傾斜操作。
上面的API提供了一個強大的工具來簡明、健壯、高效地組織各種各種不同的動畫。你可以在UIExplorer/AnimationExample中看到更多的樣例程式碼。不過還有些時候Animated
並不能支援你想要的效果,下面的章節包含了一些其它的動畫系統。
LayoutAnimation
LayoutAnimation
允許你在全域性範圍內建立
和更新
動畫,這些動畫會在下一次渲染或佈局週期執行。它常用來更新flexbox佈局,因為它可以無需測量或者計算特定屬性就能直接產生動畫。尤其是當佈局變化可能影響到父節點(譬如“檢視更多”展開動畫既增加父節點的尺寸又會將位於本行之下的所有行向下推動)時,如果不使用LayoutAnimation
,可能就需要顯式宣告元件的座標,才能使得所有受影響的元件能夠同步執行動畫。
注意儘管LayoutAnimation
非常強大且有用,但它對動畫本身的控制沒有Animated
或者其它動畫庫那樣方便,所以如果你使用LayoutAnimation
無法實現一個效果,那可能還是要考慮其他的方案。
另外,如果要在Android上使用LayoutAnimation,那麼目前還需要在UIManager
中啟用:
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
var App = React.createClass({
componentWillMount() {
// 建立動畫
LayoutAnimation.spring();
},
getInitialState() {
return { w: 100, h: 100 }
},
_onPress() {
// 讓檢視的尺寸變化以動畫形式展現
LayoutAnimation.spring();
this.setState({w: this.state.w + 15, h: this.state.h + 15})
},
render: function() {
return (
<View style={styles.container}>
<View style={[styles.box, {width: this.state.w, height: this.state.h}]} />
<TouchableOpacity onPress={this._onPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Press me!</Text>
</View>
</TouchableOpacity>
</View>
);
}
});
上面這個例子使用了一個預設值,不過你也可以自己配置你需要的動畫。參見LayoutAnimation.js。
requestAnimationFrame
requestAnimationFrame
是一個對瀏覽器標準API的相容實現,你可能已經熟悉它了。它接受一個函式作為唯一的引數,並且在下一次重繪之前呼叫此函式。一些基於JavaScript的動畫庫高度依賴於這一API。通常你不必直接呼叫它——那些動畫庫會替你管理好幀的更新。
react-tween-state(不推薦,用Animated來替代)
react-tween-state是一個極小的庫,正如它名字(tween:補間)表示的含義:它生成一個節點的狀態的中間值,從一個開始值,結束於一個到達值。這意味著它會生成這兩者之間的值,然後在每次requestAnimationFrame
的時候修改狀態。
在Wikipedia上對於補間動畫(tweening)的定義:
“補間是在兩個影像之間生成中間幀的過程,以使得第一個影像能夠平滑的變化為第二個影像”。補間幀是指在關鍵幀之間用於建立過渡假象的圖畫。”
一個最基礎的從一個值運動到另一個值的辦法就是線性過渡:只需要將結束值減去開始值,然後除以動畫總共需要經歷的幀數,再在每一幀加到當前值上,一直到結束值位置。線性過渡有時候看起來怪異且不自然,所以react-tween-state提供了一系列常用的過渡函式,可以用於使你的動畫更加自然。
這個庫並未隨React Native一起釋出——要在你的工程中使用它,則需要先在你的工程目錄下執行npm i react-tween-state --save
來安裝。
import tweenState from `react-tween-state`;
import reactMixin from `react-mixin`; // https://github.com/brigand/react-mixin
class App extends React.Component {
constructor(props) {
super(props);
this.state = { opacity: 1 };
this._animateOpacity = this._animateOpacity.bind(this);
}
_animateOpacity() {
this.tweenState(`opacity`, {
easing: tweenState.easingTypes.easeOutQuint,
duration: 1000,
endValue: this.state.opacity === 0.2 ? 1 : 0.2,
});
}
render() {
return (
<View style={{flex: 1, justifyContent: `center`, alignItems: `center`}}>
<TouchableWithoutFeedback onPress={this._animateOpacity}>
<View ref={component => this._box = component}
style={{width: 200, height: 200, backgroundColor: `red`,
opacity: this.getTweeningValue(`opacity`)}} />
</TouchableWithoutFeedback>
</View>
)
}
}
reactMixin.onClass(App, tweenState.Mixin);
在上面的例子裡我們變化的是透明度,但你可能也猜到了,我們能變化任何數值的值。可以參考它的說明文件來了解更多資訊。
Rebound (不推薦 – 使用Animated來替代)
Rebound.js是一個安卓版Rebound的JavaScript移植版。它在概念上類似react-tween-state:你有一個起始值,然後定義一個結束值,然後Rebound會生成所有中間的值並用於你的動畫。Rebound基於彈性物理模型,你不需要提供一個動畫的持續時間,它會自動根據彈性係數、助力、當前值和結束值來計算。我們在React Native內部應用了Rebound,比如Navigator
和WarningBox
。
需要注意的是Rebound動畫可以被中斷——如果你在按下動畫的過程中釋放手指,它會從當前狀態彈回初始值。
var rebound = require(`rebound`);
var App = React.createClass({
// 首先我們初始化一個spring動畫,並新增監聽函式,
// 這個函式會在spring更新時呼叫setState
componentWillMount() {
// 初始化spring
this.springSystem = new rebound.SpringSystem();
this._scrollSpring = this.springSystem.createSpring();
var springConfig = this._scrollSpring.getSpringConfig();
springConfig.tension = 230;
springConfig.friction = 10;
this._scrollSpring.addListener({
onSpringUpdate: () => {
this.setState({scale: this._scrollSpring.getCurrentValue()});
},
});
// 將spring的初始值設為1
this._scrollSpring.setCurrentValue(1);
},
_onPressIn() {
this._scrollSpring.setEndValue(0.5);
},
_onPressOut() {
this._scrollSpring.setEndValue(1);
},
render: function() {
var imageStyle = {
width: 250,
height: 200,
transform: [{scaleX: this.state.scale}, {scaleY: this.state.scale}],
};
var imageUri = "https://facebook.github.io/react-native/img/ReboundExample.png";
return (
<View style={styles.container}>
<TouchableWithoutFeedback onPressIn={this._onPressIn}
onPressOut={this._onPressOut}>
<Image source={{uri: imageUri}} style={imageStyle} />
</TouchableWithoutFeedback>
</View>
);
}
});
你還可以為彈跳值啟用邊界,這樣它們不會超出,而是會緩緩接近最終值。在上面的例子裡,我們可以新增this._scrollSpring.setOvershootClampingEnabled(true)
來啟用邊界。參見下面的gif動畫來看一個啟用了邊界的效果:
截圖來自react-native-scrollable-tab-view。
你可以在這裡看到一個類似的例子。
關於setNativeProps
正如直接操作文件所說,setNativeProps
方法可以使我們直接修改基於原生檢視的元件的屬性,而不需要使用setState
來重新渲染整個元件樹。
我們可以把這個用在Rebound樣例中來更新縮放比例——如果我們要更新的元件有一個非常深的內嵌結構,並且沒有使用shouldComponentUpdate
來優化,那麼使用setNativeProps
就將大有裨益。
// 回到上面示例的那個元件中,找到componentWillMount方法,
// 然後將scrollSpring的監聽函式替換為如下程式碼:
this._scrollSpring.addListener({
onSpringUpdate: () => {
if (!this._photo) { return }
var v = this._scrollSpring.getCurrentValue();
var newProps = {style: {transform: [{scaleX: v}, {scaleY: v}]}};
this._photo.setNativeProps(newProps);
},
});
// 最後,我們修改render方法,不再通過style來傳入transform(避免
// 重新渲染時產生衝突);然後給圖片加上ref引用。
render: function() {
return (
<View style={styles.container}>
<TouchableWithoutFeedback onPressIn={this._onPressIn} onPressOut={this._onPressOut}>
<Image ref={component => this._photo = component}
source={{uri: "https://facebook.github.io/react-native/img/ReboundExample.png"}}
style={{width: 250, height: 200}} />
</TouchableWithoutFeedback>
</View>
);
}
不過你沒辦法把setNativeProps
和react-tween-state結合使用,因為更新的補間值會自動被庫設定到state上——Rebound則不同,它通過onSprintUpdate
函式在每一幀中給我們提供一個更新後的值。
如果你發現你的動畫丟幀(低於60幀每秒),可以嘗試使用setNativeProps
或者shouldComponentUpdate
來優化它們。你還可能需要將部分計算工作放在動畫完成之後進行,這時可以使用InteractionManager。你還可以使用應用內的開發者選單中的“FPS Monitor”工具來監控應用的幀率。
導航器場景切換
正如文件導航器對比所說,Navigator
使用JavaScript實現,而NavigatoIOS
則是一個對於UINavigationController
提供的原生功能的包裝。所以這些場景切換動畫僅僅對Navigator
有效。為了在Navigator中重新建立UINavigationController
所提供的動畫並且使之可以被自定義,React Native匯出了一個NavigatorSceneConfigsAPI。
import { Dimensions } from `react-native`;
var SCREEN_WIDTH = Dimensions.get(`window`).width;
var BaseConfig = Navigator.SceneConfigs.FloatFromRight;
var CustomLeftToRightGesture = Object.assign({}, BaseConfig.gestures.pop, {
// 使用者中斷返回手勢時,迅速彈回
snapVelocity: 8,
// 如下設定可以使我們在螢幕的任何地方拖動它
edgeHitWidth: SCREEN_WIDTH,
});
var CustomSceneConfig = Object.assign({}, BaseConfig, {
// 如下設定使過場動畫看起來很快
springTension: 100,
springFriction: 1,
// 使用上面我們自定義的手勢
gestures: {
pop: CustomLeftToRightGesture,
}
});
相關文章
- Javascript實現動畫效果JavaScript動畫
- Flutter動畫實現粒子漂浮效果Flutter動畫
- 用JavaScript實現動畫效果 (轉)JavaScript動畫
- Flutter 類抽屜效果動畫實現。Flutter動畫
- 加入購物車動畫效果實現動畫
- Web 頁面如何實現動畫效果Web動畫
- javascript實現文字拼寫動畫效果JavaScript動畫
- SVG實現動態模糊動畫效果SVG動畫
- SVG 實現動態模糊動畫效果SVG動畫
- 實現漫天飛雪的動畫效果動畫
- React 中常見動畫的實現方式React動畫
- 「React」如何在React中優雅的實現動畫React動畫
- css3實現動畫閃爍效果CSSS3動畫
- ul>li*3 實現翻書動畫效果動畫
- Fiori裡花瓣的動畫效果實現原理動畫
- svg實現矩形水平運動動畫效果SVG動畫
- 用js實現動畫效果核心方式JS動畫
- activity切換無動畫效果的實現動畫
- Flutter實戰動畫番外篇-翻頁效果實現Flutter動畫
- 前端動畫效果實現的簡單比較前端動畫
- javascript實現animate()動畫效果程式碼例項JavaScript動畫
- css3實現的簡單動畫效果CSSS3動畫
- canvas實現簡答動畫張閉嘴效果Canvas動畫
- 自定義RecyclerView動畫——實現remove飛出效果View動畫REM
- PPT中如何實現川流不息的動畫效果動畫
- jQuery實現的元素淡入淡出動畫效果jQuery動畫
- css3和js實現的大白動畫效果CSSS3JS動畫
- CSS3邊框旋轉動畫實現效果CSSS3動畫
- SVG 動畫實現彈性的頁面元素效果SVG動畫
- 萬彩動畫大師教程 | 如何實現物件的閃動的動畫效果動畫物件
- CSS3實現王者匹配時的粒子動畫效果CSSS3動畫
- Flutter入門篇(三)— 如何實現登入動畫效果Flutter動畫
- 教你如何用WPF實現文字粒子閃爍動畫效果動畫
- js利用H5的requestAnimationFrame()API實現動畫效果JSH5requestAnimationFrameAPI動畫
- 滑鼠懸浮實現圖片動畫上下切換效果動畫
- 動畫-CAShapeLayer實現QQ訊息紅點拖拽效果動畫
- javascript實現的動畫效果簡單例項程式碼JavaScript動畫單例
- Android中ScrollView實現拖拽反彈效果動畫AndroidView動畫