Как девятая статья серии, эта статья подробно рассматривает принципы отрисовки в виджете Widget, исследует, как объекты RenderObject в Flutter проходят через последний этап до экрана, а также объясняет, как создать пользовательскую отрисовку в Flutter с помощью примера.
В шестой и седьмой статьях мы узнали о взаимоотношении между Widget
, Element
и RenderObject
, а также о логике размещения Widget
. В конечном итоге все Widget
преобразуются в объекты RenderObject
, которые накладываются друг на друга, чтобы создать желаемое изображение.
Поэтому в Flutter окончательное размещение (layout) и отрисовка (paint) происходят в подклассах RenderObject
, связанных с Widget
. Один из ключевых моментов RenderObject
— это то, что он является одной из основных особенностей Flutter, делающей его кросс-платформенным: все компоненты являются платформенно-независимыми, другими словами, Flutter требует лишь предоставляемого системой «Canvas», после чего разработчики могут использовать Widget
для создания RenderObject
, который затем прямым образом отрисовывается на экран через движок.
Примечание: с этого момента статья становится более объемной, поэтому может потребовать некоторого терпения со стороны читателя.## 1. Процесс отрисовки
Мы знаем, что все Widget
в конце концов преобразуются в RenderObject
, поэтому для понимания процесса отрисовки нам следует обратиться к методу paint
объекта RenderObject
.
На следующем рисунке показано, что все подклассы RenderObject
должны реализовать метод paint
, при этом этот метод не предназначен для прямого вызова пользователями; для обновления отрисовки можно использовать метод markNeedsPaint
, чтобы запустить перерисовку интерфейса.
Тогда, согласно общепринятому порядку, после вычислений размеров и позиций, метод paint
будет вызван, принимая два параметра: PaintingContext
и Offset
. Эти параметры являются ключевыми для завершения отрисовки, и здесь возникают вопросы:
PaintingContext
?Offset
?Быстрый просмотр исходного кода позволяет узнать, что:
PaintingContext
является то, что это место для отрисовки, и он содержит Canvas
в родительском классе ClipContext
. Конструктор PaintingContext
помечен как @protected
, и он автоматически создается только в методах repaintCompositedChild
и pushLayer
.Offset
в paint
主要用于提供当前控件在屏幕上的相对偏移值,在绘制时确定绘制坐标的参考点。Хорошо, продолжим. Так как PaintingContext
называется контекстом (context
), то он, конечно же, имеет отношение к контексту. Но где именно он создается?По отладке исходного кода известно, что проект запускается через WidgetsFlutterBinding
, когда вызывается метод runApp
. В предыдущих разделах мы уже знали, что WidgetsFlutterBinding
является "клеящим" классом, который активирует RendererBinding
с помощью mixin, тем самым создает корневой узел PaintingContext
.
Теперь вопрос: а Offset
? Как показано на следующем рисунке, передача Offset
осуществляется путем суммирования offset
родительского и дочернего компонентов, после чего координаты для рисования передаются уровнем ниже.
Проще говоря, через PaintingContext
и Offset
мы можем точно определить место на экране для рисования нужной графики после выполнения размещения.
Давайте начнем с интересного теста.
На нашем экране ограничим высоту зеленого контейнера Container
значением 60 пикселей. В данный момент игнорируем контроллер Slider
внутри контейнера. Мы рисуем красный квадрат размером 100x100 пикселей, но видим, что результат выглядит так: "Нань?" Почему такой маленький?
Факт заключается в том, что при обычном рисовании Container
в Flutter, AppBar
уже рассчитывает высоту состояния и заголовка, поэтому при использовании Canvas
и прямого рисования drawRect
, красный квадрат начинает отрисовываться с точки (0, 0)
, которая фактически находится в области состояния.
А если мы будем менять положение? Например, если установим вершину квадрата на уровень 300, то получим следующий результат: "Юань?" Красный квадрат выходит за границы контейнера.
Здесь проблема связана с параметром estimatedBounds
в PaintingContext
. Обычно этот параметр инициализируется значением child.paintBounds
при создании, однако есть описание этого параметра: Оказывается, можно рисовать за пределами контейнера.
Канвас позволяет рисовать за пределами этих границ.
Прямоугольник [estimatedBounds] находится в системе координат [canvas].
До этого момента можно сделать простое заключение: для Flutter весь экран является одним большим холстом, мы используем различные Offset
и Rect
, чтобы определить положение, а затем с помощью PaintingContext
и Canvas
рисуем на этом холсте целый экран. Каждое изменение приводит к полной перерисовке всего экрана.
Конечно, полная перерисовка не происходит каждый раз, здесь есть некоторые правила.
Помните метод markNeedsPaint
? Начнем с него и выведем основной процесс, как показано ниже. Можно видеть, что markNeedsPaint
действительно вызывает обновление отрисовки при выполнении requestVisualUpdate
.
Перейдем к исходному коду. Как видно из кода, когда вызывается
markNeedsPaint()
, RenderObject
начинает поиск среди родителей, определяя, начинать ли отсюда обновление отрисовки на основе значения isRepaintBoundary
. Другими словами, это определение областей для обновления.
Поэтому фактический процесс следующий: через isRepaintBoundary
определяется область для обновления, а метод requestVisualUpdate
запускает обновление и передает его для отрисовки вниз по дереву компонентов.
Из исходного кода также следует, что isRepaintBoundary
имеет только метод get
, поэтому он может быть переопределен только дочерними классами, указывающими, являются ли они границами для перерисовки. Например, isRepaintBoundary
для RenderProxyBox
, RenderView
и RenderFlow
всегда истинно.
Если область часто перерисовывается и её изменения не влияют на родительский компонент, то можно переопределить isRepaintBoundary
как true.
Как известно из вышеописанного, если isRepaintBoundary
истинно, то эта область становится областью для обновления отрисовки, и при создании этой области создается новый Layer
.
Разные Layer
могут работать независимо друг от друга, например, OffsetLayer
используется в RenderObject
для позиционирования и отрисовки.
Это также приводит к выводу: не все
RenderObject
имеют Layer
, так как это зависит от значения isRepaintBoundary
.
```Далее в RenderObject
есть ещё одно свойство — `needsCompositing`, которое влияет на количество создаваемых слоёв `Layer`, а эти `Layer` составляют дерево Layer Tree. Ну что ж, теперь у нас есть ещё одно дерево, но это именно то дерево, которое используется для отрисовки движком.
На этом мы более-менее понимаем весь процесс отрисовки RenderObject
. И этот момент отрисовки мы активируем, а не вызываем его явно, и обновление происходит при проверке области. Да, немного похоже на React!
После того как мы рассмотрели все процессы отрисовки, давайте взглянем на реализацию отрисовки компонента Slider
через его исходный код.
Структура всего компонента Slider
очень типична для Flutter. Основная структура представлена на следующей схеме.
В _RenderSlider
помимо жестов и анимаций, каждый элемент отрисовки является независимым component, который выполняет свою работу. Все эти components предоставляются через SliderTheme
и SliderThemeData
.
```Кстати, сам SliderTheme
является `InheritedWidget`. Те, кто читали предыдущие статьи, должны знать, что `InheritedWidget` обычно используют для передачи состояния между виджетами. Поэтому если вы хотите настроить `Slider`, вы можете использовать `SliderTheme` для вложения и затем настраивать нужные вам параметры через `SliderThemeData`.
Кроме того, в _RenderSlider
регистрируются жесты и анимации, которые активируют метод markNeedsPaint
при получении события, что объясняет реакцию вашего прикосновения на экране.
Также можно заметить, что все параметры внутри _SliderRender
переопределяют методы get
и set
. При установке значения также вызывается markNeedsPaint()
или _updateLabelPainter
для косвенной активации markNeedsLayout
.
Что касается различных фигур в Slider
, их отрисовка осуществляется стандартными методами Canvas
: saveLayer
, drawRect
, translate
, drawPath
и так далее.
Так заканчивается девятая глава! (///▽///)
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )