支付宝的会员页的卡片,有一个左右翻转手机,光线随手势移动的效果。html
咱们也要实现这种效果,可是咱们的卡片是在RN页里的,那么RN可否实现这样的功能呢?前端
开始先看了一下react-native-sensors, 大概写法是这样node
subscription = attitude.subscribe(({ x, y, z }) => { let newTranslateX = y * screenWidth * 0.5 + screenWidth/2 - imgWidth/2; this.setState({ translateX: newTranslateX }); } ); 复制代码
这仍是传统的刷新页面的方式——setState,最终JS和Native之间是经过bridge进行异步通讯,因此最后的结果就是会卡顿。react
如何能不经过bridge,直接让native来更新view的呢 答案是有——Using Native Driver for Animated!!!android
Animated API能让动画流畅运行,经过绑定Animated.Value到View的styles或者props上,而后经过Animated.timing()等方法操做Animated.Value进而更新动画。更多关于Animated API能够看这里。git
Animated默认是使用JS driver驱动的,工做方式以下图:github
此时的页面更新流程为:react-native
[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.bash
能够使用Animated.event关联Animated.Value到某一个View的事件上。markdown
<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>
复制代码
RN文档中关于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能够实现渲染都在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以后,页面更新就没有JS的参与了
[N] Native use
CADisplayLink
orandroid.view.Choreographer
to updateAnimated.Value
[N] Interpolate calculation
[N] UpdateAnimated.View
props
[N] TheUIView
orandroid.View
is updated.
咱们如今想要实现的效果,实际须要的是传感器的实时翻转角度数据,若是有一个相似ScrollView的onScroll的event映射出来是最合适的,如今就看如何实现。
首先看JS端,Animated API有个createAnimatedComponent方法,Animated内部的API都是用这个函数实现的
const Animated = {
View: AnimatedImplementation.createAnimatedComponent(View),
Text: AnimatedImplementation.createAnimatedComponent(Text),
Image: AnimatedImplementation.createAnimatedComponent(Image),
...
}
复制代码
而后看native,RCTScrollView的onScroll是怎么实现的
RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:eventName
reactTag:self.reactTag
scrollView:scrollView
userData:userData
coalescingKey:_coalescingKey];
[_eventDispatcher sendEvent:scrollEvent];
复制代码
这里是封装了一个RCTScrollEvent,实际上是RCTEvent的一个子类,那么必定要用这种方式么?不用不能够么?因此使用原始的调用方式试了一下:
if (self.onMotionChange) { self.onMotionChange(data); } 复制代码
发现,嗯,不出意料地not work。那咱们调试一下onScroll最后在native的调用吧:
因此最后仍是要调用[RCTEventDispatcher sendEvent:]来触发Native UI的更新,因此使用这个接口是必须的。而后咱们按照RCTScrollEvent来实现一下RCTMotionEvent,主体的body函数代码为:
- (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')} /> 复制代码
最终实现效果:
上面的实现方式有一点不太好,就是须要在render中写一个无用的AnimatedMotionView,来实现Animated.event和Animated.Value的链接。那么有没有方法去掉这个无用的view,像一个RN的module同样使用咱们的组件呢?
Animated.event作的事情就是将event和Animated.Value关联起来,那么具体是如何实现的呢?
首先咱们看一下node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js
中createAnimatedComponent
的实现,里面调用到attachNativeEvent
这个函数,而后调用到native:
NativeAnimatedAPI.addAnimatedEventToView(viewTag, eventName, mapping);
复制代码
咱们看看native代码中这个函数是怎么实现的:
- (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中的信息最终构造出一个eventDriver,这个driver最终会在咱们native构造的RCTEvent调用sendEvent的时候调用到:
- (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]; } } 复制代码
等等,那么那个viewTag和eventName的做用,就是链接起来变成了一个key?What?
这个标识RN中的view的viewTag最后只是变成一个惟一字符串而已,那么咱们是否是能够不须要这个view,只须要一个惟一的viewTag就能够了呢?
顺着这个思路,咱们再看看生成这个惟一的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方法来生成这个viewTag。
2019.02.25更新:在RN0.58.5中,因为没有暴露allocateTag()方法,因此只能赋给tag一个大数来做为workaround
到此为止,咱们就能够使用AnimatedImplementation中的attachNativeEvent方法来链接Animated.event和Animated.Value了,没必要须要在render的时候添加一个无用的view。
详细代码请移步Github: github.com/rrd-fe/reac…,以为不错请给个star :)
最后,欢迎你们star咱们的人人贷大前端团队博客,全部的文章还会同步更新到知乎专栏 和 掘金帐号,咱们每周都会分享几篇高质量的大前端技术文章。
facebook.github.io/react-nativ…