React Native中如原生般流暢地使用設備傳感器

背景

支付寶的會員頁的卡片,有一個左右翻轉手機,光線隨手勢移動的效果。html

alipay

咱們也要實現這種效果,可是咱們的卡片是在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

Using Native Driver for Animated

什麼是Animated

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 update Animated.Value [JS] Interpolate calculation [JS] Update Animated.View props
[JS→N] Serialized view update events
[N] The UIView or android.View is updated.bash

Animated.event

能夠使用Animated.event關聯Animated.Value到某一個View的事件上。markdown

<ScrollView
  scrollEventThrottle={16}
  onScroll={Animated.event(
    [{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
  )}
>
  {content}
</ScrollView>
複製代碼

useNativeDriver

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 or android.view.Choreographer to update Animated.Value [N] Interpolate calculation
[N] Update Animated.View props
[N] The UIView or android.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')} />
複製代碼

最終實現效果:

motion-event

繼續優化

上面的實現方式有一點不太好,就是須要在render中寫一個無用的AnimatedMotionView,來實現Animated.event和Animated.Value的鏈接。那麼有沒有方法去掉這個無用的view,像一個RN的module同樣使用咱們的組件呢?

Animated.event作的事情就是將event和Animated.Value關聯起來,那麼具體是如何實現的呢?

首先咱們看一下node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.jscreateAnimatedComponent的實現,裏面調用到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咱們的人人貸大前端團隊博客,全部的文章還會同步更新到知乎專欄掘金帳號,咱們每週都會分享幾篇高質量的大前端技術文章。

Reference

facebook.github.io/react-nativ…

facebook.github.io/react-nativ…

medium.com/xebia/linki…

www.raizlabs.com/dev/2018/03…

www.jianshu.com/p/7aa301632…

相關文章
相關標籤/搜索