乾貨 | 攜程機票RN複雜交互實踐

  • 2020 年 3 月 11 日
  • 筆記

作者簡介

海濤,攜程前端開發工程師,負責機票主流程預訂、React Native技術棧相關開發工作。

前言

本文將主要介紹在攜程中文APP中國機票模組中,對往返機票的預定流程改造期間,在React Native中進行複雜動畫、手勢交互的經驗總結,包括複雜交互對於RN頁面的性能開銷,以及在不斷解決問題的過程中總結出來的實踐方案。

一、背景

項目背景源自於產品需求。經過產品調研,舊有的往返機票預定分頁模式在用戶體驗中存在以下痛點:

  • 用戶需要反覆進行資訊確認,確認過程中切換頁面有較強跳出感;
  • 往返的去程列表和返程列表認知度不高,同時分頁模式下往返總價模式理解成本高;

基於這些原因,我們進行往返雙欄改版,希望既可以在同一頁面中展示資訊,又可以去容納更多資訊。相較於舊版分頁面展示往返資訊的模式,分欄的模式將兩程資訊展示在同一頁面左右分屏。這樣可以將返程資訊提前曝光,方便用戶綜合往返程資訊高效選擇航班,降低決策的費力度。項目上線後,在轉化率等業務指標數據上有明顯提升。

二、方案設計

項目主要涉及前端頁面交互UI改造,將往返程放入同一個頁面中以黃金比例分割展示往返內容,通過動畫與手勢,進行兩種狀態之間的相互切換。方案的動畫模式圖如下:

相應的組件層級結構如下圖所示:

從粗略的組件層級結構圖可以看到,每一個航班卡片都有兩種狀態。因此相較於原本的往返分頁模式,往返雙欄需要支撐2倍的數據量,以及近4倍的組件數量。同時涉及手勢、動畫以及長列表,其中頁面中同時存在近二十組不同的動畫。這種情況下對於React Native頁面而言,其所帶來的性能開銷問題顯得更加突出。

三、技術實現

3.1 手勢

對於手勢操作RN提供了較為豐富的手勢識別庫PanResponder,在這些事件API中也不乏存在一些使用中的坑點需要專門去兼容處理。本節主要簡單講述往返雙欄的手勢實現以及遇到的主要問題:

  • Android平台,子View為ScrollView手勢交互事件被列表滾動事件攔截打斷
  • 部分操作場景下,手勢事件通知參數不符合預期

這兩個問題嚴重地影響用戶的交互體驗,針對第一條所導致的問題用戶通過手勢左右切換的過程中,很容易觸發列表的滾動導致手勢中斷,進而導致手勢不跟手以及頁面抖動。

在說明解決方案之前,先簡單闡述一下React Native PanResponder手勢相關API的觸發機制。

事件捕獲階段,申請成為響應器主要包含以下回調:


// 當用戶觸摸開始時是否申請成為響應器onStartShouldSetPanResponderCapture// 當用戶滑動開始時是否申請成為響應器onMoveShouldSetPanResponderCapture

事件冒泡階段,申請成為響應器主要包含以下回調:

// 在事件冒泡階段 當用戶滑動開始時是否申請成為響應器onStartShouldSetPanResponder// 在事件冒泡階段 當用戶滑動開始時是否申請成為響應器// 本文項目使用該回調 處理申請響應器onMoveShouldSetPanResponder

響應事件處理回調主要有以下幾個:


// 手勢開始onPanResponderStart// 手勢移動,項目使用該方法作為跟手移動回調onPanResponderMove// 鬆手TouchUponPanResponderRelease

對於PanResponder手勢處理,當存在嵌套關係時,如圖所示。

其他用於輔助使用的回調事件主要有以下幾個:


// 手勢事件被中斷交出事件控制權onPanResponderTerminate// 是否交出事件控制權onPanResponderTerminationRequest

當PanResponder綁定的父View包含ScrollView作為子View時,在Android平台上即使響應事件已經交由父View做處理,左右滑動時依然會觸發List的滾動。同時當任意一個List觸發Scroll時,均會直接中斷當前PanResponder的響應事件,觸發onPanResponderTerminate交出控制權,同時並不會觸發onPanResponderTerminationRequest。

為了解決這一問題,在onMoveShouldSetPanResponder事件回調中,即獲得控制權時,執行setNativeProps方法禁用List滾動。然後在觸控事件結束之後,釋放重置,恢複列表滾動。採用該方案在真機實驗中,使用setNativeProps可以直接操作,避免觸發頁面刷新影響性能,同時也解決了手勢事件衝突的問題。


if (Platform.OS === 'ios')        return;this.firstTripSectionList.setNativeProps({ scrollEnabled: enable });this.secondTripSectionList.setNativeProps({ scrollEnabled: enable });

另外一個關於手勢遇到的問題是,當用戶在螢幕快速滑動時從onPanResponderTerminate事件獲得的移動參數不可靠與預期不符,此時無論移動方向,事件返回的代表手勢移動距離的參數dx均會為0。項目中將動畫移動的距離作為滑動方向的依據,當為0時無法判斷手勢的移動方向。

為了解決這一問題,我們進行了相應的調試排查,發現當用戶快速滑動時雖然onPanResponderTerminate拿到的dx不正確,不過在獲取控制權時的回調方法onMoveShouldSetPanResponder中可以拿到準確的dx。因此解決這一問題的方案如下,結合dx與tempGestureDirection解決該問題。

3.2 動畫

在手勢左右滑動切換往返程的同時,List中的航班卡片也會以動畫的方式在兩種狀態間切換。另外在頁面中很多交互的細節也是通過動畫來進行切換,所以頁面中存在很多動畫交互,僅用於綁定View的差值動畫便有十幾個。

由於在動畫的同時,也會觸發數據更新、頁面刷新等操作,動畫的性能體驗也是一大瓶頸,對於動畫這一部分主要有以下幾個優化方向:

3.2.1 減少參與動畫的組件數量

解決這一問題需要進行多種方式的優化。

第一點,從組件層級設計上,組件越少,View的嵌套層級越少,能夠帶來更高的渲染性能。由於列表中的每一個Item都需要進行動畫切換,所以List中實際渲染的Item數量越少越好。在保證列表滑動體驗的情況下,當ItemView渲染效率越高,則List的WindowSize閾值可以設置的更小。

對於SectionList或者FlagList的滾動體驗優化,可以針對以下參數作調整處理:

windowSize:設置可視區外最大能被渲染的元素的數量

decelerationRate:list滑動速度需注意分平台表現不同

航班卡⽚⾼度保持⼀致,通過實現getItemLayout⽅法減少⾼度計算開銷。

對於View的嵌套關係以及渲染耗時,可以使用PerformenceProfile工具進行檢測。如下圖所示,通過該工具分析組件層級關係,耗時情況,根據結構進行針對性優化。

第二點,在動畫結構設計上,上線過程中也經過了多版的迭代。最開始採用的是展開態和摺疊態同時進行透明度切換的方式,現在則以zIndex的方式實現。將摺疊態覆蓋在展開態的卡片上,僅需切換摺疊態的透明度即可,這樣便可直接省去了將近一半參與動畫的組件。

3.2.2 將用戶複合操作分解為各個操作元,保持其線性執行

第二個優化方向便是在業務邏輯實現上,盡量保證在動畫執行的過程中不進行其他操作。比如當用戶第一次進入頁面,點擊選擇了一個去程航班時,會需要同時進行多種操作,包括:更新去程選中態、自動動畫展開返程、發送服務更新數據,自動勾選返程航班。

這是用戶的一個行為所產生的操作,如果讓動畫與其他操作同時進行,則非常容易產生掉幀的現象,行程頁面卡頓,所以需要在執行動畫的過程中不進行其他操作。

3.2.3 狀態切換過程不能觸發任何render,使用Native驅動動畫

相較於JS執行緒上執行動畫,在Native執行緒上效率更高,其主要區別可從下圖中了解。通過Native執行緒執行動畫,可以省去多次在JS執行緒計算差值動畫通過橋接器更新組件View的過程,橋接器的調用次數減少,則也可以提升JS與Native進行交互的通道效率,使得動畫效率更高。

使用Native驅動執行動畫是收益最直接最明顯的優化手段,不過使用Native驅動動畫存在一定局限性。Native驅動不能改變布局數據,例如Height、Padding一類的屬性,適用於透明度動畫Opacity以及位移或者旋轉動畫,支援transform中的部分屬性。

中國機票往返的項目則是使用了transformX屬性作為左右滑動的動畫值。其啟用方式:


Animated.timing(this.animatedValue, {        toValue: 0,        duration: duration,        easing: Easing.linear,        useNativeDriver: enableNativeDriver}).start();

collapsable屬性

此外在Android平台上由於存在collapsable屬性,該屬性僅限Android平台。當一個View僅用於布局時,它可能會為了優化而從原生布局樹中移除,該屬性默認開啟。所以默認情況下,Android平台有可能會剔除單純用於布局的View,進而導致屬性開啟時,有概率會導致Android平台上組件的動畫失效,在使用時需要注意。

分析源碼發現,雖然RN框架在createAnimatedComponent時依據this._propsAnimated._isNative來設置collapsable屬性,但是並不是在所有場景下都會生效,如果組件沒有觸發Re-Render則沒有去強制設置該屬性。

因此對於這類AnimatedView需要顯示指定collapsable屬性為false,保證其不會在視圖中被移除。

四、成果對比

經過優化,將連續快速切換去程、返程狀態的手勢動畫從幀率40幀左右提升到了59幀左右,動畫性能得到了很好的改善。優化前後的效果圖如下所示。

優化前