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

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

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

Этот пост раскроет принципы создания пользовательских макетов в Flutter и позволит вам углубиться в практическое применение процесса создания пользовательских макетов с использованием двух методов реализации. В результате вы получите следующий интерфейс:

Содержание статьи:

Полный набор практических руководств по Flutter

Серия статей "Мир Flutter"

Интерфейс

1. Введение

В предыдущих разделах мы уже рассматривали взаимоотношения между 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. А что представляют собой остальные классы?

1. ContainerRenderObjectMixin

Как следует из названия, это миксин-класс ContainerRenderObjectMixin, который в основном предназначен для поддержки двусвязного списка детей RenderObject.

Добавление ContainerRenderObjectMixin в RenderBox позволяет получить двусвязный список детей, что удобно при создании макета, так как позволяет получать и управлять RenderObject в прямом и обратном направлении.

2. RenderBoxContainerDefaultsMixinRenderBoxContainerDefaultsMixin представляет собой расширение 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`.**

![image](http://img.cdn.guoshuyu.cn/2bk/image3)

Сначала нам нужно создать аналогичный эффект, показанный на рисунке выше. Для этого потребуется создание пользовательского `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, где представлены ключевые моменты:

  • 1. Мы сначала получаем первый элемент списка ContainerRenderObjectMixin, затем читаем весь список от начала до конца.
  • 2. Для каждого ребенка сначала задаем его размер через child.layout, после чего запоминаем эти размеры.
  • 3. Начинаем расстановку элементов с центра контейнера, двигаясь от внутренней части к внешней. При каждом шаге проверяем, чтобы новые элементы не пересекались с уже размещенными, используя запомненные Rect.
  • 4. После завершения расстановки, задаем общую центральную ориентацию.
/// Устанавливаем наши данные
@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.

4. CustomMultiChildLayout

Компонент CustomMultiChildLayout представляет собой упрощённую версию механизма пользовательских макетов, предоставляемую Flutter. Этот компонент основан на использовании MultiChildRenderObjectWidget, но он предлагает нам уже готовые решения с помощью RenderCustomMultiChildLayoutBox и MultiChildLayoutParentData, а также позволяет настраивать нужные части через MultiChildLayoutDelegate.image

Для использования 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 )

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

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