После публикации статьи «Глубинное понимание реализации алгоритма diff в Flutter и Compose при отрисовке UI» мне поступили запросы от экспертов, чтобы узнать больше о реализации PlatformView
в Flutter и Compose. Учитывая мой опыт работы с PlatformView
в Flutter, теперь я могу провести простое сравнение:
Flutter использует различные методы для интеграции платформенных компонентов в Android. Как независимый набор UI-библиотек, Flutter предлагает несколько способов использования PlatformView
, что делает его подходящим для различных сценариев.
На данный момент существуют три основных метода поддержки PlatformView
в Android:
VD представляет собой использование виртуального отображения для рендера нативных компонентов в памяти. Затем этот компонент занимает место на экране Flutter через ID, а затем связывается с текстурой Flutter для отображения.Проблема заключается в том, что компоненты фактически не существуют на месте отображения, можно сказать, что это просто "зеркальное" отображение UI в памяти или "зеркало второго экрана". Поэтому любые действия с этими компонентами требуют двукратной передачи данных между Flutter и нативной средой.Кроме того, поскольку компоненты рендерятся в памяти, взаимодействие с клавиатурой требует двухуровневого прокси-сервера, что может вызывать проблемы с вводом данных и взаимодействием, особенно в случае WebView
.
Конечно, современная версия VD значительно лучше первоначальной и продолжает использоваться для совместимости.
С версии 1.2 поддерживается режим HC, в котором нативные компоненты просто «накладываются» поверх FlutterView, то есть нативные компоненты добавляются в FlutterView через метод addView
. В случае необходимости отображения Flutter-компонентов поверх нативных используется новый компонент FlutterImageView
, который служит для создания нового слоя.
Например, в инспекторе макета можно видеть границы различных нативных макетов:
На следующем рисунке показано, как два синих TextView
добавлены поверх FlutterView
, а красный текст RE
скрыт за ними. Верхний красный текст также является Flutter-компонентом, но поскольку он должен отображаться поверх TextView
, используется ещё один FlutterImageView
, чтобы обеспечить смешивание Flutter-компонентов и нативных компонентов.
Здесь
FlutterImageView
также решает проблему синхронизации анимации и рендера.Однако такой подход привёл к проблеме синхронизации потоков, так как нативные компоненты рендерятся непосредственно в платформенном потоке, что вызывает проблемы при работе в UI-потоке Flutter. Это привело к появлению багов со сбоем экрана в некоторых случаях.
Хотя эта проблема была решена путём синхронизации потоков, это привело к некоторым потерям производительности, особенно до Android 10, где происходила потеря производительности из-за перехода между GPU, CPU и GPU. Поэтому режим HC подходит для случаев, требующих специфических свойств нативных компонентов, но с большими потерями производительности.
С версии 3.0 поддерживается режим TLHC, первоначально созданный для замены двух вышеописанных режимов, хотя они всё ещё существуют параллельно. В этом режиме компоненты располагаются на своих местах, но фактически используются прокси FrameLayout
, который переопределяет метод onDraw
и заменяет Canvas
дочерних нативных компонентов для реализации смешивания рисования.
На данном рисунке
TextView
пуст, так как егоCanvas
был заменён наCanvas
, созданный Flutter в оперативной памяти. На самом деле процесс TLHC очень схож с VD, поэтому можно сравнить реализацию VirtualDisplay и TextureLayer:
Из приведённой выше диаграммы можно сделать вывод:
AndroidView
, которое требовалось отрисовать, рисовалось в VirtualDisplays
. Изображение в VirtualDisplay
могло быть получено через его Surface
. Теперь же содержимое AndroidView
рисуется методом draw
объекта View
прямо в SurfaceTexture
, а затем извлекается через TextureId
.Основной момент здесь заключается в вызове super.draw(surfaceCanvas)
: создание условий работы для Android View и использование замены Canvas для отрисовки нужного Surface.
Какие проблемы возникают при использовании TLHC? Этот подход использует замену Canvas для получения UI, что не поддерживает сценарии типа SurfaceView
, поскольку они имеют свои независимые Surface и Canvas.
Поэтому текущее состояние поддержки PlatformView
выглядит следующим образом:
SurfaceView
, то система переходит на использование VD;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`**.

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

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

Можно предположить, что наш традиционный `TextView`, расположенный внутри `AndroidView`, фактически добавлен в `AndroidViewHolder`, который является контейнером типа `ViewGroup`. Здесь также присутствует объект `Owner`, название которого указывает на его важность.
С этими двумя вопросами продолжим рассмотрение. Внутри `AndroidViewHolder` мы видим реализацию `layoutNode`, то есть этот HOLDER одновременно является традиционным `ViewGroup` и имеет реализацию `layoutNode` из Compose:
При рассмотрении реализации `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'ами и выполняет различные операции построения и рисования для детей, переданных ему:
Таким образом, мы знаем следующее:
ViewGroup
— AndroidComposeView
, который является корневым 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)
)

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

Итак, видно, что при отрисовке используется именно замена `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
.
AndroidView
, идея заключается в моделировании окружения и замене Canvas. Однако, благодаря преимуществам native View системы, Compose лучше поддерживает SurfaceView
.---Исправлено согласно правилам перевода:
AndroidView
) и команд CLI.Также учтены особенности перевода, такие как использование составных терминов и сохранение математических выражений и числовых значений.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )