Этот пост раскроет принципы создания пользовательских макетов в Flutter и позволит вам углубиться в практическое применение процесса создания пользовательских макетов с использованием двух методов реализации. В результате вы получите следующий интерфейс:
В предыдущих разделах мы уже рассматривали взаимоотношения между Widget
, Element
и RenderObject
. Так что же такое пользовательское расположение? Это фактически создание пользовательского расположения child
внутри RenderObject
, а это отличается от других фреймворков тем, что в Flutter основой layout'а не является вложенное расположение, а основой Flutter является Canvas
. Мы используем Widget
, чтобы упрощать работу с RenderObject
.
Как было показано в тестировании рисования в разделе ["Девять. Глубокое понимание принципов рисования"], для Flutter весь экран представляет собой холст, где мы определяем положение через различные
Offset
иRect
, а затем рисуем UI с помощьюCanvas
. Полностью все области экрана являются целями для рисования. Если вchild
мы игнорируем правила, то можем рисовать независимо от размера и положения родителя.## 2. MultiChildRenderObjectWidget
Поняв базовые концепции, мы знаем, что ключ к созданию пользовательского расположения Widget
заключается в создании пользовательского RenderObject
. Большинство встроенных компонентов расположения в официальной библиотеке основаны на наследовании MultiChildRenderObjectWidget
. Тогда, какие шаги нам следует предпринять при создании пользовательского расположения?
Как правило, для создания пользовательского расположения мы наследуемся от двух абстрактных классов — MultiChildRenderObjectWidget
и RenderBox
. Класс MultiChildRenderObjectElement
связывает эти два класса. Кроме того, существуют несколько ключевых классов: ContainerRenderObjectMixin
, RenderBoxContainerDefaultsMixin
и ContainerBoxParentData
.
RenderBox
является подклассом RenderObject
и часто используется при создании пользовательского RenderObject
. А что представляют собой остальные классы?
Как следует из названия, это миксин-класс ContainerRenderObjectMixin
, который в основном предназначен для поддержки двусвязного списка детей RenderObject
.
Добавление ContainerRenderObjectMixin
в RenderBox
позволяет получить двусвязный список детей, что удобно при создании макета, так как позволяет получать и управлять RenderObject
в прямом и обратном направлении.
RenderBoxContainerDefaultsMixin
представляет собой расширение ContainerRenderObjectMixin
, предоставляющее общие по умолчанию поведение и управление для детей внутри ContainerRenderObjectMixin
. Интерфейс представлен ниже:```/// Вычисляет и возвращает базовую линию первого ребенка, часто используется при необходимости учета порядка детей
double defaultComputeDistanceToFirstActualBaseline(TextBaseline baseline)
/// Вычисляет и возвращает минимальную базовую линию среди всех детей, обычно используется при отсутствии зависимости от порядка детей
double defaultComputeDistanceToHighestActualBaseline(TextBaseline baseline)
/// Обрабатывает столкновения при тестировании
bool defaultHitTestChildren(BoxHitTestResult result, {Offset position})
/// По умолчанию рисует
void defaultPaint(PaintingContext context, Offset offset)
/// Возвращает список детей в виде массива
List<ChildType> getChildrenAsList()
#### 3\. ContainerBoxParentData
`ContainerBoxParentData` является подклассом `BoxParentData`, который связывает `ContainerDefaultsMixin` с `BoxParentData`. **`BoxParentData` представляет собой класс позиционирования, необходимый для рисования `RenderBox`.**
С помощью `ContainerBoxParentData` мы можем объединить `BoxParentData`, требуемое для `RenderBox`, с вышеупомянутым `ContainerParentDataMixin`. **На самом деле, получаемый двусвязный список детей представляется в форме данных родителя (`ParentData`).**
abstract class ContainerBoxParentData extends BoxParentData with ContainerParentDataMixin { }
#### 4\. MultiChildRenderObjectWidget
Реализация `MultiChildRenderObjectWidget` очень проста — она просто наследуется от `RenderObjectWidget`, предоставляет массив `children` и создает `MultiChildRenderObjectElement`.
```> Как следует из названия, `RenderObjectWidget` предоставляет `RenderObject` в качестве виджета. Но существуют ли виджеты без `RenderObject`?
>
> Да, например, такие распространённые виджеты, как `StatefulWidget`, `StatelessWidget`, `Container` и другие, имеют свои `Element`, являющиеся `ComponentElement`. Эти `ComponentElement` служат лишь контейнерами, а `renderObject` требуется им из их `child`.
>
> #### 5. MultiChildRenderObjectElementКак мы говорили в предыдущих разделах, `Element` является реализацией `BuildContext`, обычно содержащего `Widget`, `RenderObject` и выступающего в качестве моста между ними. Таким образом, `MultiChildRenderObjectElement` служит мостом при создании пользовательских компонентов отображения. Как показано ниже, `MultiChildRenderObjectElement` реализует следующие методы:
/// Внедряет, перемещает и удаляет объекты рендера с помощью методов
/// ContainerRenderObjectMixin
void insertChildRenderObject(RenderObject child, Element slot);
void moveChildRenderObject(RenderObject child, dynamic slot);
void removeChildRenderObject(RenderObject child);
/// Посещение детей осуществляется через ElementVisitor внутри Element
/// Обычно вызывается в RenderObject.get renderObject
void visitChildren(ElementVisitor visitor);
/// Добавление игнорируемых детей _forgottenChildren.add(child);
void forgetChild(Element child);
/// Преобразование списка виджетов children в список элементов List<Element>
void mount(Element parent, dynamic newSlot);
/// Обновление полученного списка элементов List<Element> через метод updateChildren
void update(MultiChildRenderObjectWidget newWidget);
Таким образом, `MultiChildRenderObjectElement` использует `ContainerRenderObjectMixin`, чтобы связать пользовательский `RenderBox` и `Widget`.
#### 6. Процесс создания пользовательского компонента
**Вышеописанный процесс описывает взаимосвязь между `MultiChildRenderObjectWidget`, `MultiChildRenderObjectElement` и тремя вспомогательными классами: `ContainerRenderObjectMixin`, `RenderBoxContainerDefaultsMixin` и `ContainerBoxParentData`.****Понимание этих ключевых классов позволяет нам рассмотреть упрощённый процесс создания пользовательского компонента отображения:**
- 1. Создайте пользовательский `ParentData`, наследуясь от `ContainerBoxParentData`.
- 2. Наследуйтесь от `RenderBox`, одновременно примешивая `ContainerRenderObjectMixin` и `RenderBoxContainerDefaultsMixin` для создания пользовательского `RenderObject`.
- 3. Наследуйтесь от `MultiChildRenderObjectWidget`, реализуя методы `createRenderObject` и `updateRenderObject`, чтобы связать пользовательский `RenderBox`.
- 4. Переопределите методы `performLayout` и `setupParentData` в `RenderBox`, чтобы реализовать пользовательское отображение. **Конечно, мы можем использовать официальный `CustomMultiChildLayout`, чтобы реализовать пользовательское расположение; это будет рассмотрено позже, а пока давайте начнём с базовых принципов.** В вышестоящих процессах включены `ContainerRenderObjectMixin` и `RenderBoxContainerDefaultsMixin`, которые также используются во многих официальных реализациях расположения, таких как `RenderFlex`, `RenderWrap`, `RenderStack`.
## 3\. Пользовательское расположение
**Пользовательское расположение реализуется внутри метода `performLayout`, где задаются размеры `child.layout` и положение `child.ParentData.offset`.**

Сначала нам нужно создать аналогичный эффект, показанный на рисунке выше. Для этого потребуется создание пользовательского `RenderCloudParentData`, которое наследует от `ContainerBoxParentData` и используется для хранения ширины, высоты и области содержимого:```dart
class RenderCloudParentData extends ContainerBoxParentData<RenderBox> {
double width;
double height;
Rect get content => Rect.fromLTWH(
offset.dx,
offset.dy,
width,
height,
);
}
Затем создаем пользовательский RenderCloudWidget
, который наследуется от RenderBox
и использует ContainerRenderObjectMixin
и RenderBoxContainerDefaultsMixin
для упрощенной реализации RenderBox
:
class RenderCloudWidget extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, RenderCloudParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, RenderCloudParentData> {
RenderCloudWidget({
List<RenderBox> children,
Overflow overflow = Overflow.visible,
double ratio,
}) : _ratio = ratio,
_overflow = overflow {
// Добавляем всех детей
addAll(children);
}
final double _ratio;
final Overflow _overflow;
@override
void setupParentData(covariant RenderBox child) {
if (child.parentData is! RenderCloudParentData)
child.parentData = RenderCloudParentData();
}
@override
void performLayout() {
// По умолчанию отсечение не требуется
_needClip = false;
}
}
Как показано ниже, далее следует основной код внутри override performLayout
, где представлены ключевые моменты:
ContainerRenderObjectMixin
, затем читаем весь список от начала до конца.child.layout
, после чего запоминаем эти размеры.Rect
./// Устанавливаем наши данные
@override
void setupParentData(RenderBox child) {
if (child.parentData is! RenderCloudParentData)
child.parentData = RenderCloudParentData();
}
/// Выполняем размещение компонентов
@override
void performLayout() {
// По умолчанию отсечение не требуется
_needClip = false;
}
``````markdown
///Если нет потомков, выходим
if (childCount == 0) {
size = constraints.smallest;
return;
}
///Инициализация области
var recordRect = Rect.zero;
var previousChildRect = Rect.zero;
RenderBox child = firstChild;
while (child != null) {
var curIndex = -1;
///Получение данных
final RenderCloudParentData childParentData = child.parentData;
child.layout(constraints, parentUsesSize: true);
var childSize = child.size;
///Запись размера
childParentData.width = childSize.width;
childParentData.height = childSize.height;
do {
///Установка пропорций осей X и Y
var rX = ratio >= 1 ? ratio : 1.0;
var rY = ratio <= 1 ? ratio : 1.0;
///Регулирование положения
var step = 0.02 * _mathPi;
var rotation = 0.0;
var angle = curIndex * step;
var angleRadius = 5 + 5 * angle;
var x = rX * angleRadius * math.cos(angle + rotation);
var y = rY * angleRadius * math.sin(angle + rotation);
var position = Offset(x, y);
///Вычисление абсолютной смещения
var childOffset = position - Alignment.center.alongSize(childSize);
++curIndex;
///Установка смещения
childParentData.offset = childOffset;
///Проверка наложения
} while (overlaps(childParentData));
///Запись области
previousChildRect = childParentData.content;
recordRect = recordRect.expandToInclude(previousChildRect);
///Следующий
child = childParentData.nextSibling;
}
///Регулирование размера размещения
size = constraints.tighten(height: recordRect.height, width: recordRect.width).smallest;
///Центрирование
var contentCenter = size.center(Offset.zero);
var recordRectCenter = recordRect.center;
var transCenter = contentCenter - recordRectCenter;
child = firstChild;
while (child != null) {
final RenderCloudParentData childParentData = child.parentData;
childParentData.offset += transCenter;
child = childParentData.nextSibling;
}
В данном случае текст внутри комментариев был переведён на русский язык, при этом остальной код и спецификация оставлены без изменений. /// Превышено ли?
_needClip = size.width < recordRect.width || size.height < recordRect.height;
} Просмотрев код, можно заметить, что ключевой момент заключается в том, как вы устанавливаете значение offset
для свойства parentData
у потомка (child
). В конечном итоге, ваш RenderCloudWidget
будет загружаться через компонент CloudWidget
. Конечно, полный код также требует использования компонентов FittedBox
и RotatedBox
, чтобы упростить реализацию, подробнее см.: GSYFlutterDemo.
class CloudWidget extends MultiChildRenderObjectWidget {
final Overflow overflow;
final double ratio;
CloudWidget({
Key key,
this.ratio = 1,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
}) : super(key: key, children: children);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCloudWidget(
ratio: ratio,
overflow: overflow,
);
}
@override
void updateRenderObject(
BuildContext context, RenderCloudWidget renderObject) {
renderObject
..ratio = ratio
..overflow = overflow;
}
}
Итак, мы можем сделать вывод, что процесс создания пользовательского макета состоит в реализации метода performLayout
внутри пользовательского класса RenderBox
.
Компонент CustomMultiChildLayout
представляет собой упрощённую версию механизма пользовательских макетов, предоставляемую Flutter. Этот компонент основан на использовании MultiChildRenderObjectWidget
, но он предлагает нам уже готовые решения с помощью RenderCustomMultiChildLayoutBox
и MultiChildLayoutParentData
, а также позволяет настраивать нужные части через MultiChildLayoutDelegate
.
Для использования CustomMultiChildLayout
вам достаточно создать новый класс, который наследуется от MultiChildLayoutDelegate
, и реализовать следующие методы:
void performLayout(Size size);
bool shouldRelayout(covariant MultiChildLayoutDelegate oldDelegate);
Создание нового класса, наследуемого от MultiChildLayoutDelegate
, и реализация метода performLayout
позволит вам быстро создать необходимый пользовательский компонент. Однако стоит отметить, что это удобство приходит за счет потери гибкости — в методе performLayout
доступен только размер текущего компонента (size
), поэтому для выполнения задачи, такой как установка позиций потомков, вам потребуются значения childSize
и childId
.
childSize
легко понять по названию, а вот что такое childId
? Для начала рассмотрим реализацию MultiChildLayoutDelegate
. Внутри этого класса есть объект Map<Object, RenderBox> _idToChild
, который хранит отображение между Object id
и RenderBox
. Для получения RenderBox
в MultiChildLayoutDelegate
требуется использовать id
.
Объект _idToChild
этого типа Map
создается во время выполнения метода performLayout
класса RenderBox
внутри метода delegate._callPerformLayout
. Созданный id
берется из поля id
класса MultiChildLayoutParentData
. Это поле можно задать с помощью LayoutId
при вложении.
Чтобы завершить вышеописанное расположение, нам нужно знать индекс каждого потомка. Поэтому мы можем установить индекс как id
для каждого потомка через LayoutId
.Таким образом, мы можем указывать id
как числовой индекс с помощью LayoutId
и сообщаем это делегату, что позволяет нам знать порядок и положение каждого потомка.
Этот id
имеет тип Object
, поэтому вы можете присваивать ему множество различных значений.
Как показано ниже, в нашем пользовательском CircleLayoutDelegate
мы знаем положение каждого контроллера (index
). Таким образом, мы знаем положение каждого элемента в круговой макете.
Мы просто используем index
, чтобы вычислить угол, на котором находится каждый потомок, а затем используем layoutChild
и positionChild
для размещения каждого элемента. Полный код доступен здесь: GSYFlutterDemo.
/// Пользовательская реализация кругового макета
class CircleLayoutDelegate extends MultiChildLayoutDelegate {
final List<String> customLayoutId;
final Offset center;
Size childSize;
CircleLayoutDelegate(this.customLayoutId,
{this.center = Offset.zero, this.childSize});
@override
void performLayout(Size size) {
for (var item in customLayoutId) {
if (hasChild(item)) {
double r = 100;
int index = int.parse(item);
double step = 360 / customLayoutId.length;
double hd = (2 * math.pi / 360) * step * index;
var x = center.dx + math.sin(hd) * r;
var y = center.dy - math.cos(hd) * r;
childSize ??= Size(size.width / customLayoutId.length,
size.height / customLayoutId.length);
/// Устанавливаем размер потомка
layoutChild(item, BoxConstraints.loose(childSize));
}
final double centerX = childSize.width / 2.0;
}
``` final double centerY = childSize.height / 2.0;
var result = new Offset(x - centerX, y - centerY);
/// Устанавливаем позицию для потомка
positionChild(item, result);
}
}
}
@override
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
Общим выводом является то, что второй способ реализации более прост, но он также утрачивает некоторую гибкость и степень самонастройки, хотя и становится более строгим и косвенным. При этом при создании своего RenderBox
, можно использовать аналогичный подход с использованием делегата для вторичной обёртки, что сделает такую пользовательскую компоновку более регламентированной и контролируемой.
Таким образом, шестнадцатая статья наконец завершена! (///▽///)
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )