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

背景

支付寶的會(huì)員頁(yè)的卡片树灶,有一個(gè)左右翻轉(zhuǎn)手機(jī)亿驾,光線隨手勢(shì)移動(dòng)的效果吧雹。

alipay

我們也要實(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 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.

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 or android.view.Choreographer to update Animated.Value
[N] Interpolate calculation
[N] Update Animated.View props
[N] The UIView or android.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)試圖

所以最后還是要調(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)效果:

motion-event

繼續(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.jscreateAnimatedComponent的實(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?

黑人問號(hào)臉

這個(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/

http://www.reibang.com/p/7aa301632e4c

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市奸鸯,隨后出現(xiàn)的幾起案子咪笑,更是在濱河造成了極大的恐慌,老刑警劉巖娄涩,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡惠豺,警方通過查閱死者的電腦和手機(jī)重罪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來球恤,“玉大人辜昵,你說我怎么就攤上這事∷檗啵” “怎么了路鹰?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)收厨。 經(jīng)常有香客問我晋柱,道長(zhǎng),這世上最難降的妖魔是什么诵叁? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任雁竞,我火速辦了婚禮,結(jié)果婚禮上拧额,老公的妹妹穿的比我還像新娘碑诉。我一直安慰自己,他們只是感情好侥锦,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布进栽。 她就那樣靜靜地躺著,像睡著了一般恭垦。 火紅的嫁衣襯著肌膚如雪快毛。 梳的紋絲不亂的頭發(fā)上格嗅,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音唠帝,去河邊找鬼屯掖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛襟衰,可吹牛的內(nèi)容都是我干的贴铜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼瀑晒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼绍坝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瑰妄,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤陷嘴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后间坐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灾挨,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年竹宋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劳澄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜈七,死狀恐怖秒拔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情飒硅,我是刑警寧澤砂缩,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站三娩,受9級(jí)特大地震影響庵芭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜雀监,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一双吆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧会前,春花似錦好乐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至临庇,卻和暖如春笛坦,著一層夾襖步出監(jiān)牢的瞬間区转,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工版扩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人侄泽。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓礁芦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親悼尾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柿扣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容

  • 持續(xù)更新中...... 一套企業(yè)級(jí)的 UI 設(shè)計(jì)語(yǔ)言和 React 實(shí)現(xiàn)。 https://mobile.ant....
    日不落000閱讀 5,673評(píng)論 0 35
  • React Native Controllers Important: Please review the fol...
    taiji1985閱讀 1,818評(píng)論 0 1
  • 原文地址:https://medium.com/airbnb-engineering/react-native-a...
    莫寂嵐閱讀 3,254評(píng)論 0 9
  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,309評(píng)論 0 10
  • 今天我們迎來了高端讀寫工作室十五天中的的最后一天闺魏,在這些天里未状,我們得到了進(jìn)步,我學(xué)會(huì)了文言文的閱讀析桥,學(xué)會(huì)了寫影評(píng)司草,...
    petermeng閱讀 415評(píng)論 1 3