preface
It has to be said that the speed of Flutter drawing UI is not of the same magnitude as that of native. Flutter drawing UI is much faster than native ListView controls. Native writing, such as Android, requires an Adapter, ViewHolder, Add an XML layout file, and a Flutter can be dozens of lines.
The more you use a control, the more familiar you are with its principles. The family of ScrollViews in Flutter is very similar to Android, except for ListView and GridView. There are also CustomScrollView and NestedScrollView. Today we’re going to talk about ListView.
ListView
ListView and GridView both inherit from BoxScrollView, but this is not the place for drawing and layout. Flutter is not the same as native. Take Android for example. Flutter is a bit more complicated. First, we use most widgets, such as ListView, but a Widget can be interpreted as a configuration description file, such as the following code:
Container {
width: 100,
height: 100,
color: Colors.white,
}
Copy the code
Here it says that we need a container with a width of 100 and a height of 100 and a color of white, and then we actually draw the RenderObject. In between the Widget and RenderObject is an Element that converts our configured Widget Tree into an Element Tree. Element is a further abstraction of the Widget. Element has two subclasses. One is RenderObjectElement, which holds the RenderObject, and one ComponentElement, which combines multiple RenderObjectElement. This is the core of Flutter UI. Understand these three classes well.
Back to our theme, we said that the ListView inherits from the BoxScrollView, which in turn inherits from the ScrollView, which is a StatelessWidget, It relies on Scrollable for scrolling, and the widgets in the scrollcontainer are called slivers. Sliver is used to consume rolling events.
@override
Widget build(BuildContext context) {
// slivers
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
/ / to omit
returnprimary && scrollController ! =null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
Copy the code
BoxScrollView implements buildSlivers(), which has only one sliver, that is, one consumer in the scrolling container. Again, this is created by calling the buildChildLayout abstract method.
@override
List<Widget> buildSlivers(BuildContext context) {
// buildChildLayout
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
/ / to omit
return <Widget>[ sliver ];
}
Copy the code
Finally, our ListView implements buildChildLayout() :
@override
Widget buildChildLayout(BuildContext context) {
if(itemExtent ! =null) {
// If the subterm is fixed height
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
// Default
return SliverList(delegate: childrenDelegate);
}
Copy the code
The SliverList is a RenderObjectWidget. As we mentioned above, RenderObject is responsible for rendering and layout. ListView is no exception:
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context;
return RenderSliverList(childManager: element);
}
Copy the code
RenderSliverList is the core implementation of ListView and the focus of this article.
RenderSliverList
Those of you who have experience with Android custom controls will know that when we customize a control, we generally involve these steps: Measure and Draw also involve layout processes if they are custom viewgroups. Flutter is no exception, but it incorporates measure and layout into layout. Draw is called Paint. They call it different, but they do the same thing. The system calls performLayout() to do the measurement and layout, and RenderSliverList is all about layout, so we’ll just focus on this method.
The performLayout() code is long, so we’ll omit some non-core code.
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;
final BoxConstraints childConstraints = constraints.asBoxConstraints();
Copy the code
ScrollOffset represents the offset that has been rolled, and cacheOrigin represents the relative position of the pre-layout. For better visual effect, the ListView adds a pre-laid area to the visible area, which represents the area to be displayed in the next scroll, called cacheExtent. This value is configurable and defaults to 250.
// viewport.dart
double get cacheExtent => _cacheExtent;
double _cacheExtent;
set cacheExtent(double value) {
value = value ?? RenderAbstractViewport.defaultCacheExtent;
assert(value ! =null);
if (value == _cacheExtent)
return;
_cacheExtent = value;
markNeedsLayout();
}
static const double defaultCacheExtent = 250.0;
Copy the code
RemainingCacheExtent is the offset currently available to the sliver, which contains the pre-laid area. Here we use a very crude picture to explain.
The C area represents our screen, which we consider to be the visible area. In practice, it might be smaller because the ListView might have some padding, magin, or other layout. Area B has two pre-layout areas for the header and A pre-layout area for the bottom, which is our cacheExtent value, and area A has A reclaim area.
final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
Copy the code
Here, the constraints. ScrollOffset is A + B, and the region is gone. In this case, if you use the default, it is equal to -250, which means that the region of B has a height of 250, so when it is completely invisible, its relative y value is -250. The scrollOffset calculated here is where the layout starts. If cacheExtent = 0, it will start at the top of C, That is, constraints. ScrollOffset. Otherwise, constraints. ScrollOffset + constraints. CacheOrigin.
if (firstChild == null) {
// If there are no children
if(! addInitialChild()) {// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return; }}// There is at least one child
// trailing
RenderBox leadingChildWithLayout, trailingChildWithLayout;
// Find the last child that is at or before the scrollOffset.
RenderBox earliestUsefulChild = firstChild;
for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
earliestScrollOffset > scrollOffset;
earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
// Insert new children in the header
earliestUsefulChild =
insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
if (earliestUsefulChild == null) {
final SliverMultiBoxAdaptorParentData childParentData = firstChild
.parentData;
childParentData.layoutOffset = 0.0;
if (scrollOffset == 0.0) { earliestUsefulChild = firstChild; 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 = SliverGeometry(
scrollOffsetCorrection: -scrollOffset,
);
return; }}final double firstChildScrollOffset = earliestScrollOffset -
paintExtentOf(firstChild);
if (firstChildScrollOffset < -precisionErrorTolerance) {
// Double error
}
final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild
.parentData;
/ / update the parentData
childParentData.layoutOffset = firstChildScrollOffset;
assert(earliestUsefulChild == firstChild);
// Update the header and tailleadingChildWithLayout = earliestUsefulChild; trailingChildWithLayout ?? = earliestUsefulChild; }Copy the code
The code above deals with the case that there is space between the children and scrollOffset in the head, and no padding. Let me draw a simple graph.
This area is called needLayout. When you scroll from the bottom up, this is where the layout happens.
bool inLayoutRange = true;
RenderBox child = earliestUsefulChild;
int index = indexOf(child);
// endScrollOffset indicates the offset of children that is currently laid out
double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
bool advance() {
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) {// Need to layout the new children, insert a new one at the end
child = insertAndLayoutChild(childConstraints,
after: trailingChildWithLayout,
parentUsesSize: true,);if (child == null) {
// We have run out of children.
return false; }}else {
// Lay out the child.
child.layout(childConstraints, parentUsesSize: true);
}
trailingChildWithLayout = child;
}
assert(child ! =null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.layoutOffset = endScrollOffset;
assert(childParentData.index == index);
// Update endScrollOffset with the current child's offset + the range required by the child
endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
return true;
}
Copy the code
// Find the first child that ends after the scroll offset.
while (endScrollOffset < scrollOffset) {
// Record items that need to be recycled
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 = SliverGeometry(
scrollExtent: extent,
paintExtent: 0.0,
maxPaintExtent: extent,
);
return; }}Copy the code
Not in the visible view, not in the cache area, the record header needs to be reclaimed.
// Now find the first child that ends after our end.
while (endScrollOffset < targetEndScrollOffset) {
if(! advance()) { reachedEnd =true;
break; }}Copy the code
As you scroll down, the call advance() keeps inserting new children at the bottom.
// 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); }}/ / recycling
collectGarbage(leadingGarbage, trailingGarbage);
Copy the code
Record the tail to be recycled, all together. The area marked by Nedd Grabage in the image above.
double estimatedMaxScrollOffset;
if (reachedEnd) {
// No child needs to be laid out
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;
// Feedback the layout consumption request
geometry = 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,);// The layout is complete
childManager.didFinishLayout();
Copy the code
conclusion
After analyzing the ListView layout process, we can find that the whole process is relatively clear.
- The areas that need to be laid out include the visible area and the cache area
- Recycle outside of the layout area