Does this topic look a bit abstract today? List nesting again? Didn't you share "Various Fancy Nesting of ListView and PageView" before ? So what's different about the adaptive size layout support this time?
It is only needed in some strange scenarios.
First, let's look at the following piece of code. The basic logic is: we hope vertical
that ListView
each Item in is self-adaptive in size according to its content, and there will be horizontal
such ListView
a child in Item.
horizontal
ListView
We also hope that it can children
adapt to its own size. So what do you think is wrong with this code? Does it work properly?
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) => "TEST\n")
.toString()),
),
);
}),
),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
);
}
The answer is no, because vertical
in nestedListView
, and the horizontal does not specify the height, and the vertical does not specify , so we will get the error shown in the following figure:horizontal
ListView
ListView
ListView
itemExtent
Why is there such a problem? To put it simply, we all know that Flutter is a layout process Size
that passes child needs to determine its own size through the constraints of the parent, and then the parent Size
Determine its own size based on the returned by child .
Those who are interested in this part can read "Take you to understand different Flutter"
But it is a bit special for the sliding control, because the sliding control needs to be "infinite" in theory on the main axis of its sliding direction, so for the sliding control, it needs to have a fixed size of "window". That is, ViewPort
this "window" needs to have a size in the direction of the main axis.
For example ListView
, under normal circumstances there is one ViewPort
, and then SliverList
build , and then ViewPort
move correspondingly under the "window" through gestures, so as to achieve the effect of sliding the list.
If you are interested, you can read "A Different Angle Takes You to Understand the Implementation of Sliding Lists in Flutter"
Then let's go back to the nested question vertical
above :ListView
horizontal
ListView
- Because the vertical
ListView
is not setitemExtent
, each of its children will not have a fixed height, because our requirement is that each Item adapts to its height according to its own needs. - The horizontal one
ListView
does not have a clear height, and the verticalListView
height theory of the parent is "infinite height", so the horizontal oneListView
cannot be calculated to obtain an effective height.
In addition, ListView
unlike Row
/ Column
and other controls, children
the theory is "infinite", and the parts that are not displayed are generally not laid out and drawn, so it is not possible to calculate the height of all controls like Row
/ Column
to determine its own height .
So what are the ways to break it? Currently, two solutions are available.
SingleChildScrollView
As shown in the following code, the easiest thing to do first is to ListView
replace SingleChildScrollView
, because there ListView
is SingleChildScrollView
only one child, so its is ViewPort
also special.
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) => "TEST\n")
.toString()),
),
);
}),
),
),
Container(
height: 1000,
color: Colors.green,
),
],
),
),
)
In SingleChildScrollView
the _RenderSingleChildViewport
of , the size of the child can be easily obtained through child!.layout
after , and then the combined Row
height of all the children can be calculated in conjunction with , so that a horizontal list effect can be achieved.
After running, the result is shown in the figure below. You can see that the vertical and ListView
horizontal SingleChildScrollView
are rendered correctly, but there is an "uneven" height layout at this time.
As shown in the following code, we only need to Row
nest IntrinsicHeight
to align its internal height, because IntrinsicHeight
during layout, it will call child's in advance getMaxIntrinsicHeight
to obtain the child's height, and modify the constraint information passed to the child by the parent.
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()),
),
);
}),
),
),
),
The running effect is as follows. It can be seen that the heights of all horizontal items are the same at this time, but this solution also has two fatal problems:
SingleChildScrollView
Here is the heightRow
calculated , that is, all children need to be calculated at one time during layout. If the list is too long, performance loss will occurIntrinsicHeight
The process of calculating the layout will be time-consuming, and may reach O(N²). Although Flutter has cached this part of the calculation results, it does not hinder its time-consuming.
UnboundedListView
The second solution is to customize based ListView
on . Didn’t we say ListView
that won’t count the size of children like Row
that ? Then we can customize a UnboundedListView
to count.
This part of the idea first came from Github: https://gist.github.com/vejmartin/b8df4c94587bdad63f5b4ff111ff581c
First of all, we ListView
define UnboundedListView
, mixin
through the override
corresponding Viewport
and Sliver
, that is:
buildChildLayout
ReplaceSliverList
the in with our customUnboundedSliverList
buildViewport
ReplaceViewport
the in with our customUnboundedViewport
buildSlivers
Process logic inPadding
andSliverPadding
replace with customUnboundedSliverPadding
class UnboundedListView = ListView with UnboundedListViewMixin;
/// BoxScrollView 的基础上
mixin UnboundedListViewMixin on ListView {
Widget buildChildLayout(BuildContext context) {
return UnboundedSliverList(delegate: childrenDelegate);
}
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
return UnboundedViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
);
}
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry? effectivePadding = padding;
if (padding == null) {
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery != null) {
// Automatically pad sliver with padding from 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);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
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];
}
}
The next step is to implement UnboundedViewport
, the same routine:
- Firstly,
Viewport
based on , by modifyingcreateRenderObject
to ourRenderViewPort
UnboundedRenderViewport
- Based on the custom logic of
RenderViewport
adding andperformLayout
, it is actually adding a parameter, which is obtained through statistics inlayoutChildSequence
unboundedSize
RenderSliver
class UnboundedViewport = Viewport with UnboundedViewportMixin;
mixin UnboundedViewportMixin on Viewport {
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 {
bool get sizedByParent => false;
double _unboundedSize = double.infinity;
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;
}
}
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;
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,
);
double unboundedSize = 0;
while (firstChild != null) {
if (firstChild.geometry is UnboundedSliverGeometry) {
final UnboundedSliverGeometry childGeometry =
firstChild.geometry as UnboundedSliverGeometry;
unboundedSize = math.max(unboundedSize, childGeometry.crossAxisSize);
}
firstChild = advance(firstChild);
}
if (axis == Axis.horizontal) {
size = Size(size.width, unboundedSize);
} else {
size = Size(unboundedSize, size.height);
}
return result;
}
}
Next, we inherit and SliverGeometry
customize one UnboundedSliverGeometry
, mainly by adding a crossAxisSize
parameter to record the current statistics of the sub-axis height, so that the above ViewPort
can be obtained.
class UnboundedSliverGeometry extends SliverGeometry {
UnboundedSliverGeometry(
{
SliverGeometry? existing, required this.crossAxisSize})
: super(
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;
}
As shown in the following code, we finally SliverList
implement UnboundedSliverList
, which is also the core logic, mainly to implement performLayout
the part of the code. We need to add custom logic to some nodes on the basis of the original code to count each node participating in the layout. Item height, so as to get a maximum value.
The code looks very long, but in fact we have added very little.
class UnboundedSliverList = SliverList with UnboundedSliverListMixin;
mixin UnboundedSliverListMixin on SliverList {
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element =
context as SliverMultiBoxAdaptorElement;
return UnboundedRenderSliverList(childManager: element);
}
}
class UnboundedRenderSliverList extends RenderSliverList {
UnboundedRenderSliverList({
required RenderSliverBoxChildManager childManager,
}) : super(childManager: childManager);
// See RenderSliverList::performLayout
void performLayout() {
final SliverConstraints constraints = this.constraints;
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double scrollOffset =
constraints.scrollOffset + constraints.cacheOrigin;
assert(scrollOffset >= 0.0);
final double remainingExtent = constraints.remainingCacheExtent;
assert(remainingExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingExtent;
BoxConstraints childConstraints = constraints.asBoxConstraints();
int leadingGarbage = 0;
int trailingGarbage = 0;
bool reachedEnd = false;
if (constraints.axis == Axis.horizontal) {
childConstraints = childConstraints.copyWith(minHeight: 0);
} else {
childConstraints = childConstraints.copyWith(minWidth: 0);
}
double unboundedSize = 0;
// should call update after each child is laid out
updateUnboundedSize(RenderBox? child) {
if (child == null) {
return;
}
unboundedSize = math.max(
unboundedSize,
constraints.axis == Axis.horizontal
? child.size.height
: child.size.width);
}
unboundedGeometry(SliverGeometry geometry) {
return UnboundedSliverGeometry(
existing: geometry,
crossAxisSize: unboundedSize,
);
}
// This algorithm in principle is straight-forward: find the first child
// that overlaps the given scrollOffset, creating more children at the top
// of the list if necessary, then walk down the list updating and laying out
// each child and adding more at the end if necessary until we have enough
// children to cover the entire viewport.
//
// It is complicated by one minor issue, which is that any time you update
// or create a child, it's possible that the some of the children that
// haven't yet been laid out will be removed, leaving the list in an
// inconsistent state, and requiring that missing nodes be recreated.
//
// To keep this mess tractable, this algorithm starts from what is currently
// the first child, if any, and then walks up and/or down from there, so
// that the nodes that might get removed are always at the edges of what has
// already been laid out.
// Make sure we have at least one child to start from.
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = unboundedGeometry(SliverGeometry.zero);
childManager.didFinishLayout();
return;
}
}
// We have at least one child.
// These variables track the range of children that we have laid out. Within
// this range, the children have consecutive indices. Outside this range,
// it's possible for a child to get removed without notice.
RenderBox? leadingChildWithLayout, trailingChildWithLayout;
RenderBox? earliestUsefulChild = firstChild;
// A firstChild with null layout offset is likely a result of children
// reordering.
//
// We rely on firstChild to have accurate layout offset. In the case of null
// layout offset, we have to find the first child that has valid layout
// offset.
if (childScrollOffset(firstChild!) == null) {
int leadingChildrenWithoutLayoutOffset = 0;
while (earliestUsefulChild != null &&
childScrollOffset(earliestUsefulChild) == null) {
earliestUsefulChild = childAfter(earliestUsefulChild);
leadingChildrenWithoutLayoutOffset += 1;
}
// We should be able to destroy children with null layout offset safely,
// because they are likely outside of viewport
collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
// If can not find a valid layout offset, start from the initial child.
if (firstChild == null) {
if (!addInitialChild()) {
// There are no children.
geometry = unboundedGeometry(SliverGeometry.zero);
childManager.didFinishLayout();
return;
}
}
}
// Find the last child that is at or before the scrollOffset.
earliestUsefulChild = firstChild;
for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild!)!;
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)!) {
// We have to add children before the earliestUsefulChild.
earliestUsefulChild =
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) {
// insertAndLayoutLeadingChild only lays out the children before
// firstChild. In this case, nothing has been laid out. We have
// to lay out firstChild manually.
firstChild!.layout(childConstraints, parentUsesSize: true);
earliestUsefulChild = firstChild;
updateUnboundedSize(earliestUsefulChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
break;
} else {
// We ran out of children before reaching the scroll offset.
// We must inform our parent that this sliver cannot fulfill
// its contract and that we need a scroll offset correction.
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
));
return;
}
}
final double firstChildScrollOffset =
earliestScrollOffset - paintExtentOf(firstChild!);
// firstChildScrollOffset may contain double precision error
if (firstChildScrollOffset < -precisionErrorTolerance) {
// Let's assume there is no child before the first child. We will
// correct it on the next layout if it is not.
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
));
final SliverMultiBoxAdaptorParentData childParentData =
firstChild!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = 0.0;
return;
}
final SliverMultiBoxAdaptorParentData childParentData =
earliestUsefulChild.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout ??= earliestUsefulChild;
}
assert(childScrollOffset(firstChild!)! > -precisionErrorTolerance);
// If the scroll offset is at zero, we should make sure we are
// actually at the beginning of the list.
if (scrollOffset < precisionErrorTolerance) {
// We iterate from the firstChild in case the leading child has a 0 paint
// extent.
while (indexOf(firstChild!) > 0) {
final double earliestScrollOffset = childScrollOffset(firstChild!)!;
// We correct one child at a time. If there are more children before
// the earliestUsefulChild, we will correct it once the scroll offset
// reaches zero again.
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;
// We only need to correct if the leading child actually has a
// paint extent.
if (firstChildScrollOffset < -precisionErrorTolerance) {
geometry = unboundedGeometry(SliverGeometry(
scrollOffsetCorrection: -firstChildScrollOffset,
));
return;
}
}
}
// At this point, earliestUsefulChild is the first child, and is a child
// whose scrollOffset is at or before the scrollOffset, and
// leadingChildWithLayout and trailingChildWithLayout are either null or
// cover a range of render boxes that we have laid out with the first being
// the same as earliestUsefulChild and the last being either at or after the
// scroll offset.
assert(earliestUsefulChild == firstChild);
assert(childScrollOffset(earliestUsefulChild!)! <= scrollOffset);
// Make sure we've laid out at least one child.
if (leadingChildWithLayout == null) {
earliestUsefulChild!.layout(childConstraints, parentUsesSize: true);
updateUnboundedSize(earliestUsefulChild);
leadingChildWithLayout = earliestUsefulChild;
trailingChildWithLayout = earliestUsefulChild;
}
// Here, earliestUsefulChild is still the first child, it's got a
// scrollOffset that is at or before our actual scrollOffset, and it has
// been laid out, and is in fact our leadingChildWithLayout. It's possible
// that some children beyond that one have also been laid out.
bool inLayoutRange = true;
RenderBox? child = earliestUsefulChild;
int index = indexOf(child!);
double endScrollOffset = childScrollOffset(child)! + paintExtentOf(child);
bool advance() {
// returns true if we advanced, false if we have no more children
// This function is used in two different places below, to avoid code duplication.
assert(child != null);
if (child == trailingChildWithLayout) inLayoutRange = false;
child = childAfter(child!);
if (child == null) inLayoutRange = false;
index += 1;
if (!inLayoutRange) {
if (child == null || indexOf(child!) != index) {
// We are missing a child. Insert it (and lay it out) if possible.
child = insertAndLayoutChild(
childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,
);
updateUnboundedSize(child);
if (child == null) {
// We have run out of children.
return false;
}
} else {
// Lay out the child.
child!.layout(childConstraints, parentUsesSize: true);
updateUnboundedSize(child!);
}
trailingChildWithLayout = child;
}
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData =
child!.parentData! as SliverMultiBoxAdaptorParentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
endScrollOffset = childScrollOffset(child!)! + paintExtentOf(child!);
return true;
}
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
leadingGarbage += 1;
if (!advance()) {
assert(leadingGarbage == childCount);
assert(child == null);
// we want to make sure we keep the last child around so we know the end scroll offset
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;
}
}
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if (!advance()) {
reachedEnd = true;
break;
}
}
// Finally count up all the remaining children and label them as garbage.
if (child != null) {
child = childAfter(child!);
while (child != null) {
trailingGarbage += 1;
child = childAfter(child!);
}
}
// At this point everything should be good to go, we just have to clean up
// the garbage and report the geometry.
collectGarbage(leadingGarbage, trailingGarbage);
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,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint ||
constraints.scrollOffset > 0.0,
),
);
// We may have started the layout while scrolled to the end, which would not
// expose a new child.
if (estimatedMaxScrollOffset == endScrollOffset)
childManager.setDidUnderflow(true);
childManager.didFinishLayout();
}
}
Although the above code is very long, in fact, many of them are RenderSliverList
our own source code, as shown in the figure below, the only thing we really modify and add is this:
- Increment
updateUnboundedSize
andunboundedGeometry
are used to record layout height and buildUnboundedSliverGeometry
SliverGeometry
Change all original toUnboundedSliverGeometry
- Call it after all the positions
layout
involvedupdateUnboundedSize
, because we can get the child after it is laid outSize
, and then we can get their maximum value by statistics, which can beUnboundedSliverGeometry
returnedViewPort
.
Finally, as shown in the following code, it will be UnboundedListView
added to the vertical ListView
at the beginning. After running, you can see that the height of the list itself is changing as you slide horizontally.
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) => "TEST\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,
),
],
),
),
);
So does this meet our needs? As shown in the following code, if I modify the code as shown below, after running, you can see that the horizontal list at this time becomes jagged.
UnboundedListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 100,
itemBuilder: (context, index) {
print('$index');
return Container(
padding: EdgeInsets.all(2),
child: Container(
width: 50,
color: Colors.blue,
alignment: Alignment.bottomCenter,
child: Text(List.generate(
math.Random().nextInt(15), (index) => "TEST\n")
.toString()),
),
);
}),
But at this time we can't solve it in a IntrinsicHeight
similar way, because ListView
the Items in are all dynamically processed, that is, the addition and destruction of Items within a specific cheap range need to be processed during layout. Specifically, performLayout
in andscrollOffset
will be used to determine the layout of Items targetEndScrollOffset
scope.
As a result, when we access through
firstChild
the linked list structure, we cannotlayout
get the child before because it has not been added to the linked list at this time, and it is also limited byinsertAndLayoutLeadingChild
theinsertAndLayoutChild
coupling implementation and private method of and , which is inconvenient and simple Rewrite support.
But "there is no such thing as a path", since we can't layout
handle it before child, then we can do one more redundant layout layout
after , as shown in the following code:
- We first
unboundedSize
extract asUnboundedRenderSliverList
a global variable in didFinishLayout
Before , throughfirstChild
the linked list structure, re-passlayout(childConstraints.tighten(height: unboundedSize)
the layout once more
double unboundedSize = 0;
// See RenderSliverList::performLayout
void performLayout() {
····
var tmpChild = firstChild;
while (tmpChild != null) {
tmpChild.layout(childConstraints.tighten(height: unboundedSize),
parentUsesSize: true);
tmpChild = childAfter(tmpChild);
}
childManager.didFinishLayout();
····
}
After running, you can see that the lists are all aligned at this time, and the loss is that the child will have a double layout. For the performance loss here, compared SingleChildScrollView
with implementation, you can choose which logic to use according to the actual scene. Of course, for performance considerations It is not necessary to give the horizontal ListView
a height, such an implementation is the optimal solution .
Well, this little trick is solved here. I don’t know if you have any ideas about similar implementations. If you have a better solution, please leave a message for discussion.
The complete code is available at: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/un_bounded_listview.dart