在Flutter中嵌入Native組件的正確姿式是...

做者:閒魚技術-塵蕭java

引言

在漫長的從Native向Flutter過渡的混合工程時期,要想平滑地過渡,在Flutter中使用Native中較爲完善的控件會是一個很好的選擇。本文但願向你們介紹AndroidView的使用方式以及在此基礎之上拓展的雙端嵌入Native組件的解決方案。android

1. 使用教程

1.1. DemoRun

嵌入地圖這一場景可能在不少App中都會存在,可是如今的地圖SDK都沒有提供Flutter的庫,而本身開發一套地圖顯然不太現實。這種場景下,使用混合棧的形式是一個比較好的選擇。咱們能夠直接在Native的繪圖樹中嵌入一個Map,可是這個方案嵌入的View並不在Flutter的繪圖樹中,是一種比較暴力且不優雅的方式,使用起來也很費勁。web

這時候,使用Flutter官方提供的控件AndroidView就是一種比較優雅的解決方案了。這裏作了一個簡單的嵌入高德地圖的demo,就讓咱們跟着這個應用場景,看一下AndroidView的使用方式和實現原理。api

demo_pic

1.2. AndroidView使用方式

AndroidView的使用方式和MethodChannel相似,比較簡單,主要分爲三個步驟:markdown

第一步:在dart代碼的相應位置使用AndroidView,使用時須要傳入一個viewType,這個String將用於惟一標識該Widget,用於和Native的View創建關聯。網絡

第二步:在native側添加代碼,寫一個PlatformViewFactory,PlatformViewFactory的主要任務是,在create()方法中建立一個View並把它傳給Flutter(這個說法並不許確,可是咱們姑且能夠這麼理解,後續會進行解釋)app

第三步:使用registerViewFactory()方法註冊剛剛寫好的PlatformViewFactory,該方法須要傳入兩個參數,第一個參數須要和以前在Flutter端寫的viewType對應,第二個參數是剛剛寫好的的PlatformViewFactory。ide

配置高德地圖的部分這裏就省略不說了,官方有比較詳細的文檔,能夠去高德開發者平臺進行查閱。性能

以上即是使用AndroidView的全部操做,整體看起來仍是比較簡單的,可是真正要用起來,仍是有兩個沒法忽視的問題:優化

  1. View最終的顯示尺寸由誰決定?
  2. 觸摸事件是如何處理的?

下面就讓小閒魚來給各位一一解答。

2. 原理講解

想要解決上面的兩個問題,首先必須得理解所謂"傳View"的本質是什麼?

2.1. 所謂"傳View"的本質是什麼?

要解決這個問題,天然避免不了的須要去閱讀源碼,從更深的層面去看這個傳遞的整個過程,能夠整理出一張這樣的流程圖:

咱們能夠看到,Flutter最終拿到的是native層返回的一個textureId。根據native的知識ky h這個textureId是已經在native側渲染好了的view的繪圖數據對應的ID,經過這個ID能夠直接在GPU中找到相應的繪圖數據並使用,那麼Flutter是如何去利用這個ID的呢?

在以前的深刻了解Flutter界面開發中,也給你們介紹了Flutter的繪圖流程。我這裏也給你們再簡單整理一下

Flutter的Framework層最後會遞交給Engine層一個layerTree,在管線中會遍歷layertree的每個葉子節點,每個葉子節點最終會調用Skia引擎完成界面元素的繪製,在遍歷完成後,在調用glPresentRenderBuffer(IOS)或者glSwapBuffer(Android)按完成上屏操做。

Layer的種類有不少,而AndroidView則使用的是其中的TextureLayer。TextureLayer在以前的《Flutter外接紋理》中有更爲詳細的介紹,這裏就再也不贅述。TextureLayer在被遍歷到時,會調用一個engine層的方法SceneBuilder::addTexture() 將textureId做爲參數傳入。最終在繪製的時候,skia會直接在GPU中根據textureId找到相應的繪製數據,並將其繪製到屏幕上。

那麼是否是誰拿到這個ID均可以進行這樣的操做呢?答案固然是否認的,Texture數據存儲在建立它的EGLContext對應的線程中,因此若是在別的線程進行操做是沒法獲取到對應的數據的。這裏須要引入幾個概念:

  • 顯示屏對象(Display):提供合理的顯示器的像素密度和大小的信息
  • Presentation:它給Android提供了在對應的上下文(Context)和顯示屏對象(Display)上繪製的能力,一般用於雙屏異顯。

這裏不展開講解Presentation,咱們只須要明白Flutter是經過Presentation實現了外接紋理,在建立Presentation時,傳入FlutterView對應的Context和建立出來的一個虛擬顯示屏對象,使得Flutter能夠直接經過ID找到並使用Native建立出來的紋理數據。

2.2. View最終的顯示尺寸由誰決定?

經過上面的流程你們應該都能想到,顯示尺寸看起來像是由兩部分決定的:AndroidView的大小,Android端View的大小。那麼實際上究竟是有誰來決定的呢,讓咱們來作一個實驗?

直接新建一個Flutter工程,並把中間改爲一個AndroidView。

//Flutter
class _MyHomePageState extends State<MyHomePage> {
  double size = 200.0;

  void _changeSize() {
    setState(() {
      size = 100.0;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: Container(
        color: Color(0xff0000ff),
        child: SizedBox(
          width: size,
          height: size,
          child: AndroidView(
            viewType: 'testView',
          ),
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _changeSize,
        child: new Icon(Icons.add),
      ),
    );
  }
}
複製代碼

在Android端也要加上對應的代碼,爲了更好地看出裁切效果,這裏使用ImageView。

//Android
@Override
public PlatformView create(final Context context, int i, Object o) {
    final ImageView imageView = new ImageView(context);
    imageView.setLayoutParams(new ViewGroup.LayoutParams(500,500));
    imageView.setBackground(context.getResources().getDrawable(R.drawable.idle_fish));
    return new PlatformView() {
        @Override
        public View getView() {
            return imageView;
        }

        @Override
        public void dispose() {

        }
    };
}
複製代碼

image-20181114170348189

首先先看AndroidView,AndroidView對應的RenderObject是RenderAndroidView,而一個RenderObject的最終大小的肯定是存在兩種可能,一種是由父節點所指定,還有一種是在父節點指定的範圍中根據自身狀況肯定大小。打開對應的源碼,能夠看到其中有個很重要的屬性sizedByParent = true,也就是說AndroidView的大小是由其父節點所決定的,咱們可使用Container、SizedBox等控件控制AndroidView的大小。

AndroidView的繪圖數據是Native層所提供的,那麼當Native中渲染的View的實際像素大小大於AndroidView的大小時,會發生什麼呢?一般狀況下,這種狀況的處理思路無非就兩種選擇,一種是裁切,另外一種是縮放。Flutter保持了其一向的作法,全部out of the bounds的Widget統一使用裁切的方式進行展現,上面所描述的狀況就被看成是一種out of the bounds。

當這個View的實際像素大小小於AndroidView的時候,會發現View並不會相應地變小(Container的背景色並無顯露出來),沒有內容的地方會被白色填充。這其中的緣由是SingleViewPresentation::onCreate中,會使用一個FrameLayout做爲rootView。

2.3. 觸摸事件如何傳遞

Android的事件流你們應該都很熟悉了,自頂向下傳遞,自底向上處理或迴流。Flutter一樣是使用這一規則,可是其中AndroidView經過兩個類來去處理手勢:

MotionEventsDispatcher:負責將事件封裝成Native的事件並向Native傳遞;

AndroidViewGestureRecognizer:負責識別出相應的手勢,其中有兩個屬性:

cachedEventsforwardedPointers,只有當PointerEvent的pointer屬性在forwardedPointers中時纔會去進行分發,不然會存在cacheEvents中。這裏的實現主要是爲了解決一些事件的衝突,好比滑動事件,能夠經過gestureRecognizers來進行處理,這裏能夠參考官方註釋。

/// For example, with the following setup vertical drags will not be dispatched to the Android view as the vertical drag gesture is claimed by the parent [GestureDetector].
/// 
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <OneSequenceGestureRecognizer>[],
/// ),
/// )
/// 
/// To get the [AndroidView] to claim the vertical drag gestures we can pass a vertical drag gesture recognizer in [gestureRecognizers] e.g:
/// 
/// GestureDetector(
/// onVerticalDragStart: (DragStartDetails d) {},
/// child: SizedBox(
/// width: 200.0,
/// height: 100.0,
/// child: AndroidView(
/// viewType: 'webview',
/// gestureRecognizers: <OneSequenceGestureRecognizer>[ new VerticalDragGestureRecognizer() ],
/// ),
/// ),
/// )
複製代碼

因此總結起來,這部分流程總結起來其實也很簡單:事件最初從Native到Flutter這一階段不在本文的討論範圍以內,Flutter按照本身的規則去處理事件,若是AndroidView贏得了事件,事件就會被封裝成相應的Native端的事件而且經過方法通道傳回Native,Native再根據本身的處理事件的規則去處理。

3. 總結

3.1. 方案侷限性

往大里說:這套方案是Google爲了解決開發者日益增加的業務需求與落後的生態環境之間的矛盾而產生的,這一矛盾是一個新生態必然須要去面對的主要矛盾。爲了解決這一個問題,最簡單的方式固然就是容許開發者使用老生態中已經很是成熟的控件。固然,這樣是能夠臨時解決Flutter生態發展不全面的問題,可是使用這套方案不可避免的須要去編寫雙端代碼(甚至如今iOS尚未對應的控件,固然以後確定會更新),不能作到真正的跨端。

往小裏說:這套方案存在着性能上的缺陷,在AndroidView這個類的第三句註釋中,官方就已經提到了這是一套比較昂貴的方案,避免在使用Flutter控件也能實現的狀況下去使用它。若是以前有看過《Flutter外接紋理》這一文章的同窗應該知道,Flutter實現外接紋理的方案中,數據從GPU->CPU->GPU的過程代價是比較大的,在大量使用的場景會形成明顯的性能缺陷。咱們經過一些手段繞過了中間CPU這一步,而且將這項技術在APP中落地,用於處理圖片資源。

3.2. 實際應用

目前閒魚從Native向Flutter的遷移工做遇到了Native的本地圖片資源在Flutter側沒法訪問的問題,在如今Flutter和Native必將長期共存的狀況下,從新拷貝一份資源以Flutter的規則來存儲固然能夠,可是不可避免地增大了包體積,並且很差管理。

面對這個問題,咱們的解法即是借鑑了AndroidView使用Texture的思路並在將其優化。實現了Native和Flutter的圖片資源歸一化。除了用於加載位於Native資源目錄下的本地圖片以外,還能夠利用Native的圖片庫來加載網絡圖片。

咱們這麼去作的緣由是咱們在Native側的圖片庫較爲完善而且經受過大量的線上考驗,如今這一階段,咱們不但願將過多的精力投入到重複造輪子這一件事上,而處理網絡圖片資源和處理本地圖片資源的思路實際上是同樣的,因此咱們選擇將圖片資源進行了統一地整合,在與官方的團隊進行溝通並完善後會和你們同步,敬請關注咱們的公衆號。

3.4. 引用

高德地圖SDK文檔

萬萬沒想到——Flutter外接紋理

Android7.1 Presentation雙屏異顯原理分析

相關文章
相關標籤/搜索