Flutter PlatformView 在閒魚直播業務中的實踐

作者:閒魚技術——黑荊

背景

閒魚近期實現了端上直播間的 Flutter 技術重構,驗證和拓展了 Flutter 在音視頻領域的業務邊界。因爲直播豐富的玩法和可變的交互,通常我們會在直播間頁面覆蓋一層互動層,用於處理和展示業務互動行爲。這一互動層,通常是由 H5/Weex 等技術來實現的,以滿足動態性和業務投放的需求。因爲其背後有着一整套配套的解決方案和能力,顯然在 Flutter 場景下,複用或移植成熟的 Native 能力是比較好的解決方案,PlatformView 是最適合用於實現該組件的技術,這也是我們採用的方案。

什麼是 PlatformView?

PlatformView 技術是 Flutter 提供的一種能夠將 Native 組件嵌入到 Flutter 頁面中的能力,有了這種能力,一些 Native 上非常成熟的功能組件,例如地圖、廣告頁面、WebView 就可以很方便地和 Flutter 結合,在 Flutter 頁面上展示。

實現技術上,iOS 中,PlatformView 的 Native View 會被加入到 Flutter 的 UI 視圖層級中,這種方式稱之爲 Hybrid Composition;而 Android 支持使用 VirtualDisplay 和 Hybrid Composition 兩種模式,前者將 Native View 繪製到內存中,然後和 Flutter Widget 一起渲染到 Flutter 頁面中,後者和 iOS 上類似。閒魚目前在 Android 中使用的是 VirtualDisplay 這種模式。

直播間互動層組件


互動層是一個覆蓋整個直播間的組件,隸屬於某一特定的直播間,可以跟隨該直播間上下翻頁滑動,一般情況下處於直播間 View 層級的最上層,這樣才能做到可以在直播間任意位置佈局任意的元素並展示。它是一個背景透明的組件,當用戶點擊互動層上的元素時,由互動層來進行響應交互,而當用戶點擊透明區域時,事件會穿透互動層,由下面層級上最合適的組件來進行響應,不影響正常的直播間功能。這就要求我們對該組件的事件分發進行一些處理。這裏主要的處理方案是,獲取用戶的點擊位置座標點,判斷組件該像素點的透明度,根據一定的閾值來區分究竟是該由誰來響應。以 iOS 爲例,我們知道 iOS 的事件響應分爲兩個階段:第一階段用於尋找最佳響應者,即在 View Tree 上不斷調用 hitTest 和 pointInside 方法進行檢測,第二階段纔是真正響應事件。所以對於 iOS 實現來說 ,我們只要干預第一階段,重寫該互動層 View 的 pointInside 方法增加上我們的透明度判斷邏輯,就可以實現。Android 也是進行類似的處理。


PlatformView 互動層的事件分發問題

雖然通過 PlatformView 可以很方便地嵌入互動層 Native View,但在這個場景下事件處理遇到了一些麻煩。通常情況下,如果嵌入的 PlatformView 是一個正常響應事件的組件,那一般不會有問題,但由上面的分析可知,我們需要對事件進行特殊處理,因爲互動層 PlatformView 處於整個直播間的最上層,所以正常情況下,所有它下面的 Flutter 組件都無法響應事件。我們可以將 PlatformView 的 hitTestBehavior 設置爲 PlatformViewHitTestBehavior.transparent,這樣可以將事件穿透到下面其他的 Flutter 組件,但是互動層本身就無法響應事件了。

PlatformView 有兩個身份,一個是作爲 Native View,另外一個是作爲 Flutter 組件樹上的一個節點(Widget),因此它上面的事件由兩部分共同配合處理。

所以我們需要先探究一下 Flutter 框架是如何處理 PlatformView 在 Native 和 Flutter 兩側的事件分發和控制的,下面以 iOS 爲例來進行具體分析。

PlatformView 事件響應分析



由 UI 層級圖和下面的源碼可以看出,PlatformView 是 FlutterView 的 subView。源碼中的 embeded_view 是我們真正提供給 Flutter 的 Native View,它會傳遞給 FlutterTouchInterceptingView 的構造方法,成爲其 subView,FlutterTouchInterceptingView 又是 ChildClippingView 的 subView。


FlutterTouchInterceptingView

這幾個 view 中,FlutterTouchInterceptingView 是 Flutter 用來控制和處理 PlatformView 事件的關鍵。Flutter 需要有這個一個View 來攔截事件,因爲事件畢竟是從 Native 傳遞給 Flutter 的,而 PlatformView 本身也是個 Native View,如果不對作用在 PlatformView 上的事件進行攔截的話,PlatformView 自身就會消化掉事件,而 Flutter 側則感知不到也沒法控制了。



FlutterTouchInterceptingView 的 frame 和 embededView 保持一致,並作爲其 superView,根據 iOS 的事件傳遞規則,FlutterTouchInterceptingView 會先接收到事件。由註釋可知,FlutterTouchInterceptingView 實現了兩個能力:一是它會延遲或者阻止事件到達 embededView;二是它會將所有的事件直接轉發給 FlutterView。而這兩點,分別是由 DelayingGestureRecognizer 和 ForwardingGestureRecognizer 這兩個手勢來完成的。


DelayingGestureRecognizer

DelayingGestureRecognizer 需要延遲其他事件響應,並且將除了 ForwardingGestureRecognizer 之外的其他手勢都失效。



DelayingGestureRecognizer 是添加在 embededView 的 superView(FlutterTouchInterceptingView) 上的手勢(Gesture),之所以能夠起作用攔截 embededView 的手勢和事件,原因在於 iOS 的手勢識別優先級高於普通的事件響應,且響應鏈一旦確定,每次事件響應,整條響應鏈上手勢的 delegate 方法都會被調用,用於確定最終可以識別的手勢。如下圖,如果觸摸了 View4,確定 View 4 爲最佳響應者,則從 View4 到 rootView 上的所有手勢(gesture2、gesture3、gesture4、gesture5)的 delegate 方法都會被調用。


ForwardingGestureRecognizer

ForwardingGestureRecognizer 的實現就很簡單了,它重寫了事件的相關處理方法,將事件直接轉發。


因爲 PlatformView 也作爲 Flutter Widget Tree 的一個節點,事件轉發到 Flutter 之後,遵循 Flutter 的事件處理機制,和其他手勢一起在競技場(Arena)中角逐是否能夠響應。

最終如果競爭成功,事件該由 PlatformView 來響應,則 FlutterTouchInterceptingView 的 releaseGesture 方法會被調用,DelayingGestureRecognizer 手勢會被置成 Failed 狀態,其他事件就可以開始響應。相應地如果競爭失敗,那麼 FlutterTouchInterceptingView 的 blockGesture 方法會被調用,DelayingGestureRecognizer 手勢會被置成 Ended 狀態,這表明事件被它響應並且完成了,其他手勢或者 View 響應者就不會再響應該事件了。



解決事件分發問題

由上面 iOS 上的的事件原理可知,Flutter 主要是通過 DelayingGestureRecoginzer 和 ForwardingGestureRecoginizer 這兩個手勢來干預和控制 PlatformView 上 Native 的事件分發行爲。所以可以想到,如果沒有這兩個事件,事件的響應又會和我們熟悉的 Native 流程一致。

所以想要自定義 PlatformView 事件分發,在 iOS 上我們可以這麼做:

  1. 根據需要設置 PlatformView Widget 的 hitTestBehavior 參數;
  2. 重寫 PlatformView 的 pointInside 方法,在裏面增加控制 Flutter 這兩個手勢的邏輯。因爲 hitTest 尋找最佳響應者的過程一定在響應鏈響應之前,所以此處對 Flutter 手勢的處理,不會影響事件轉發給 Flutter 後的處理邏輯。

具體到直播互動場景來說,爲了能讓事件在大多數情況下能夠被互動層下面的組件響應,在 Flutter 側 PlatformView Widget 的 hitTestBehavior 需要設置爲 PlatformViewHitTestBehavior.transparent,目的是爲了讓 PlatformView Widget 之下的 Flutter Widget 可以響應事件。重寫 PlatformView 的 pointInside,如果透明度判斷認爲該由互動層來響應,則禁用 Flutter 的這兩個手勢;如果不該由互動層響應(即該由其他 Flutter 組件來響應),則恢復這兩個手勢響應,不影響正常的邏輯。相關實現代碼如下:



因爲 DelayingGestureRecoginzer 和 ForwardingGestureRecoginizer 這兩個手勢是定義在 FlutterTouchInterceptingView 中,而 FlutterTouchInterceptingView 是我們互動層 Native View 的 superView,所以代碼中的 self.fluttterForwardGestureRecognizer 和 self.flutterDelayingGestureRecognizer 可以通過反射、循環遍歷 superView 的手勢列表來獲取到。

關於 Android 上的處理

因爲 Android 上 PlatfromView 採用了 VirtualDisplay 方案實現的,所以 FlutterView 和 PlatfromView 並不是真正處於同一個 ViewTree 中,因而在這個問題的處理上面和 iOS 略有不同,但原理相通。這裏簡單的說一下Android 上面的情況。

PlatformView 原本的設計中是由 FlutterView 接收 Android 的原生 TouchEvent,然後轉化爲 Flutter 中用於事件處理的 Event,再分發給 Flutter中 的 Widget。當 PlatformView 的 Widget(AndroidView)接收到事件後,會再次將事件轉化爲 Android 的 TouchEvent,然後轉給 Native 的 PlatformView 實現 View。如圖:

我們可以在 FlutterView 外面再套一層用於事件處理的 View(EventInterceptView),該 View 會優先接收到事件,然後根據實際需要,決定事件轉發給 FlutterView 還是 PlatformView。如圖:

通過上述方案,我們在 iOS 和 Android 兩端都可以做到在 PlatformView 互動層中實現透明度判斷邏輯,並交給正確的響應者(Flutter 組件或者 Native View)來進行事件響應。使用 PlatformView 也讓我們最大限度地複用了直播互動層背後對應的一整套解決方案,業務能力和使用體驗都達到了和 Native 原生實現一樣的效果。

寫在最後

PlatformView 提供了在 Flutter 頁面中嵌入 Native View 的能力,方便了很多業務場景和功能的實現,但 PlatformView 技術也還存在一些問題,例如使用了 PatformView 在某些場景下可能會導致圖片、視頻的紋理錯亂等。閒魚在直播業務中第一次在生產環境中使用了 PlatformView 技術,也解決了一些已知問題,但仍有很多問題是我們還沒有發現和解決的,後續我們也會繼續在這方面進行研究,探索使用 PlatformView 的最佳實踐!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章