Это первая из серии статей, которые я планирую написать о том, как Flutter отображает наши приложения и как мы можем использовать возможности Flutter для визуализации виджетов. Эти статьи служат для:

  • Помогите мне задокументировать и объединить все, что я узнал по теме, и
  • Представьте эту тему другим в постепенном доступном формате.

Несмотря на то, что не обязательно знать о процессе рендеринга Flutter, чтобы писать 99% приложений, все же может быть полезно понять его и уметь рассуждать об этом; определенно будет чрезвычайно полезно знать, что происходит за кулисами. Кроме того, могут быть случаи, когда вам просто нужно будет засучить рукава и отрендерить этот виджет самостоятельно! И, что наиболее важно, изучение рендеринга - это очень весело!

И последнее замечание перед тем, как я начну: эта серия статей требует знания Flutter от среднего до продвинутого. Предполагается, что вы хорошо разбираетесь в Flutter, и не пытается заново объяснить основные концепции.

Часть 1

Эта часть будет наполовину теоретической, наполовину практической. Я начну с объяснения теории, достаточной для того, чтобы мы начали работать, а затем перейду к разговору о том, как мы можем сами рендерить наши собственные виджеты (как круто!). Однако в более поздних частях я вернусь к теории и углублюсь в ее детали.

Так как же Flutter отображает наши приложения?

На очень высоком уровне рендеринг во Flutter проходит четыре фазы:

  1. Этап макета: на этом этапе Flutter точно определяет, насколько велик каждый объект и где он будет отображаться на экране.
  2. Фаза рисования: на этом этапе Flutter предоставляет каждому виджету холст и приказывает ему рисовать на нем себя.
  3. Фаза компоновки: на этом этапе Flutter собирает все вместе в сцену и отправляет ее на графический процессор для обработки.
  4. Фаза растеризации: на этом заключительном этапе сцена отображается на экране в виде матрицы пикселей.

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

и все начинается с выкладки ..

Во Flutter этап макета состоит из двух линейных проходов: перехода ограничений вниз по дереву и перехода сведения о макете вверх по дереву.

Процесс прост:

  1. родитель передает определенные ограничения каждому из своих дочерних элементов. Эти ограничения представляют собой набор правил, которые ребенок должен соблюдать, раскладывая себя. Это как если бы родитель говорит ребенку: «Хорошо, делай, что хочешь, при условии, что ты соблюдаешь эти ограничения». Один простой пример ограничений - ограничение максимальной ширины; родитель может передать своему дочернему элементу максимальную ширину, в пределах которой ему разрешено отображать. Когда ребенок получает эти ограничения, он знает, что не следует пытаться визуализировать что-либо шире, чем они.
  2. Затем дочерний элемент генерирует новые ограничения и передает их своим собственным дочерним элементам, и это продолжается до тех пор, пока мы не дойдем до листового виджета без дочерних элементов.
  3. Затем этот виджет определяет свои детали макета на основе переданных ему ограничений. Например, если его родительский элемент передал ему ограничение максимальной ширины в 500 пикселей. Он мог сказать: «Я использую все это!» Или «Я буду использовать только 100 пикселей». Таким образом, он определяет детали, необходимые для его макета, и передает их обратно своему родительскому объекту.
  4. Родитель, в свою очередь, делает то же самое. Он использует детали, передаваемые от его дочерних элементов, чтобы определить, какими будут его собственные детали, а затем передает их вверх по дереву, и мы продолжаем подниматься по дереву либо до корня, либо до тех пор, пока не будут достигнуты определенные пределы.

Но о каких «ограничениях» и «деталях компоновки» мы говорим? Это зависит от используемого протокола макета. Во Flutter существует два основных протокола компоновки: протокол коробки и протокол ленты. Протокол бокса используется для отображения объектов в простой двумерной декартовой системе координат, а протокол ленты используется для отображения объектов, которые реагируют на прокрутку.

В блочном протоколе ограничения, которые родитель передает своим потомкам, называются BoxConstraints. Эти ограничения определяют максимальную и минимальную ширину и высоту, которые разрешено иметь каждому дочернему элементу. Например, родительский элемент может передать своему дочернему элементу следующие BoxConstraints:

Это означает, что ребенок может иметь любой размер в зеленых пределах. То есть любая ширина от 150 до 300 и любая высота больше 100 (поскольку maxHeight равно бесконечности). Таким образом, дочерний элемент решает, какой размер он хочет иметь в рамках этих ограничений, и информирует своего родителя о своем решении. Таким образом, детали макета - это размер, который ребенок выбирает.

В протоколе ленты все немного сложнее. Родитель передает своему дочернему элементу SliverConstraints, содержащий информацию о прокрутке и ограничения, такие как смещение прокрутки, перекрытие и т. Д. Потомок, в свою очередь, отправляет обратно своему родительскому элементу SliverGeometry . Мы рассмотрим протокол ленты более подробно в более поздней части.

Потом рисуем ..

Как только родитель узнает все детали макета своих дочерних элементов, он может приступить к рисованию как себя, так и своих дочерних элементов. Для этого Flutter передает ему PaintingContext, содержащий Canvas, на котором он может рисовать. Контекст рисования также позволяет ему рисовать своих дочерних элементов и создавать новые слои рисования для случаев, когда нам нужно рисовать предметы друг на друге.

Затем мы выполняем композитинг и растеризацию.

который мы пока пропустим, чтобы рассмотреть более конкретные детали процесса рендеринга.

Итак, давайте посмотрим на ...

Дерево рендеринга

Вы знаете о дереве виджетов и о том, как ваши виджеты составляют дерево, корень которого начинается с виджета «Приложение», а затем распадается на ветви и ветви по мере добавления виджетов в приложение. Однако вы можете не знать, что есть другое дерево, соответствующее вашему дереву виджетов, которое называется деревом рендеринга ..

Видите ли, вы познакомились с несколькими видами виджетов; StatefulWidget, StatelessWidget, InheritedWidget и т. д. Но есть также другой вид виджета, называемый RenderObjectWidget. У этого виджета нет метода build, а скорее метода createRenderObject, который позволяет ему создать RenderObject и добавить его в дерево рендеринга.

RenderObject - самый важный компонент процесса визуализации. Для дерева рендеринга это то же самое, что виджет для дерева виджетов. Все в дереве рендеринга является RenderObject. И каждый RenderObject имеет множество свойств и методов, используемых для выполнения рендеринга. Она имеет:

  • объект constraints, который представляет ограничения, переданные ему от его родителя
  • объект parentData, который позволяет своему родителю прикреплять к нему полезную информацию.
  • метод performLayout, в котором мы можем выложить его и его дочерние элементы.
  • метод paint, с помощью которого мы можем раскрашивать его и раскрашивать его дочерние элементы.
  • и т.п.

Однако RenderObject - это абстрактный класс. Его необходимо расширить для выполнения любого фактического рендеринга. И два наиболее важных класса, расширяющих RenderOject, - это RenderBox и, как вы уже догадались, RenderSliver. Эти два класса являются родителями всех объектов рендеринга, которые реализуют протокол коробки и протокол ленты соответственно. Эти два класса также расширяются десятками и десятками других классов, которые обрабатывают определенные сценарии и реализуют детали процесса рендеринга.

Чтобы завершить эту первую часть руководства, давайте попробуем создать наш собственный RenderObjectWidget.

Скупой виджет

Мы создадим виджет, который назовем Stingy. Этот виджет скуп, потому что какие бы ограничения он ни получил от своего родителя, он дает только минимум своему дочернему элементу. Кроме того, он занимает всю предоставленную ему ширину и высоту и размещает своего дочернего элемента в правом нижнем углу! Какой бедный ребенок!

Начнем с создания простого приложения:

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: ConstrainedBox(
          constraints: BoxConstraints(
            maxWidth: double.infinity,
            minWidth: 100.0,
            maxHeight: 500.0,
            minHeight: 100.0
          ),
          child: Stingy(
            child: Container(
              color: Colors.red,
            )
          ),
        ),
      ),
    )
  );
}
  • Мы создали приложение, содержащее ConstrainedBox, внутри которого разместили наш виджет Stingy. ConstrainedBox - это виджет, который позволяет нам явно указать, какие ограничения мы хотим передать дочернему элементу. Таким образом, мы предоставили нашему виджету Stingy ширину от 100,0 до бесконечности и высоту от 100,0 до 500,0.
    Примечание. Необязательно использовать ConstrainedBox, но мы использовали его в этом примере, чтобы иметь возможность явно указать, какие ограничения мы передадим нашему скупому виджет, чтобы мы могли легко их увидеть и рассуждать о них.
  • Мы предоставили нашему виджету Stingy дочерний элемент, который представляет собой просто красный контейнер.

А теперь посмотрим на Стинги!

class Stingy extends SingleChildRenderObjectWidget {

  Stingy({Widget child}): super(child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderStingy();
  }
}
  • Наш виджет расширяет SingleChildRenderObjectWidget, который представляет собой RenderObjectWidget, который, как следует из названия, принимает единственный дочерний элемент.
  • Наша функция createRenderObject, которая создает и возвращает RenderObject, создает экземпляр класса, который мы назвали RenderStingy.

Класс RenderStingy выглядит следующим образом:

class RenderStingy extends RenderShiftedBox {

  RenderStingy(): super(null);

  @override
  void performLayout() {
    child.layout(BoxConstraints(
      minHeight: 0.0,
      maxHeight: constraints.minHeight,
      minWidth: 0.0,
      maxWidth: constraints.minWidth
    ), parentUsesSize: true);


    final BoxParentData childParentData = child.parentData;
    childParentData.offset 
      = Offset(this.constraints.maxWidth - child.size.width,
        this.constraints.maxHeight - child.size.height);

    size = Size(this.constraints.maxWidth, constraints.maxHeight);
  }
}
  • RenderStingy расширяет RenderShiftedBox, который является одним из многих вспомогательных классов, расширяющих RenderBox, чтобы упростить реализацию блочного протокола. RenderShiftedBox реализует все детали протокола и оставляет вам реализацию функции performLayout. В этой функции вы должны расположить своего дочернего элемента и выбрать смещение, при котором вы хотите его отображать.
  • Чтобы разместить дочерний элемент, вы просто вызываете child.layout и задаете ему необходимые ограничения. Как описано ранее, Stingy - скупой виджет! Таким образом, он предоставляет своему потомку ограничения в диапазоне от 0 до минимальных ограничений.
  • Мы предоставляем child.layout еще один параметр, parentUsesSize. Если установлено значение false, это означает, что родителю все равно, какой размер выбирает дочерний элемент, что очень полезно для оптимизации процесса макета; если дочерний элемент изменит свой размер, родителю не придется менять макет! Однако в нашем случае мы хотим поместить дочерний элемент в правый нижний угол, что означает, что мы действительно заботимся о том, какой размер он выбирает, что заставляет нас установить для parentUsesSize значение true.
  • К моменту завершения child.layout дочерний элемент уже определил детали своего макета, которые, поскольку мы следуем протоколу бокса, имеют желаемый размер. Затем мы можем приступить к размещению его на желаемом смещении, то есть в правом нижнем углу.
  • Наконец, точно так же, как наш ребенок выбрал размер на основе ограничений, которые мы ему дали, нам также нужно выбрать размер для Stingy, чтобы его родитель знал, как его разместить. Итак, Стинги просто говорит: «Дай мне все, что у тебя есть!» и занимает максимальную предоставленную ему ширину и высоту.

И теперь, если вы запустите код, вы можете увидеть, как StingyWidget использовал всю высоту и ширину, разрешенные ему его родителем, а затем поместил свой дочерний элемент, красный прямоугольник размером 100x100 в нижний правый угол:

Обратите внимание, что Контейнер - это виджет со сложным поведением. В нашем случае он бездетный, поэтому он решил развернуться, чтобы максимально заполнить допустимые ограничения.

В общем, каждый виджет будет обрабатывать предоставленные ему ограничения по-разному. Например, если мы заменим этот контейнер на RaisedButton:

Stingy(
  child: RaisedButton(
    child: Text('Hello there..'),
    onPressed: () {
      print('It\'s been so long..');
    },
  )
)

мы видим это:

RaisedButton использует всю предоставленную ему ширину, но ограничивает высоту до 48,0.

Это знаменует конец первой части этого урока. Надеюсь, это было полезно!

Пока я не напишу следующую часть, почему бы вам не попробовать поиграть со StingyWidget и его дочерними элементами! Кроме того, попробуйте взглянуть на код различных виджетов, которые предоставляет Flutter, вы увидите, что некоторые из них являются виджетами без сохранения состояния и с отслеживанием состояния, а другие - с виджетами RenderObjectWidgets. Вы также увидите, что есть еще много чего узнать о рендеринге во Flutter! Так что до встречи в следующей части!

ОБНОВЛЕНИЕ: К сожалению, из-за смены карьеры я больше не работаю с Flutter, и у меня нет времени, чтобы завершить другие части этого руководство. Если кто-нибудь напишет к нему хорошее продолжение, дайте мне знать, и я буду более чем счастлив предоставить ссылку на его работу.

Использованная литература: