Смотрится сегодняшний материал немного абстрактно? Ещё один вложенный список? Но ведь уже было обсуждение различных вариантов вложения ListView и PageView! А что же особенного в этой поддержке адаптивной высоты?
Это может понадобиться при некоторых уникальных сценариях.
Давайте рассмотрим следующий фрагмент кода. Основная идея заключается в том, чтобы каждый элемент списка vertical
был адаптирован под содержимое, а также чтобы элементы могли содержать в себе горизонтальные списки (horizontal
).
Наш горизонтальный список мы тоже хотели бы сделать адаптивным по размеру относительно своих детей. Как вы считаете, есть ли проблемы в этом коде? Может ли он работать корректно?
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Container(
color: Colors.white,
child: ListView(
children: [
ListView(
scrollDirection: Axis.horizontal,
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "ТЕСТ\n")
.toString()),
),
);
}),
),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
);
}
```Ответ — нет, этот код не будет работать правильно, потому что внутри вертикального списка (`vertical`) находится горизонтальный список (`horizontal`). Горизонтальный список не имеет указанной высоты, а вертикальный список не имеет указанных значений `itemExtent`. Поэтому мы получаем ошибку такого типа:

Почему возникает такая проблема? Вкратце говоря, мы знаем, что Flutter использует процесс отрисовки, где ограничения передаются сверху вниз, а размеры возвращаются снизу вверх. То есть дети должны использовать ограничения родителей для определения своего размера, а родители используют размеры детей для определения своего размера.
> Для тех, кто интересуется этой темой, рекомендую прочитать [«Познакомьтесь с другим подходом к использованию Flutter»](https://juejin.cn/post/7053777774707736613).
Однако для скроллируемых компонентов есть особенность: теоретически они должны иметь «бесконечную» ширину или высоту в направлении скроллинга. Поэтому для таких компонентов требуется установка фиксированного размера окна просмотра, то есть `ViewPort`, который имеет фиксированный размер в направлении главной оси.
Например, `ListView`. Обычно он имеет один `ViewPort`, внутри которого находится `SliverList`, создающий список. При помощи жестов можно прокручивать этот список внутри `ViewPort`.
> Если вас заинтересовало это, вы можете прочитать ["Разберитесь с реализацией скроллирующего списка в Flutter с разных углов"](https://juejin.cn/post/6956215495440007175).
Теперь вернемся к вопросу о том, как встроить горизонтальный `ListView` внутрь вертикального `ListView`:
- Поскольку вертикальный `ListView` не имеет установленного значения `itemExtent`, каждый его потомок не имеет фиксированной высоты. Это позволяет каждому элементу адаптироваться к своей высоте в зависимости от потребностей.
- Горизонтальный `ListView` также не имеет явно указанной высоты. В качестве родителя ему служит вертикальный `ListView`, который теоретически может иметь «бесконечную» высоту. Из-за этого горизонтальному `ListView` невозможно рассчитать эффективную высоту.
Кроме того, поскольку `ListView` отличается от компонентов типа `Row`/`Column`, количество его детей теоретически может быть бесконечным. Часть этих детей, которая не видна, обычно не распределяется и не рисуется, поэтому нельзя рассчитывать общую высоту всех элементов, чтобы определить высоту самого `ListView`.
Какие же способы решения проблемы существуют? На данный момент предлагается два способа решения.
# SingleChildScrollViewКак показано ниже, самый простой способ — заменить горизонтальный `ListView` на `SingleChildScrollView`. В отличие от `ListView`, `SingleChildScrollView` имеет только одного потомка, поэтому его `ViewPort` имеет специфическую структуру.
```dart
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Container(
color: Colors.white,
child: ListView(
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10),
(index) => "ТЕСТ\n"
).toString()),
),
);
}),
),
),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
)
В _RenderSingleChildViewport
внутри компонента SingleChildScrollView
при размещении можно легко получить размер дочернего элемента через вызов child!.layout
, а затем используя Row
рассчитать общую высоту всех дочерних элементов, что позволяет реализовать эффект горизонтального списка.
После запуска результат показан ниже, как видно, в вертикальном списке ListView
горизонтальный SingleChildScrollView
отображается правильно, но возникают проблемы с неравномерной высотой размещения.
Как показано ниже, чтобы решить эту проблему, достаточно вложить IntrinsicHeight
внутрь Row
. Это позволит выровнять высоту внутренних элементов, так как IntrinsicHeight
во время размещения предварительно вызывает метод getMaxIntrinsicHeight
для получения высоты дочернего элемента и модификации переданных ограничений родителем.
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: IntrinsicHeight(
child: Row(
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
alignment: Alignment.bottomCenter,
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "TEST\n")
.toString()),
),
);
}),
),
),
),
Результат выполнения представлен ниже, как видно, теперь все горизонтальные элементы имеют одинаковую высоту, однако этот способ решения также имеет два серьезных недостатка:- Внутри SingleChildScrollView
высота рассчитывается с помощью Row
, то есть при размещении требуется одновременно рассчитывать все дочерние элементы, что может привести к снижению производительности при длинных списках;
IntrinsicHeight
может быть затратным по времени, возможно до O(N²), хотя Flutter кэширует результаты этих вычислений, это не устраняет его временные затраты.Второе решение заключается в создании пользовательского компонента на основе ListView
.
Ранее мы уже упоминали, что ListView
не будет подобно Row
считать размеры всех дочерних элементов. Поэтому вполне возможно создать пользовательский компонент UnboundedListView
для решения этой проблемы.
Эта идея была первоначально представлена на GitHub: https://gist.github.com/vejmartin/b8df4c94587bdad63f5b4ff111ff581c
Сначала мы определяем класс UnboundedListView
, основываясь на ListView
, используя метод mixin
для переопределения соответствующих Viewport
и Sliver
. Это включает:
SliverList
внутри buildChildLayout
на нашу собственную реализацию UnboundedSliverList
.Viewport
внутри buildViewport
на нашу собственную реализацию UnboundedViewport
.Padding
внутри buildSlivers
, заменив SliverPadding
на нашу собственную реализацию UnboundedSliverPadding
.class UnboundedListView = ListView with UnboundedListViewMixin;
/// На основе BoxScrollView
mixin UnboundedListViewMixin on ListView {
@override
Widget buildChildLayout(BuildContext context) {
return UnboundedSliverList(delegate: childrenDelegate);
}
@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
return UnboundedViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
);
}
}
``` @override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry? effectivePadding = padding;
if (padding == null) {
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery != null) {
// Автоматически добавляем отступ слайвера с помощью padding из MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Используем отступ главной оси с помощью SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Оставляем за собой отступ второй оси.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
} if (effectivePadding != null)
sliver =
UnboundedSliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[sliver];
}
}
```Сначала реализуем `UnboundedViewport`:
- На основе `Viewport`, используя метод `createRenderObject`, заменим `RenderViewport` на наш `UnboundedRenderViewport`.
- Добавим кастомную логику в методах `performLayout` и `layoutChildSequence`. В основном это будет добавление параметра `_unboundedSize`, который вычисляется через дочерний `RenderSliver`.
```dart
class UnboundedViewport = Viewport with UnboundedViewportMixin;
mixin UnboundedViewportMixin on Viewport {
@override
RenderViewport createRenderObject(BuildContext context) {
return UnboundedRenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ??
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
);
}
}
class UnboundedRenderViewport = RenderViewport
with UnboundedRenderViewportMixin;
mixin UnboundedRenderViewportMixin on RenderViewport {
@override
bool get sizedByParent => false;
double _unboundedSize = double.infinity;
@override
void performLayout() {
BoxConstraints constraints = this.constraints;
if (axis == Axis.horizontal) {
_unboundedSize = constraints.maxHeight;
size = Size(constraints.maxWidth, 0);
} else {
_unboundedSize = constraints.maxWidth;
size = Size(0, constraints.maxHeight);
}
super.performLayout();
switch (axis) {
case Axis.vertical:
offset.applyViewportDimension(size.height);
break;
case Axis.horizontal:
offset.applyViewportDimension(size.width);
break;
}
}
@override
double layoutChildSequence({
required RenderSliver? child,
required double scrollOffset,
required double overlap,
required double layoutOffset,
required double remainingPaintExtent,
required double mainAxisExtent,
required double crossAxisExtent,
required GrowthDirection growthDirection,
required RenderSliver? advance(RenderSliver child),
required double remainingCacheExtent,
required double cacheOrigin,
}) {
crossAxisExtent = _unboundedSize;
var firstChild = child;
```Дальнейшее продолжение кода можно адаптировать аналогичным образом.
```markdown
final result = super.layoutChildSequence(
child: child,
scrollOffset: scrollOffset,
overlap: overlap,
layoutOffset: layoutOffset,
remainingPaintExtent: remainingPaintExtent,
mainAxisExtent: mainAxisExtent,
crossAxisExtent: crossAxisExtent,
growthDirection: growthDirection,
advance: advance,
remainingCacheExtent: remainingCacheExtent,
cacheOrigin: cacheOrigin,
);
Двойной безграничный размер равен нулю. пока первое дитя не равно null { если первое дитя геометрия является UnboundedSliverGeometry { конечное UnboundedSliverGeometry дитя геометрия равно первому дитю геометрии как тип UnboundedSliverGeometry; безграничный размер равно максимальному значению из безграничного размера и дитя геометрии crossAxisSize; } первое дитя равно продвижению первого дитя; } если ось равна горизонтальной оси { размер равно Размер ширине размера и безграничному размеру; } иначе { размер равно Размер безграничному размеру и высоте размера; }
возвращаемый результат; }
Затем мы наследуем `SliverGeometry` и создаем `UnboundedSliverGeometry`, главным образом добавив параметр `crossAxisSize`, который используется для записи текущего значения высоты второй оси, чтобы верхний `Viewport` мог получить доступ к нему.
---
Перевод:
Двойной безграничный размер равен нулю.
Пока первое дитя не равно null {
Если первое дитя геометрия является UnboundedSliverGeometry {
Конечное UnboundedSliverGeometry дитя геометрия равно первому дитю геометрии как тип UnboundedSliverGeometry;
Безграничный размер равно максимальному значению из безграничного размера и дитя геометрии crossAxisSize;
}
Первое дитя равно продвижению первого дитя;
}
Если ось равна горизонтальной оси {
Размер равно Размер ширине размера и безграничному размеру;
} иначе {
Размер равно Размер безграничному размеру и высоте размера;
}
Возвращаемый результат;
}
Затем мы наследуем `SliverGeometry` и создаем `UnboundedSliverGeometry`, главным образом добавив параметр `crossAxisSize`, который используется для записи текущего значения высоты второй оси, чтобы верхний `Viewport` мог получить доступ к нему.```dart
Класс UnboundedSliverGeometry расширяет SliverGeometry {
Конструктор UnboundedSliverGeometry(
{SliverGeometry? existing, required double crossAxisSize})
: супер(
scrollExtent: existing?.scrollExtent ?? 0.0,
paintExtent: existing?.paintExtent ?? 0.0,
paintOrigin: existing?.paintOrigin ?? 0.0,
layoutExtent: existing?.layoutExtent,
maxPaintExtent: existing?.maxPaintExtent ?? 0.0,
maxScrollObstructionExtent: existing?.maxScrollObstructionExtent ?? 0.0,
hitTestExtent: existing?.hitTestExtent,
visible: existing?.visible,
hasVisualOverflow: existing?.hasVisualOverflow ?? false,
scrollOffsetCorrection: existing?.scrollOffsetCorrection,
cacheExtent: existing?.cacheExtent,
);
final double crossAxisSize;
}
```Как показано ниже, в конечном итоге мы реализуем `UnboundedSliverList` на основе `SliverList`. Это и есть основная логика, которая заключается в реализации части кода `performLayout`. Нам нужно добавить специфическую логику в некоторых узлах, чтобы считывать высоту каждого элемента, участвующего в макете, и найти максимальное значение.
> Код кажется длинным, но фактически мы добавили очень мало нового кода.
```dart
Класс UnboundedSliverList равен SliverList с UnboundedSliverListMixin;
Миксин UnboundedSliverListMixin на SliverList {
@override
Создание объекта отрисовки RenderSliverList равно RenderSliverList.createRenderObject(контекст);
Конечное SliverMultiBoxAdaptorElement элемент равно контекст как тип SliverMultiBoxAdaptorElement;
Возврат UnboundedRenderSliverList(детский менеджер: элемент);
}
Класс БезграничныйОтрисовщикСливерЛист расширяет ОтрисовщикСливерЛист {
БезграничныйОтрисовщикСливерЛист({
Required ОтрисовщикСливерКонтейнерДетей менеджерДетей,
}) : super(менеджерДетей: менеджерДетей);
// Смотрите ОтрисовщикСливерЛист::выполнение_размещения
@Override
void выполнение_размещения() {
Final СлайверОграничения ограничения = This.ограничения;
МенеджерДетей.началоРазмещения();
МенеджерДетей.установкаПодошелаКонец(False);
}
}
``` ```java
final double смещениеПрокрутки =
ограничения.смещениеПрокрутки + ограничения.кэшИсходнойПозиции;
assert(смещениеПрокрутки >= 0.0);
final double оставшийся_расстояние = ограничения.оставшийсяКэшРасстояние;
assert(оставшийся_расстояние >= 0.0);
final double целевоеКонечноСмещениеПрокрутки = смещениеПрокрутки + оставшийся_расстояние;
ОграниченияДетей childConstraints = ограничения.asBoxConstraints();
int ведущий_мусор = 0;
int следующий_мусор = 0;
boolean достигнут_конца = false;
``` if (ограничения.ось == Ось.horizontal) {
childConstraints = childConstraints.copyWith(minHeight: 0);
} else {
childConstraints = childConstraints.copyWith(minWidth: 0);
}
double бескрайний_размер = 0;
// следует вызвать обновление после каждого размещения ребенка
void обновлениебескрайногоразмера(RenderBox? ребенок) {
if (ребенок == null) {
return;
}
бескрайний_размер = math.max(
бескрайний_размер,
ограничения.ось == Ось.horizontal
? ребенок.size.height
: ребенок.size.width);
}```dart
void безграничный_геометрия(SliverGeometry геометрия) {
return БезграничныйСлайверГеометрия(
существующее: геометрия,
поперечный_размер: безграничный_размер,
);
}
}
## Алгоритм
Этот алгоритм в принципе прост: найти первого потомка, который пересекает указанный `scrollOffset`, создавая больше детей сверху списка при необходимости, затем пройтись по списку снизу вверх, обновляя и располагая каждого ребенка и добавляя больше детей в конец при необходимости, пока количество детей не будет достаточно большим, чтобы покрыть весь viewport.
Он усложняется одной небольшой проблемой, которая заключается в том, что каждый раз, когда вы обновляете или создаёте ребенка, возможно, что некоторые дети, которые еще не были расположены, будут удалены, оставляя список в неконсистентном состоянии, и требуя воссоздания отсутствующих узлов.
Чтобы сделать эту путаницу управляемой, этот алгоритм начинается с того, что является текущим первым ребенком, если таковой имеется, а затем проходит вверх и/или вниз от этого места, так что узлы, которые могут быть удалены, всегда находятся на границах уже расположенного контента.
// Убедитесь, что у нас есть хотя бы один потомок для начала.
if (firstChild == null) {
if (!addInitialChild()) {
// Нет потомков.
geometry = безграничный_геометрия(SliverGeometry.zero);
childManager.didFinishLayout();
return;
}
}
```// У нас есть хотя бы один потомок.
// Эти переменные отслеживают диапазон потомков, которых мы выстроили. Внутри этого диапазона потомки имеют последовательные индексы. За пределами этого диапазона возможно удаление потомка без уведомления.
RenderBox? leadingChildWithLayout, trailingChildWithLayout;
RenderBox? earliestUsefulChild = firstChild;```markdown
// Найдите последнего потомка, который находится до или в момент scrollOffset.
earliestUsefulChild = firstChild;
for (double最早的有效子元素滚动偏移量 = childScrollOffset(earliestUsefulChild!)!;
最早有效滚动偏移量 > scrollOffset;
最早有效子元素滚动偏移量 = childScrollOffset(earliestUsefulChild)!){
// Мы должны добавить детей перед earliestUsefulChild.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData 子元素父数据 = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
子元素父数据.layoutOffset = 0.0;
}
}
Перевод:
// Найдите последнего потомка, который находится до или в момент scrollOffset.
earliestUsefulChild = firstChild;
for (double earliestEffectiveChildIndexScrollOffset = childScrollOffset(earliestUsefulChild!)!;
earliestEffectiveChildIndexScrollOffset > scrollOffset;
earliestEffectiveChildIndexScrollOffset = childScrollOffset(earliestUsefulChild)!){
// Мы должны добавить детей перед earliestUsefulChild.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childElementParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childElementParentData.layoutOffset = 0.0;
}
}
``````markdown
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only performs layout of children up to the first child. In this case nothing has been laid out yet. We need to manually lay out the first child.
первыйРебёнок!.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = первыйРебёнок;
updateUnboundedSize(earliestUsefulChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
// We finished laying out children before reaching the scroll offset value.
// We should inform our parent that this sliver cannot fulfill its contract and that we require a correction in the scroll offset value.
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
));
return;
}
``` final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(первого_ребёнка!);
// значение firstChildScrollOffset может содержать ошибку двойной точности
if (firstChildScrollOffset < -precisionErrorTolerance) {
// Давайте предположим, что нет детей перед первым ребенком. Мы исправим это при следующем размещении, если это не так.
геометрия = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
));
final SliverMultiBoxAdaptorParentData childParentData =
первого_ребёнка!.parentData! как SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}```md
```dart
final SliverMultiBoxAdaptorParentData childParentData =
earliestUseful_child.parentData! как SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
убедиться(earliestUsefulChild == первый_ребёнок);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
убедиться(childScrollOffset(первый_ребёнок!)! > -precisionErrorTolerance);
// Если смещение прокрутки равно нулю, мы должны убедиться, что действительно находимся в начале списка.
if (scrollOffset < precisionErrorTolerance) {
// Мы итерируем от первого ребенка, если ведущий ребенок имеет нулевой размер рисунка.
while (indexOf(firstChild!) > 0) {
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
// Мы исправляем одного ребенка за раз. Если есть более ранние дети перед earliestUsefulChild, мы исправим это, когда смещение прокрутки снова достигнет нуля.
earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
assert(earliestUsefulChild != null);
final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild!);
final SliverMultiBoxAdaptorParentData childParentData = firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
// Нужно исправлять только тогда, когда ведущий ребенок действительно имеет размер рисунка.
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
));
return;
}
}
}
// На данном этапе earliestUsefulChild является первым потомком,
// чей scrollOffset находится до или включительно с текущим scrollOffset,
// а leadingChildWithLayout и trailingChildWithLayout либо равняются null,
// либо охватывают диапазон render box'ов, которые мы уже выстроили,
// причём первый из которых совпадает с earliestUsefulChild,
// а последний — либо после, либо включительно с scrollOffset. assert(earliestUsefulChild == firstChild);
assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
// Убедитесь, что хотя бы один потомок был размещен.
if (leadingChildWithLayout == null) {
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}```markdown
bool в_диапазоне_расположения = true;
RenderBox? потомок = самый_ранний_полезный_потомок;
int индекс = индекс_потока(потомок! );
double конец_скролл_оффсета = child_scroll_offset(потомок)! + paint_extent_of(потомок);
bool продвинуться() {
// Возвращает true, если мы продвинулись, false, если больше нет потомков
// Эта функция используется в двух разных местах ниже, чтобы избежать дублирования кода.
assert(потомок != null);
if (потомок == последний_потомок_с_расположением) в_диапазоне_расположения = false;
потомок = child_after(потомок! );
if (потомок == null) в_диапазоне_расположения = false;
индекс += 1;
if (! в_диапазоне_расположения) {
if (потомок == null || индекс_потока(потомок! ) != индекс) {
// Мы пропустили одного потомка. Вставляем его (и располагаем) при возможности.
потомок = вставить_и_расположить_потомка(
child_constraints,
после: последний_потомок_с_расположением,
родитель_использует_размер: true,
);
обновить_небезграничный_размер(потомок);
if (потомок == null) {
// У нас закончились потомки.
return false;
}
} else {
// Располагаем потомка.
потомок!.layout(child_constraints, родитель_использует_размер: true);
}
}
return true;
}
Здесь текст внутри кода был переведён на русский язык, сохранив структуру и форматирование исходного текста. обновить_небезграничный_размер(потомок! ); } последний_потомок_с_расположением = потомок; } assert(потомок != null); final SliverMultiBoxAdaptorParentData childParentData = потомок!.родительДанные! as SliverMultiBoxAdaptorParentData; childParentData.layoutOffset = конец_скролл_оффсета; assert(childParentData.index == индекс); конец_скролл_оффсета = childScrollOffset(потомок!)! + paintExtentOf(потомок!); return true; }
// Найдите первого потомка, который заканчивается после смещения прокрутки.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// Мы хотим убедиться, что последний потомок остаётся активным, чтобы знать конечное смещение прокрутки
collectGarbage(leadingGarbage - 1, 0);
assert(firstChild == lastChild);
final double extent =
childScrollOffset(lastChild!)! + paintExtentOf(lastChild!);
geometry = unboundedGeometry(
SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
),
);
return;
}
}
// Теперь найдите первого потомка, который заканчивается после нашего конца.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
// В конце подсчитайте всех оставшихся потомков и пометьте их как мусор.
if (child != null) {
child = childAfter(child!);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child!);
}
}
// На этом этапе всё должно быть готово, нам просто нужно очистить мусор и отчёт о геометрии.
collectGarbage(leadingGarbage, trailingGarbage);
```
```markdown
assert(debugAssertChildListIsNonEmptyAndContiguous());
double estimatedMaxScrollOffset;
if (reachedEnd) {
estimatedMaxScrollOffset = endScrollOffset;
} else {
estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
constraints,
firstIndex: indexOf(firstChild!),
lastIndex: indexOf(lastChild!),
leadingScrollOffset: childScrollOffset(firstChild!),
trailingScrollOffset: endScrollOffset,
);
assert(estimatedMaxScrollOffset >=
endScrollOffset - childScrollOffset(firstChild!)!);
}
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild!)!,
to: endScrollOffset,
);
final double cacheExtent = calculateCacheOffset(
constraints,
from: childScrollOffset(firstChild!)!,
to: endScrollOffset,
);
final double targetEndScrollOffsetForPaint =
constraints.scrollOffset + constraints.remainingPaintExtent;
geometry = unboundedGeometry(
SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintExtent,
cacheExtent: cacheExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Консервативный подход для предотвращения мерцания при прокрутке.
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint ||
constraints.scrollOffset > 0.0,
),
);
```
В данном случае единственным местом, где требовалось исправление, было замена "мерцание при скролле" на "мерцание при прокрутке". Остальной текст был корректен и не требовал изменения.```markdown
Не обращайте внимания на то, что вышеупомянутый код выглядит длинным; на самом деле большая часть — это исходный код `RenderSliverList`. Как показано ниже:
Действительно, мы добавили только следующее:
- В начале добавлены методы `updateUnboundedSize` и `unboundedGeometry`, чтобы записывать высоту layouts и генерировать `UnboundedSliverGeometry`.
- Все прежние `SliverGeometry` были заменены на `UnboundedSliverGeometry`.
- После каждого вызова `layout` был добавлен вызов `updateUnboundedSize`, так как после того, как children были отрисованы, мы можем получить их размеры и суммировать максимальные значения, чтобы вернуть `UnboundedSliverGeometry` в `Viewport`.

Наконец, как показано ниже, `UnboundedListView` был добавлен в начальный вертикальный `ListView`. После запуска можно заметить, что при горизонтальной прокрутке высота списка меняется.
```
``````dart
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Container(
color: Colors.white,
child: ListView(
children: [
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: IntrinsicHeight(
child: Row(
children: List<Widget>.generate(50, (index) {
return Padding(
padding: EdgeInsets.all(2),
child: Container(
alignment: Alignment.bottomCenter,
color: Colors.blue,
child: Text(List.generate(
math.Random().nextInt(10), (index) => "ТЕСТ\n")
.toString()),
),
);
}),
),
),
),
UnboundedListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 100,
itemBuilder: (context, index) {
print('$index');
return Padding(
padding: EdgeInsets.all(2),
child: Container(
height: index * 1.0 + 10,
width: 50,
color: Colors.blue,
),
);
}),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
);
```Так вот, это значит, что мы не можем использовать методы вроде `IntrinsicHeight`, так как элементы списка (`ListView`) создаются динамически. **Это означает, что при размещении требуется обрабатывать добавление и удаление элементов в определенной области**, что реализуется через `scrollOffset` и `targetEndScrollOffset` в методе `performLayout`.
```> Это делает невозможным получение потомка (`child`) до его размещения через связь `firstChild`, поскольку он еще не был добавлен в эту цепочку. Также ограничиваются возможности переопределения методов `insertAndLayoutLeadingChild` и `insertAndLayoutChild` из-за их внутренней реализации.
Однако, как говорится, «не бывает беды, чтобы не было радости», если мы не можем обработать элементы до их размещения, то можно сделать дополнительное размещение после него, как показано ниже:
- Мы сначала выносим `unboundedSize` в качестве глобальной переменной внутри `UnboundedRenderSliverList`
- Перед вызовом `didFinishLayout`, используем структуру `firstChild` для повторного размещения через `layout(childConstraints.tighten(height: unboundedSize))`
```dart
double unboundedSize = 0;
// Смотрите RenderSliverList::performLayout
@Override
void performLayout() {
...
var tmpChild = firstChild;
while (tmpChild != null) {
tmpChild.layout(childConstraints.tighten(height: unboundedSize),
parentUsesSize: true);
tmpChild = childAfter(tmpChild);
}
childManager.didFinishLayout();
...
}
```
После выполнения можно заметить, что список теперь полностью выровнен, а затраты заключаются в том, что child будет иметь двойное расположение. В отношении этих производительных потерь, сравнивая с реализацией `SingleChildScrollView`, можно выбрать подходящий вариант в зависимости от конкретной ситуации. **Конечно, для повышения производительности рекомендуется при необходимости задать высоту горизонтальному `ListView`, так как это обеспечивает оптимальное решение**.
Таким образом, эта небольшая хитрость решена. Не знаете ли вы, есть ли ещё какие-либо идеи по аналогичной реализации? Если у вас есть лучшее решение, приветствуем ваши комментарии и предложения.
> Полный код доступен по адресу: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/un_bounded_listview.dart
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )