FlutterのPlatform Viewsの表示モードを整理する

## はじめに
FlutterはiOSやAndroidなど複数のプラットフォームで動作するUIフレームワークです。Flutterは独自のUIレンダリングの仕組みを持っており、基本的には各プラットフォームのネイティブUIを使用しないため、プラットフォーム間での動作の差異がほとんどないという特徴があります。

しかしながらマップやウェブビューなど、プラットフォームが提供しているUIをFlutterで表示したいことがあります。そのようなケースに対応するための仕組みが **Platform Views** です。

あるプラットフォーム上でそのプラットフォーム向けのUIを表示する（例えばiOSで`UIButton`を表示する）というのは当たり前なことです。しかし、それをFlutterで構築されたアプリの中で表示する場合、Flutterが独自にレンダリングするUIとプラットフォームのUIを同時に組み合わせて表示しなければならないことになります。

iOS/Androidにおけるそのような課題をFlutterはどのように解決しているのか？を整理する機会があったため、こちらの記事にまとめます。

:::note info
記事中に各表示モードにおいてネイティブでのビュー階層がどうなっているかを、以下のようなプラットフォームビューを含むWidgetで示します。
* 背面に緑色のWidget
* 中間にプラットフォームビューとしてGoogle MapsのWidget
* 前面に青色のWidget

<img alt="iOS" width="195" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/c0193f1d-fee5-8453-6f1d-6e494d9d3fb9.png" />
<img alt="Android" width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/fc4b8d9e-d7c6-afa6-46e2-02e8b7c15cde.png" />

```dart
  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Platform Views'),
        ),
        body: Stack(
          alignment: Alignment.center,
          children: [
            SizedBox(
              width: size.width,
              height: size.height,
              child: const ColoredBox(color: Colors.green),
            ),
            SizedBox(
              width: size.width,
              height: size.height * 0.4,
              child: const GoogleMap(
                initialCameraPosition: CameraPosition(
                  target: LatLng(37.42796133580664, -122.085749655962),
                  zoom: 15,
                ),
              ),
            ),
            SizedBox(
              width: size.width * 0.2,
              height: size.height,
              child: const ColoredBox(color: Colors.blue),
            ),
          ],
        ),
      ),
    );
  }
```
:::

## Android
AndroidにおけるPlatform Viewsの表示モードは以下の3種類から選択することができます。なぜ3つも選択肢があるのかというと、それぞれの表示モードに異なるメリット・デメリットがあり、利用シーンによって最適なモードが異なるためです。逆に言うと、現時点で万能な表示モードが存在しないということになります。

* Virtual Display
* Hybrid Composition
* Texture Layer Hybrid Composition

### Virtual Display
* プラットフォームビューをAndroidの[Virtual Display](https://developer.android.com/reference/android/hardware/display/VirtualDisplay)に描画し、その内容をFlutterの[Texture](https://api.flutter.dev/flutter/widgets/Texture-class.html)に接続する方式
* **テキスト入力やアクセシビリティなどの機能において互換性の問題が発生する**
    * プラットフォームビューがVirtuarl Display内で描画され、実際にはAndroidのビュー階層に存在しないことが原因と思われる
* Android SDK 20（4.4W / KitKat Watch）以降で利用可能
* [公式Wiki / Virtual Display](https://github.com/flutter/flutter/wiki/Virtual-Display)

### Hybrid Composition
* Androidのビュー階層の中に直接プラットフォームビューを表示する方式
    * プラットフォームビューの背面にあるFlutter UIを表示する層
    * プラットフォームビューを表示する層
    * プラットフォームビューの前面にあるFlutter UIを表示する層
* プラットフォームビューがAndroidのビュー階層の中にいるため、Virtual Displayのような互換性の問題が発生しにくい
* **パフォーマンス面で問題があり、特にAndroid 10.0（Q）以前のバージョンが顕著**
* Android SDK 19（4.4 / KitKat）以降で利用可能
* [公式Wiki / Hybrid Composition](https://github.com/flutter/flutter/wiki/Hybrid-Composition)

#### Hybrid Compositionのビュー階層
プラットフォームビューの前面にくるFlutterのWidgetは、複数のビューに分割されて描画されているのが興味深いです。

##### [FlutterImageView](https://api.flutter.dev/javadoc/io/flutter/embedding/android/FlutterImageView.html)
![Android_ビュー階層_HC3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/bed84baf-a387-d16f-6abb-664ac687feaf.png)

:::note info
> ImageReader によって提供された Flutter UI を Canvas に描画します。
> FlutterImageView は、開発者が Flutter UI をレンダリングする必要があるが、同時に対話型のPlatformView もレンダリングする必要がある状況を想定しています。
> このビューは、Flutter UI を画像として提供する ImageReader を取得し、onDraw でそれを Canvas にレンダリングします。

（リファレンスからの抜粋をChatGPTにて翻訳）
:::

##### [FlutterMutatorView](https://api.flutter.dev/javadoc/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.html) / [FlutterMutatorsStack](https://api.flutter.dev/javadoc/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorsStack.html)

![Android_ビュー階層_HC4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/ba098820-3651-d24c-19ba-e9247698f02e.png)

:::note info
> ミューテータは PlatformView に適用され、一連の変換を行うことができます。Mutators に関する情報については、FlutterMutatorsStack.FlutterMutator を参照してください。

（リファレンスからの抜粋をChatGPTにて翻訳）
:::

##### [PlatformOverlayView](https://api.flutter.dev/javadoc/io/flutter/plugin/platform/PlatformOverlayView.html)

![Android_ビュー階層_HC5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/e952ad51-3af4-6775-94c7-c3dacb2c77d0.png)

:::note info
> Flutter コンテンツをプラットフォームビュー上に表示するためのホストビュー。

（リファレンスからの抜粋をChatGPTにて翻訳）
:::

### Texture Layer Hybrid Composition
* Virtual Display・Hybrid Compositionそれぞれの問題を解決して置き換えることを目的に開発され、Flutter 3にて導入された新しい方式
* Hybrid Compositionのようにプラットフォームビューは実際に画面上の正しい位置に配置される
* Virtual Displayと同様に描画はTextureを使用し、drawをリダイレクトすることでTextureに描画内容を反映する
* **プラットフォームビューが [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView) そのもの、もしくはそれを含む場合に機能しない**
  * これによりVirtual DisplayやHybrid Compositionを完全に置き換えられない
* Android SDK 23（6.0 / Marshmallow）以降で利用可能
* [公式Wiki / Texture Layer Hybrid Composition](https://github.com/flutter/flutter/wiki/Texture-Layer-Hybrid-Composition)

#### Texture Layer Hybrid Compositionのビュー階層

##### [FlutterSurfaceView](https://api.flutter.dev/javadoc/io/flutter/embedding/android/FlutterSurfaceView.html)
![Android_ビュー階層_TLHC2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/ed180648-cc5d-f38b-3383-710d9ba992c4.png)

##### [PlatformViewWrapper](https://api.flutter.dev/javadoc/io/flutter/plugin/platform/PlatformViewWrapper.html)

![Android_ビュー階層_TLHC3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/f7e3452a-7a9d-7faa-a154-fe9dc06cd20a.png)

:::note info
> プラットフォームビューをラップしてジェスチャーをインターセプトし、このビューをPlatformViewRenderTarget に投影します。
> Android プラットフォームビューは、TextureLayer を使用してエンジンによって構成されます。ビューは通常のビューのように Android ビューヒエラルキーに埋め込まれますが、PlatformViewRenderTarget に投影されるため、エンジンによって効率的に構成できます。
> ビューは Android ビューヒエラルキーに存在するため、キーボードおよびアクセシビリティの操作は通常通りに動作します。

（リファレンスからの抜粋をChatGPTにて翻訳）
:::

### 表示モードの指定方法
Flutterの [PlatformViewService](https://api.flutter.dev/flutter/services/PlatformViewsService-class.html) の初期化方法によって表示モードを指定することができる。

* [PlatformViewService.initAndroidView](https://api.flutter.dev/flutter/services/PlatformViewsService/initAndroidView.html)
  * Texture Layer Hybrid Composition モードが適用される
  * 適用できない場合（※）は Virtual Display モードにフォールバックされる
* [PlatformViewService.initSurfaceAndroidView](https://api.flutter.dev/flutter/services/PlatformViewsService/initSurfaceAndroidView.html)
  * Texture Layer Hybrid Composition モードが適用される
  * 適用できない場合（※）は Hybrid Composition モードにフォールバックされる
* [PlatformViewService.initExpensiveAndroidView](https://api.flutter.dev/flutter/services/PlatformViewsService/initExpensiveAndroidView.html)
  * Hybrid Composition モードが適用される

:::note warn
※ 以下のいずれかの場合は Texture Layer Hybrid Compositionモードを適用できない
* Android SDK 23未満
* プラットフォームビューに [SurfaceView](https://developer.android.com/reference/android/view/SurfaceView) を含む
:::

## iOS
iOSの表示モードはHybrid Compositionのみです。AndroidのHybrid Compositionと同じ仕組みをとっていますが、パフォーマンス面の問題もないようです。

[公式Wiki / Hybrid Composition iOS](https://github.com/flutter/flutter/wiki/Hybrid-Composition-iOS)

#### ビュー階層
##### FlutterView
![iOS_ビュー階層_HC1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/f6208c50-93bc-b71e-821a-8453878f0b9d.png)

##### ChildClippingView > FlutterTouchInterceptingView
![iOS_ビュー階層_HC2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/f6bed471-1dcf-b8ca-fae9-be7d23702460.png)

##### FlutterOverlayView
![iOS_ビュー階層_HC3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16508/8d945ed7-8559-2b35-c09a-e612cfe4a0e0.png)