背景
支付寶的會(huì)員頁(yè)的卡片树灶,有一個(gè)左右翻轉(zhuǎn)手機(jī)亿驾,光線隨手勢(shì)移動(dòng)的效果吧雹。
我們也要實(shí)現(xiàn)這種效果骨杂,但是我們的卡片是在RN頁(yè)里的,那么RN能否實(shí)現(xiàn)這樣的功能呢雄卷?
調(diào)研
開始先看了一下react-native-sensors搓蚪,
大概寫法是這樣
subscription = attitude.subscribe(({ x, y, z }) =>
{
let newTranslateX = y * screenWidth * 0.5 + screenWidth/2 - imgWidth/2;
this.setState({
translateX: newTranslateX
});
}
);
這還是傳統(tǒng)的刷新頁(yè)面的方式——setState,最終JS和Native之間是通過bridge進(jìn)行異步通信丁鹉,所以最后的結(jié)果就是會(huì)卡頓妒潭。
如何能不通過bridge,直接讓native來更新view的呢
答案是有——Using Native Driver for Animated4铡vㄔ帧!
Using Native Driver for Animated
什么是Animated
Animated API能讓動(dòng)畫流暢運(yùn)行冯凹,通過綁定Animated.Value到View的styles或者props上谎亩,然后通過Animated.timing()等方法操作Animated.Value進(jìn)而更新動(dòng)畫。更多關(guān)于Animated API可以看這里。
Animated默認(rèn)是使用JS driver驅(qū)動(dòng)的匈庭,工作方式如下圖:
此時(shí)的頁(yè)面更新流程為:
[JS] The animation driver uses
requestAnimationFrame
to updateAnimated.Value
[JS] Interpolate calculation
[JS] UpdateAnimated.View
props
[JS→N] Serialized view update events
[N] TheUIView
orandroid.View
is updated.
Animated.event
可以使用Animated.event關(guān)聯(lián)Animated.Value到某一個(gè)View的事件上夫凸。
<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>
useNativeDriver
RN文檔中關(guān)于useNativeDriver的說明如下:
The
Animated
API is designed to be serializable. By using the native driver, we send everything about the animation to native before starting the animation, allowing native code to perform the animation on the UI thread without having to go through the bridge on every frame. Once the animation has started, the JS thread can be blocked without affecting the animation.
使用useNativeDriver可以實(shí)現(xiàn)渲染都在Native的UI線程,使用之后的onScroll是這樣的:
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true } // <-- Add this
)}
>
{content}
</Animated.ScrollView>
使用useNativeDriver之后嚎花,頁(yè)面更新就沒有JS的參與了
[N] Native use
CADisplayLink
orandroid.view.Choreographer
to updateAnimated.Value
[N] Interpolate calculation
[N] UpdateAnimated.View
props
[N] TheUIView
orandroid.View
is updated.
我們現(xiàn)在想要實(shí)現(xiàn)的效果寸痢,實(shí)際需要的是傳感器的實(shí)時(shí)翻轉(zhuǎn)角度數(shù)據(jù),如果有一個(gè)類似ScrollView的onScroll的event映射出來是最合適的紊选,現(xiàn)在就看如何實(shí)現(xiàn)啼止。
實(shí)現(xiàn)
首先看JS端,Animated API有個(gè)createAnimatedComponent方法兵罢,Animated內(nèi)部的API都是用這個(gè)函數(shù)實(shí)現(xiàn)的
const Animated = {
View: AnimatedImplementation.createAnimatedComponent(View),
Text: AnimatedImplementation.createAnimatedComponent(Text),
Image: AnimatedImplementation.createAnimatedComponent(Image),
...
}
然后看native献烦,RCTScrollView的onScroll是怎么實(shí)現(xiàn)的
RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:eventName
reactTag:self.reactTag
scrollView:scrollView
userData:userData
coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:scrollEvent];
這里是封裝了一個(gè)RCTScrollEvent,其實(shí)是RCTEvent的一個(gè)子類卖词,那么一定要用這種方式么巩那?不用不可以么?所以使用原始的調(diào)用方式試了一下:
if (self.onMotionChange) {
self.onMotionChange(data);
}
發(fā)現(xiàn)此蜈,嗯即横,不出意料地not work。那我們調(diào)試一下onScroll最后在native的調(diào)用吧:
所以最后還是要調(diào)用[RCTEventDispatcher sendEvent:]來觸發(fā)Native UI的更新裆赵,所以使用這個(gè)接口是必須的东囚。然后我們按照RCTScrollEvent來實(shí)現(xiàn)一下RCTMotionEvent,主體的body函數(shù)代碼為:
- (NSDictionary *)body
{
NSDictionary *body = @{
@"attitude":@{
@"pitch":@(_motion.attitude.pitch),
@"roll":@(_motion.attitude.roll),
@"yaw":@(_motion.attitude.yaw),
},
@"rotationRate":@{
@"x":@(_motion.rotationRate.x),
@"y":@(_motion.rotationRate.y),
@"z":@(_motion.rotationRate.z)
},
@"gravity":@{
@"x":@(_motion.gravity.x),
@"y":@(_motion.gravity.y),
@"z":@(_motion.gravity.z)
},
@"userAcceleration":@{
@"x":@(_motion.userAcceleration.x),
@"y":@(_motion.userAcceleration.y),
@"z":@(_motion.userAcceleration.z)
},
@"magneticField":@{
@"field":@{
@"x":@(_motion.magneticField.field.x),
@"y":@(_motion.magneticField.field.y),
@"z":@(_motion.magneticField.field.z)
},
@"accuracy":@(_motion.magneticField.accuracy)
}
};
return body;
}
最終战授,在JS端的使用代碼為
var interpolatedValue = this.state.roll.interpolate(...)
<AnimatedDeviceMotionView
onDeviceMotionChange={
Animated.event([{
nativeEvent: {
attitude: {
roll: this.state.roll,
}
},
}],
{useNativeDriver: true},
)
}
/>
<Animated.Image style={{height: imgHeight, width: imgWidth, transform: [{translateX:interpolatedValue}]}} source={require('./image.png')} />
最終實(shí)現(xiàn)效果:
繼續(xù)優(yōu)化
上面的實(shí)現(xiàn)方式有一點(diǎn)不太好页藻,就是需要在render中寫一個(gè)無用的AnimatedMotionView,來實(shí)現(xiàn)Animated.event和Animated.Value的連接植兰。那么有沒有方法去掉這個(gè)無用的view份帐,像一個(gè)RN的module一樣使用我們的組件呢?
Animated.event做的事情就是將event和Animated.Value關(guān)聯(lián)起來楣导,那么具體是如何實(shí)現(xiàn)的呢废境?
首先我們看一下node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js
中createAnimatedComponent
的實(shí)現(xiàn),里面調(diào)用到attachNativeEvent
這個(gè)函數(shù)筒繁,然后調(diào)用到native:
NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping);
我們看看native代碼中這個(gè)函數(shù)是怎么實(shí)現(xiàn)的:
- (void)addAnimatedEventToView:(nonnull NSNumber *)viewTag
eventName:(nonnull NSString *)eventName
eventMapping:(NSDictionary<NSString *, id> *)eventMapping
{
NSNumber *nodeTag = [RCTConvert NSNumber:eventMapping[@"animatedValueTag"]];
RCTAnimatedNode *node = _animationNodes[nodeTag];
......
NSArray<NSString *> *eventPath = [RCTConvert NSStringArray:eventMapping[@"nativeEventPath"]];
RCTEventAnimation *driver =
[[RCTEventAnimation alloc] initWithEventPath:eventPath valueNode:(RCTValueAnimatedNode *)node];
NSString *key = [NSString stringWithFormat:@"%@%@", viewTag, eventName];
if (_eventDrivers[key] != nil) {
[_eventDrivers[key] addObject:driver];
} else {
NSMutableArray<RCTEventAnimation *> *drivers = [NSMutableArray new];
[drivers addObject:driver];
_eventDrivers[key] = drivers;
}
}
eventMapping中的信息最終構(gòu)造出一個(gè)eventDriver彬坏,這個(gè)driver最終會(huì)在我們native構(gòu)造的RCTEvent調(diào)用sendEvent的時(shí)候調(diào)用到:
- (void)handleAnimatedEvent:(id<RCTEvent>)event
{
if (_eventDrivers.count == 0) {
return;
}
NSString *key = [NSString stringWithFormat:@"%@%@", event.viewTag, event.eventName];
NSMutableArray<RCTEventAnimation *> *driversForKey = _eventDrivers[key];
if (driversForKey) {
for (RCTEventAnimation *driver in driversForKey) {
[driver updateWithEvent:event];
}
[self updateAnimations];
}
}
等等,那么那個(gè)viewTag和eventName的作用膝晾,就是連接起來變成了一個(gè)key?What?
這個(gè)標(biāo)識(shí)RN中的view的viewTag最后只是變成一個(gè)唯一字符串而已务冕,那么我們是不是可以不需要這個(gè)view血当,只需要一個(gè)唯一的viewTag就可以了呢?
順著這個(gè)思路,我們?cè)倏纯瓷蛇@個(gè)唯一的viewTag臊旭。我們看一下JS加載UIView的代碼(RN版本0.45.1)
mountComponent: function(
transaction,
hostParent,
hostContainerInfo,
context,
) {
var tag = ReactNativeTagHandles.allocateTag();
this._rootNodeID = tag;
this._hostParent = hostParent;
this._hostContainerInfo = hostContainerInfo;
...
UIManager.createView(
tag,
this.viewConfig.uiViewClassName,
nativeTopRootTag,
updatePayload,
);
...
return tag;
}
我們可以使用ReactNativeTagHandles的allocateTag方法來生成這個(gè)viewTag落恼。
2019.02.25更新:在RN0.58.5中,由于沒有暴露allocateTag()方法离熏,所以只能賦給tag一個(gè)大數(shù)來作為workaround
到此為止佳谦,我們就可以使用AnimatedImplementation中的attachNativeEvent方法來連接Animated.event和Animated.Value了,不必需要在render的時(shí)候添加一個(gè)無用的view滋戳。
詳細(xì)代碼請(qǐng)移步Github: https://github.com/rrd-fe/react-native-motion-event-manager钻蔑,覺得不錯(cuò)請(qǐng)給個(gè)star :)
Reference
https://facebook.github.io/react-native/docs/animations#using-the-native-driver
https://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html
https://medium.com/xebia/linking-animations-to-scroll-position-in-react-native-5c55995f5a6e
https://www.raizlabs.com/dev/2018/03/react-native-animations-part1/