1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-PlatformView.md 32 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Взаимодействие платформенных представлений в Flutter и Compose: сравнение реализации

После публикации статьи «Глубинное понимание реализации алгоритма diff в Flutter и Compose при отрисовке UI» мне поступили запросы от экспертов, чтобы узнать больше о реализации PlatformView в Flutter и Compose. Учитывая мой опыт работы с PlatformView в Flutter, теперь я могу провести простое сравнение:

Иллюстрация

Flutter

Flutter использует различные методы для интеграции платформенных компонентов в Android. Как независимый набор UI-библиотек, Flutter предлагает несколько способов использования PlatformView, что делает его подходящим для различных сценариев.

На данный момент существуют три основных метода поддержки PlatformView в Android:

  • Виртуальное отображение (Virtual Display, VD)
  • Гибридная композиция (Hybrid Composition, HC)
  • Гибридная композиция слоев текстур (Texture Layer Hybrid Composition, TLHC)

VD

VD представляет собой использование виртуального отображения для рендера нативных компонентов в памяти. Затем этот компонент занимает место на экране Flutter через ID, а затем связывается с текстурой Flutter для отображения.Проблема заключается в том, что компоненты фактически не существуют на месте отображения, можно сказать, что это просто "зеркальное" отображение UI в памяти или "зеркало второго экрана". Поэтому любые действия с этими компонентами требуют двукратной передачи данных между Flutter и нативной средой.Кроме того, поскольку компоненты рендерятся в памяти, взаимодействие с клавиатурой требует двухуровневого прокси-сервера, что может вызывать проблемы с вводом данных и взаимодействием, особенно в случае WebView.

Конечно, современная версия VD значительно лучше первоначальной и продолжает использоваться для совместимости.


Схема VD

Поддержка режима HC с версии 1.2

С версии 1.2 поддерживается режим HC, в котором нативные компоненты просто «накладываются» поверх FlutterView, то есть нативные компоненты добавляются в FlutterView через метод addView. В случае необходимости отображения Flutter-компонентов поверх нативных используется новый компонент FlutterImageView, который служит для создания нового слоя.

Например, в инспекторе макета можно видеть границы различных нативных макетов:

На следующем рисунке показано, как два синих TextView добавлены поверх FlutterView, а красный текст RE скрыт за ними. Верхний красный текст также является Flutter-компонентом, но поскольку он должен отображаться поверх TextView, используется ещё один FlutterImageView, чтобы обеспечить смешивание Flutter-компонентов и нативных компонентов.

Здесь FlutterImageView также решает проблему синхронизации анимации и рендера.Однако такой подход привёл к проблеме синхронизации потоков, так как нативные компоненты рендерятся непосредственно в платформенном потоке, что вызывает проблемы при работе в UI-потоке Flutter. Это привело к появлению багов со сбоем экрана в некоторых случаях.

Хотя эта проблема была решена путём синхронизации потоков, это привело к некоторым потерям производительности, особенно до Android 10, где происходила потеря производительности из-за перехода между GPU, CPU и GPU. Поэтому режим HC подходит для случаев, требующих специфических свойств нативных компонентов, но с большими потерями производительности.

Поддержка режима TLHC с версии 3.0

С версии 3.0 поддерживается режим TLHC, первоначально созданный для замены двух вышеописанных режимов, хотя они всё ещё существуют параллельно. В этом режиме компоненты располагаются на своих местах, но фактически используются прокси FrameLayout, который переопределяет метод onDraw и заменяет Canvas дочерних нативных компонентов для реализации смешивания рисования.

На данном рисунке TextView пуст, так как его Canvas был заменён на Canvas, созданный Flutter в оперативной памяти. На самом деле процесс TLHC очень схож с VD, поэтому можно сравнить реализацию VirtualDisplay и TextureLayer:

Из приведённой выше диаграммы можно сделать вывод:

  • Переход от VD к TLHC позволяет плавно переключаться между плагинами, так как основные изменения затрагивают логику извлечения и рендера текстур;
  • В Flutter ранее содержимое AndroidView, которое требовалось отрисовать, рисовалось в VirtualDisplays. Изображение в VirtualDisplay могло быть получено через его Surface. Теперь же содержимое AndroidView рисуется методом draw объекта View прямо в SurfaceTexture, а затем извлекается через TextureId.

Основной момент здесь заключается в вызове super.draw(surfaceCanvas): создание условий работы для Android View и использование замены Canvas для отрисовки нужного Surface.

Какие проблемы возникают при использовании TLHC? Этот подход использует замену Canvas для получения UI, что не поддерживает сценарии типа SurfaceView, поскольку они имеют свои независимые Surface и Canvas.

Поэтому текущее состояние поддержки PlatformView выглядит следующим образом:

  • По умолчанию используется режим TLHC, но если обнаруживается, что подключаемый View является SurfaceView, то система переходит на использование VD;
  • Поддержка использования HC может быть достигнута через интерфейс initExpensiveAndroidView.

Дополнительные ссылки:

Принцип работы PlatformView в Jetpack Compose заслуживает подробного рассмотрения, так как информации об этом пока немного.

Как известно, Jetpack Compose представляет собой новый фреймворк для создания пользовательского интерфейса на платформе Android, однако его дерево отрисовки UI не совместимо напрямую со «стандартными XML View». Compose представляет собой самостоятельную библиотеку UI, её модель UI больше напоминает Flutter, хотя функции @Composable не работают аналогично Flutter. При компиляции кода Compose добавляет параметр Composer для функций @Composable, а сама структура UI Node Tree создаётся уже внутри этого скрытого Composer.

Таким образом, если вам требуется внедрить нативный компонент в Jetpack Compose, вы будете использовать реализацию PlatformView. Суть PlatformView заключается в том, чтобы отрисовать «традиционные XML View» в дерево отрисовки Compose, а в случае с Compose на платформе Android используется AndroidView:

@Composable
fun CustomView() {
    var selectedItem by remember { mutableStateOf("Привет от View") }
}
```    // Добавляет View в Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Занимает максимальное пространство в дереве UI Compose
        factory = { context ->
            // Создает View
            TextView(context).apply {
                text = "Привет от View"
                textSize = 30f
                textAlignment = TextView.TEXT_ALIGNMENT_CENTER
            }
        },
        update = { view ->
            // View был создан или состояние было обновлено
            // Здесь можно добавить логику, если это необходимо
            
            // Так как selectedItem считывается здесь, AndroidView будет перерисован,
            // когда состояние изменится
            // Пример взаимодействия Compose -> View
            view.text = selectedItem
        }
    )
}Как показано выше, с помощью `AndroidView` мы можем добавить традиционный Android `TextView` в Compose. Конечно, это не имеет практического значения, но служит примером.

После отрисовки, мы видим, что в дереве компонентов Layout Inspector нет `TextView`, так как он просто отрисован в Compose, но **он фактически не является прямым компонентом LayoutNode в Compose, а лишь прикреплен к `AndroidView`**.

![](http://img.cdn.guoshuyu.cn/20250117_PlatformView/image8.png)

Чтобы понять принцип работы `AndroidView`, нам следует рассмотреть его реализацию через `factory`. Из исходного кода видно, что основной механизм  это создание представления `layoutNode` через `ViewFactoryHolder`, которое позволяет «входить» в дерево отрисовки Compose:

![](http://img.cdn.guoshuyu.cn/20250117_PlatformView/image9.png)

А реализация `ViewFactoryHolder` на платформе Android находится в базовом классе `AndroidViewHolder`, где можно заметить, что **`AndroidViewHolder` представляет собой реальный традиционный `ViewGroup`**:

![](http://img.cdn.guoshuyu.cn/20250117_PlatformView/image10.png)

Можно предположить, что наш традиционный `TextView`, расположенный внутри `AndroidView`, фактически добавлен в `AndroidViewHolder`, который является контейнером типа `ViewGroup`. Здесь также присутствует объект `Owner`, название которого указывает на его важность.

С этими двумя вопросами продолжим рассмотрение. Внутри `AndroidViewHolder` мы видим реализацию `layoutNode`, то есть этот HOLDER одновременно является традиционным `ViewGroup` и имеет реализацию `layoutNode` из Compose:![Иллюстрация](http://img.cdn.guoshuyu.cn/20250117_PlatformView/image11.png)

При рассмотрении реализации `layoutNode` можно отметить следующее:

- Когда `layoutNode` присоединяется к компоненту Compose с помощью метода `onAttach`, вызывается метод `addAndroidView`, который фактически представляет собой операцию `addView` контейнера `ViewGroup`.
- При отсоединении `layoutNode` с помощью метода `onDetach` вызывается метод `removeAndroidView`, который представляет собой операцию `removeViewInLayout` контейнера `ViewGroup`.
- Также используется `MeasurePolicy` для управления макетом, что позволяет синхронизировать состояние макета Compose с `AndroidViewHolder`, представляющим контейнер `ViewGroup`.

Таким образом, `AndroidViewHolder` выступает как "промежуточная станция", которая синхронизирует жизненный цикл и состояние макета Compose с традиционным контролем `ViewGroup`, тем самым создавая условия для макета и отрисовки для добавляемого `TextView`. Можно сделать вывод:

>`AndroidViewHolder` похож на агента Compose, который моделирует среду UI Compose в `ViewGroup`, контролируя отрисовку и макет `ViewGroup` для управления традиционными XML-контролями `View`.

Тогда `AndroidViewHolder` начинает работать в системе LayoutNode Compose с момента выполнения метода `onAttach`. Ключевой момент здесь заключается в том, что `AndroidComposeView` использует следующий метод:

```kotlin
(owner as? AndroidComposeView)?.addAndroidView(this, layoutNode)

Здесь появляется новый объект AndroidComposeView, который является вышеупомянутым owner. А что такое AndroidComposeView?Иллюстрация

Рассмотрев исходный код AndroidComposeView, можно сказать, что он также является ViewGroup, внутри которого находится AndroidViewsHandler, управляемый ViewGroup. Например, метод addAndroidView добавляет HOLDER в HANDLER. До этого момента мы имеем три компонента:

  • AndroidComposeView
  • AndroidViewsHandler
  • AndroidViewHolder

Все они являются реализацией традиционного ViewGroup, и их взаимосвязь может быть представлена следующим образом:

Теперь процесс становится более понятным. Нам нужно выяснить, что такое AndroidComposeView, откуда он берется, а затем продолжать движение, чтобы лучше понять его реализацию.

Мы начинаем с внутреннего root-узла в AndroidComposeView. Это позволяет нам предположить, что это верхний уровень узла, поэтому мы начнем поиск с вершины:

Мы начинаем с активности и спускаемся ниже, после нескольких простых изменений, мы можем найти создание AndroidComposeView в методе AbstractComposeView.setContent:

Поскольку реализация AbstractComposeView является ComposeView, можно заметить:

AndroidComposeView создается ComposeView при инициализации и добавляется через addView, а корневой узел UiApplier в Composition — это AndroidComposeView.Поэтому именно поэтому ранее наш owner был источником AndroidComposeView, а затем следует AndroidViewsHandler, который управляет всеми HOLDER'ами и выполняет различные операции построения и рисования для детей, переданных ему:

Таким образом, мы знаем следующее:

  • При инициализации Compose создаёт верхний уровень узла ViewGroupAndroidComposeView, который является корневым LayoutNode.
  • Внутри AndroidComposeView находится AndroidViewsHandler, который использует HashMap для управления и запуска построения и перерисовки дочерних HOLDER'ов.
  • AndroidViewHolder представляет собой прокси LayoutNode, который синхронизирует жизненный цикл и состояние макета Compose UI со старыми ViewGroup-компонентами.

Структура будет примерно такой, но хотя он добавляется в ViewGroup через addView, он не рендерится непосредственно внутри ViewGroup, а вместо этого "делегируется" рендерингу в области Scope, соответствующей LayoutNode:

Например, мы подключили два SurfaceView в Compose. Если мы выведем структуру традиционной разметки, то можем получить примерно такой результат:

В этом примере SurfaceView будет рассмотрен подробнее позднее.

Заключительный этап — отрисовка. После того как мы узнали процесс, можно обратиться к реализации layoutNode внутри AndroidViewHolder. Здесь используется Canvas, полученный из drawBehind:

Обычно, декоратор drawBehind позволяет рисовать контент после любого составного функционала, например:```kotlin Text( "Здравствуйте, Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )


![](http://img.cdn.guoshuyu.cn/20250117_PlatformView/image22.png)

А здесь `Canvas` берётся из `DrawScope`. `DrawScope` представляет собой высокоуровневую обёртку над интерфейсом `Canvas`, а внутренний `Canvas` использует нативную платформу `Canvas`. Поскольку Compose поддерживает многоплатформенные приложения, то на платформе Android это объект `AndroidCanvas`. Этот объект доступен через `canvas.nativeCanvas`, что эквивалентно `android.graphics.Canvas`.

Процесс представлен на следующем схематическом рисунке. Основная идея заключается в том, чтобы передать `Canvas` из `drawBehind` в «традиционный XML View», таким образом, при отрисовке используется цепочка `Canvas` из системы Compose:

![](http://img.cdn.guoshuyu.cn/20250117_PlatformView/image23.png)

Итак, видно, что при отрисовке используется именно замена `Canvas` традиционного `View` родителем `ViewGroup` типа `AndroidViewHolder`. Это значит, что содержание `View` рисуется через `Canvas` Compose на его `LayoutNode`.

Кроме того, `pointerInteropFilter` также обрабатывает события жестов. Жесты пользователя на текущем `LayoutNode` отправляются в `AndroidViewHolder` типа `ViewGroup`, что приводит к активации событий кликов и других эффектов традиционных компонентов Android.При переходах между экранами (`navigate`) объект `AndroidViewHolder` соответственно добавляется или удаляется. > Из этого ракурса **реализация PlatformView в Compose и концепция TextureLayer в Flutter очень схожи — они оба используют замену Canvas и имитацию среды размещения для внедрения View, но при этом существуют важные различия**, которые проявляются в `SurfaceView`.Поскольку `SurfaceView` имеет свой независимый Surface и Canvas, он не может быть "заменён" родительским Canvas, что является проблемой в Flutter с TLHC, но в Compose вы заметите, что `SurfaceView` работает нормально внутри `AndroidView`:

```kotlin

@Composable
fun ContentExample() {
    Box() {
        ComposableSurfaceView(Modifier.size(100.dp))
        Text("Compose", modifier = Modifier
            .drawBehind {
                drawRoundRect(
                    color = Color(0x9000FFFF), cornerRadius = CornerRadius(10.dp.toPx())
                )
            }
            .padding(vertical = 30.dp))
    }
}

@Composable
fun ComposableSurfaceView(modifier: Modifier = Modifier) {
    AndroidView(factory = { context ->
        SurfaceView(context).apply {
            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
            )
            holder.addCallback(MySurfaceCallback()) // добавление обратного вызова
        }

    }, modifier = modifier)
}

class MySurfaceCallback : SurfaceHolder.Callback {
    private var _canvas: Canvas? = null
    override fun surfaceCreated(holder: SurfaceHolder) {
        _canvas = holder.lockCanvas()
        _canvas?.drawColor(android.graphics.Color.GRAY) // установка цвета фона
        _canvas?.drawCircle(100f, 100f, 50f, Paint().apply {
            color = android.graphics.Color.YELLOW
        }) // отрисовка желтого круга
        holder.unlockCanvasAndPost(_canvas)
    }
}

Как видно из приведенного выше кода, фоновый серый цвет и желтый круг в SurfaceView отображаются корректно, а также текст "Compose" с фоновым цветом правильно отображается поверх SurfaceView:

Не кажется ли вам странным, почему Canvas в SurfaceView не был заменен, но содержимое и уровень SurfaceView всё ещё корректно отображаются в дереве UI Compose?На самом деле причина проста, хотя Compose и «традиционные XML Views» представляют собой две различные системы UI, суть Compose заключается в том, что это всё ещё View в рамках Android, то есть он всё ещё находится в рамках системы View:> Основываясь на системе отображения Android с использованием Surface, Window и SurfaceFlinger.

Давайте вспомним, как работает SurfaceView.

  • В Android практически все контролы основаны на классе View. Все видимые объекты типа View рендерятся на поверхность (Surface), которая предоставляется SurfaceFlinger, то есть это текущее окно (Window).

  • Несмотря на то что SurfaceView наследуется от View, он имеет свою независимую поверхность, которую прямым образом отправляет в SurfaceFlinger.

Это объясняет, почему SurfaceView имеет свой собственный Canvas. Проще говоря, это вид, который может рисовать на поверхности и выводить её напрямую через SurfaceFlinger.

Обычно SurfaceView всегда представляет собой прозрачный прямоугольник (Rect) на уровне окна, словно "дырка" в окне, и по умолчанию его порядок Z всегда ниже, чем у связанного оконного уровня, то есть поверхность SurfaceView находится под основной поверхностью.

При окончательном рендере SurfaceFlinger соединяет слои изображения SurfaceView и окна вместе.

Перейдём к Compose. Базовый уровень Compose всё ещё использует традиционный View, поэтому он также зависит от поверхности View и SurfaceFlinger, то есть:> Compose и «традиционный View» используют одно и то же окно (Window) и DecorView. AndroidView выступает в роли моста между «традиционным View» и деревом компонентов Compose. Хотя содержимое SurfaceView рисуется независимо, но на экране они совместно используют одно окно (Window). SurfaceFlinger всё равно управляет всеми окнами централизованно.

Добавление метода setZOrderOnTop(true) к вышеупомянутому коду SurfaceView приведёт к тому, что текст Compose станет невидимым, поскольку меняется порядок Z:

Это и есть главная разница между Compose и Flutter при работе с AndroidView:

Flutter полностью отсоединён от системы рендера, тогда как Compose остаётся внутри системы View. Поэтому использование SurfaceView не является проблемой, а даже официально представлено специальное обёртывание для него — AndroidExternalSurfaceScope.

Однако стоит отметить, что в системе «традиционных XML Views» каждый View имеет свой RenderNode, тогда как в Compose обычно используется всего один RenderNode для всех компонентов страницы, то есть так называемый однопоточный режим состояния. Внутренний механизм Compose заключается в том, чтобы использовать Composer для сборки LayoutNodes и последующего помещения их в RenderNode.

ПоследнееКак можно заметить, на платформе Android идеи реализации в Flutter и Compose очень схожи. В обоих случаях используется AndroidView, идея заключается в моделировании окружения и замене Canvas. Однако, благодаря преимуществам native View системы, Compose лучше поддерживает SurfaceView.---

Исправлено согласно правилам перевода:

  • Оставлены без изменения имена переменных (AndroidView) и команд CLI.
  • Сохранено исходное форматирование и разметка (Markdown).
  • Применена профессиональная IT-терминология.
  • Удалены кавычки вокруг ключевых слов, чтобы сохранить единый стиль документации.

Также учтены особенности перевода, такие как использование составных терминов и сохранение математических выражений и числовых значений.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master