If you had asked me this question before WRITING this article, I would have answered: I don’t care how it rolls, as long as it rolls! Since WRITING this article, my answer (meaningfully) is: This is how it rolls…

1. Let me scare you

There are three trees in the Flutter framework: Widget Tree, Element Tree and RenderObject Tree. So today we are going to talk about layer tree. (WTF?

1.1 Why Layer Tree?

We know that the real rendering of flutter is done by the RenderObject, so where does the rendering end up? Obviously the Layer Tree. In the process of debugging APP, we can print information on the console through debugDumpLayerTree() method to view the layer Tree generated at last.

1.2 Layer classification

There are not many types of layers, let’s talk about a few common layers, the rest of the layer readers can look at the documentation to understand.

The TransformLayer inherits the OffsetLayer and is the root node of the Layer tree. The Layer, as its name implies, does matrix transformations.

OffsetLayer inherits from ContainerLayer and can contain children, mainly used for offsets.

PictureLayer and TexttureLayer are the two layers that actually draw the display content. One is the layer that shows the colors, borders, images, shapes, etc., and the other is the layer that draws the text.

The ClipRectLayer is responsible for clipping the sublayers.

1.3 When did Layer play?

Say below.

Okay, that’s easy. I don’t think you’re freaked out. 😣

The above things are not important for the time being, you can put it down first. 🤣

2. First plate axe

Scrolling is a form of animation that uses visual persistence to continuously update the position of a view to achieve scrolling effects. To understand how a ListView scrolls, let’s look at a simpler type of scrolling.

2.1 How does SingleChildScrollView scroll?

SingleChildScrollView: SingleChildScrollView: SingleChildScrollView: SingleChildScrollView: SingleChildScrollView: SingleChildScrollView (source code)

Start with the widget level

class SingleChildScrollView extends StatelessWidget { ... @overrideWidget build(BuildContext context) { final Scrollable scrollable = Scrollable( dragStartBehavior: dragStartBehavior, axisDirection: axisDirection, controller: scrollController, physics: physics, restorationId: restorationId, viewportBuilder: (BuildContext context, ViewportOffset offset) { return _SingleChildViewport( axisDirection: axisDirection, offset: offset, child: contents, clipBehavior: clipBehavior, ); }); retrun scrollable; }... }Copy the code

SingleChildScrollView is a StatelessWidget and in its build method we see Scrollable and viewportBuilder. Let’s take a look at Scrollable.

Scrollable is a StatefulWidget so let’s look directly at the build method corresponding to state.

class ScrollableState ... {... @overrideWidget build(BuildContext context) { Widget result = _ScrollableScope( scrollable: this, position: position, // TODO(ianh): Having all these global keys is sad. child: Listener( onPointerSignal: _receivedPointerSignal, child: RawGestureDetector( key: _gestureDetectorKey, gestures: _gestureRecognizers, behavior: HitTestBehavior.opaque, excludeFromSemantics: widget.excludeFromSemantics, child: Semantics( explicitChildNodes: ! widget.excludeFromSemantics, child: IgnorePointer( key: _ignorePointerKey, ignoring: _shouldIgnorePointer, ignoringSemantics: false, child: widget.viewportBuilder(context, position), ), ), ), ), ); return result; // The source code is not like this}... }Copy the code

We see that the RawGestureDetector handles gesture recognition. Then we notice that the child in the innermost Widget calls widget.viewPortBuilder (). There’s an important argument here: position (context: I’m important too!) .

All right, it’s time to check out the viewportBuilder. In the first block of code we see viewportBuilder returned _SingleChildViewPort, we check found it inherits SingleChildRenderObjectWidget shows that the key is not here, It should be in the corresponding RenderObject, look at the source code, it does not do anything, just create, update the corresponding RenderObject. So let’s look at the corresponding RenderObject.

So here’s the RenderObject. (Ok, I used to get lost in widgets and RenderObjects)

class _RenderSingleChildViewport extends RenderBox ... {... @overridevoid attach(PipelineOwner owner) { super.attach(owner); _offset.addListener(_hasScrolled); } @overridevoid detach() { _offset.removeListener(_hasScrolled); super.detach(); } void _hasScrolled() { markNeedsPaint(); . } @overridevoid paint(PaintingContext context, Offset offset) { if (child ! = null) { final Offset paintOffset = _paintOffset; void paintContents(PaintingContext context, Offset offset) { context.paintChild(child, offset + paintOffset); } if (_shouldClipAtPaintOffset(paintOffset) && clipBehavior ! = Clip.none) { context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintContents, clipBehavior: clipBehavior); } else { paintContents(context, offset); }}}... }Copy the code

We analyze the _RenderSingleChildViewPort. The _offset parameter is position. The _offset parameter is position. The _offset parameter is position. . Sensitive to the fact that these two variables are of different types ([clever]), LET’s see what a ScrollPosition is:

The parent of ScrollPosition is ViewportOffset (type _offset) and inherits from ChangeNotifier.

RenderObject attaches when building the render Tree. _offset adds a listener _hasScrolled to enable redraw (markNeedsPaint()).

Moving on to the paint method, the paint method itself takes an offset argument and records a paintOffset variable internally. Offset is its parent’s offset, so the paintOffset should be the offset of the RenderObject. The following code is also very simple, is to judge and draw. According to the judgment condition method name can be roughly guessed is: if you need to cut offset, to cut what to draw, otherwise directly draw. When the internal scrollable Widget view is rolled out of the scope of the SingleChildScrollView, it needs to be clipped, and when the scroll content is not large enough to scroll, it needs to be drawn directly.

SingleChildScrollView: SingleChildScrollView: SingleChildScrollView

3. Second plate axe

3.1 Guess ListView scrolling.

The basic principle of scrolling I have been thoroughly familiar with the heart, and through SingleChildScrollView source code and practice to verify, ListView rolling principle is not so easy?

To verify ListView scrolling, I wrote my own widget and renderObject (copy _RenderColoredBox 😏) and printed a log in the paint method. Looking forward to printing something as I scroll through the ListView. But nothing happens! Well, it actually prints once, just once.

I feel cheated of my feelings! (Don’t piss off a serious person)

There must be something wrong. So where did it start to go wrong?

Finger touch screen movement produces slippage, GuestureDetecture should have no problem. It should also be necessary to monitor gesture slides and calculate position, which is notified when ChangeNotifier changes. That should be fine, too. The notification will trigger redraw, there is a problem, no redraw! So we need to see what happens in the RenderObject of the Render ListView where we add a listener to position(_offset).

According to the front of the experience we can directly find rendering ListView corresponding RenderObject RenderShrinkWrappingViewPort/RenderViewPort but this two RenderObject didn’t Attach method, so we have to go to their father’s

Again, RenderObject

abstract class RenderViewportBase { ... @overridevoid attach(PipelineOwner owner) { super.attach(owner); _offset.addListener(markNeedsLayout); }... @overridevoid paint(PaintingContext context, Offset offset) { if (firstChild == null) return; if (hasVisualOverflow && clipBehavior ! = Clip.none) { context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintContents, clipBehavior: clipBehavior); } else { _paintContents(context, offset); } } void _paintContents(PaintingContext context, Offset offset) { for (final RenderSliver child in childrenInPaintOrder) { if (child.geometry! .visible) context.paintChild(child, offset + paintOffsetOf(child)); }}... @protecteddouble layoutChildSequence({... {...}) while (child ! = null) { ... child.layout(SliverConstraints(...) ); . }... }}Copy the code

There is a problem with the listener added to _offset(position). Instead of drawing the marker, the marker needs to be laid out. Okay, we’re going to look at layout.

Let’s look at RenderViewPort, let’s do things that have a lot of parameters, complex parameters, simple parameters.

class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentData> { ... @overridevoid performLayout() { ... double correction; int count = 0; do { ... correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment); . } while (count < _maxLayoutCycles); // static const int _maxLayoutCycles = 10; . }... double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) { ... // positive scroll offsets return layoutChildSequence(...) ; }}Copy the code

We know that the Layout procedure actually calls the performLayout method, and the point is to call the two methods and then go to the parent layoutChildSequence method. RenderViewPortBase (RenderViewPortBase) : RenderViewPortBase (RenderViewPortBase) : RenderViewPortBase (RenderViewPortBase)

I’m sick of it. I don’t want to hear it.

I want to know why the ListView will scroll. I haven’t drawn paint yet, so I’m confused about layout. And I don’t know what a child is… “I think it’s paint! [😤]

Okay, let’s go to paint.

Paint is implemented in the parent class RenderViewPortBase, so let’s take a look. The code is the next-to-last block up here.

1. The feeling of familiarity is coming. Paint does a clipping judgment, and then it calls _paintContents, and _paintContents calls context.paintChild to do the drawing, So let’s look at PaintContext’s paintChild method.

void paintChild(RenderObject child, Offset offset) {
    ...
    if (child.isRepaintBoundary) {  
        stopRecordingIfNeeded();  
        _compositeChild(child, offset);
    } else {  
        child._paintWithContext(this, offset);
    } 
    ...
}

void _compositeChild(RenderObject child, Offset offset) {
    if (child._needsPaint) {  
        repaintCompositedChild(child, debugAlsoPaintedParent: true);
    }
    final OffsetLayer childOffsetLayer = child._layer as OffsetLayer;
    childOffsetLayer.offset = offset;
    appendLayer(child._layer!); 
}
Copy the code

So we see that paintChild method is very simple, so make a judgment call, call a method, and since isRepaintBoundary is kind of unfamiliar, let’s just do else, and then we’re going to follow it into the RenderObject class

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin ... {... void _paintWithContext(PaintingContext context, Offset offset) { ... paint(context, offset); . }... }Copy the code

Paint is called directly in the _paintWithContext method in the RenderObject! If I call paint directly every time, why doesn’t my paint method print a log!

If you go wrong, try again.

So child.isRepaintBoundary is true and then stop recording and composing child and look at _compositeChild. I found something exciting, OffsetLayer! This thing front said, is to do offset with, rolling is not offset! I have a bold idea 😏

The ListView Widget calls paint when the item is first rendered, draws itself onto the layer, and then simply adjusts the OffsetLayer when scrolling!

4. The third plate axe

Well, now that you know how the ListView scrolls, are you sure?

4.1 Confirm that this is how the ListView scrolls

This is a bit lazy, the widget part is not the source code, readers can view, LET me focus on.

ListView inherits from BoxScrollView, and BoxScrollView inherits from ScrollView. ScrollView is a StatelessWidget whose build method constructs a Scrollable that contains a viewPortBuilder. The buildViewPort method takes a parameter, slivers, that is returned by the buildSlivers method declared in the Scrollable class and implemented in BoxScrollView. The buildSlivers method calls the buildChildLayout method declared in BoxScrollView and implemented in ListView(in this case). The buildChildLayout method returns SliverList or SliverFixedExtentList. SliverList, SliverList createRenderObject returns RenderSliverList.

So we’re going to pick up where we left off, and we’re going to talk about RenderSliverList. Which back? Layout muddled that back! It’s not for nothing!

We know that when we build our ViewPort we pass in slivers, and in this case slivers are [RenderSliverList]. When the RenderObject tree is constructed during the build process, it is inserted into the tree as a child node of the RenderViewPort. So the Child. layout called in the layoutChildSequence is the layout method that RenderSliverList corresponds to. And then we go to performLayout.

class RenderSliverList extends RenderSliverMultiBoxAdaptor { ... @override void performLayout() { ... 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); . }... }Copy the code

The performLayout method is nearly 300 lines of code, and it’s a pain in the neck (although it has a lot of comments). Geometry is used to describe the space occupied by RenderSliver(List).

With layout done, it’s time to look at Paint. This is the parent class of RenderSliverList.

abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ... {... @overridevoid paint(PaintingContext context, Offset offset) { ... Offset mainAxisUnit, crossAxisUnit, originOffset; bool addExtent; switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, Constraints. GrowthDirection)) {case AxisDirection. Up: mainAxisUnit = const Offset (0.0, 1.0); CrossAxisUnit = const Offset(1.0, 0.0); OriginOffset = offset + offset (0.0, geometry! .paintExtent); addExtent = true; break; . } RenderBox? child = firstChild; while (child ! = null) { final double mainAxisDelta = childMainAxisPosition(child); final double crossAxisDelta = childCrossAxisPosition(child); Offset childOffset = Offset( originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta, originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta, ); if (addExtent) childOffset += mainAxisUnit * paintExtentOf(child); // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) { context.paintChild(child, childOffset); } child = childAfter(child); }}... }Copy the code

Here you can see the paint method, which uses geometry to calculate the offset based on the principal axis. Then loop over the child.

4.2 A bit of a mess?

RenderViewPort paint: RenderSliverList paint: RenderViewPort paint: RenderSliverList paint: RenderViewPort paint: RenderSliverList paint: RenderViewPort paint Let’s go through the paint process.

Where should I start? Let’s look at a field, because we went the wrong way back once, because we had this thing called isRepaintBoundary. This field is important! It’s responsible for repainting! The page of the flutter app can be partially redrawn. The starting point of the redrawing is the isRepaintBoundary marked true with the RenderObject. Rendering performance optimization is often involved here as well.

This illustration shows some renderobjects that have been overwritten by isRepaintBoundary. The default value of isRepaintBoundary is false. Overwriting of course returns true. So our RenderViewPort is a “redraw boundary”.

For easy viewing, I re-insert some of the source code already inserted above.

abstract class RenderViewportBase<ParentDataClass ... {... @overridevoid paint(PaintingContext context, Offset offset) { if (firstChild == null) return; if (hasVisualOverflow && clipBehavior ! = Clip.none) { context.pushClipRect(needsCompositing, offset, Offset.zero & size, _paintContents, clipBehavior: clipBehavior); } else { _paintContents(context, offset); } } void _paintContents(PaintingContext context, Offset offset) { for (final RenderSliver child in childrenInPaintOrder) { if (child.geometry! .visible) context.paintChild(child, offset + paintOffsetOf(child)); }}... }Copy the code

You can see that the paint method of RenderViewPort(Base) calls context.PaintChild.

class PaintingContext extends ClipContext {
    ...
    void paintChild(RenderObject child, Offset offset) {
        if (child.isRepaintBoundary) {  
            stopRecordingIfNeeded();  
            _compositeChild(child, offset);
        } else {  
            child._paintWithContext(this, offset);
        }
    }

    void _compositeChild(RenderObject child, Offset offset) {
        // Create a layer for our child, and paint the child into it.
        if (child._needsPaint) {  
            repaintCompositedChild(child, debugAlsoPaintedParent: true);
        }

        final OffsetLayer childOffsetLayer = child._layer as OffsetLayer;
        childOffsetLayer.offset = offset;
        appendLayer(child._layer!);
    }
    ...
}
Copy the code

The paintChild here is the RenderSliverList. We looked at the framework’s “ReapintBoundary” earlier and didn’t see anything related to RenderSliver, so it wasn’t. So let’s go to the _paintWithContext method of RenderSliverList, which is implemented in RenderObject.

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin ... {... void _paintWithContext(PaintingContext context, Offset offset) { ... paint(context, offset); . }... }Copy the code

As you can see, the paint method is called. So it’s time to look at the paint method for RenderSliverList. Insert code repeatedly…

abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ... {... @overridevoid paint(PaintingContext context, Offset offset) { ... Offset mainAxisUnit, crossAxisUnit, originOffset; bool addExtent; switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, Constraints. GrowthDirection)) {case AxisDirection. Up: mainAxisUnit = const Offset (0.0, 1.0); CrossAxisUnit = const Offset(1.0, 0.0); OriginOffset = offset + offset (0.0, geometry! .paintExtent); addExtent = true; break; . } RenderBox? child = firstChild; while (child ! = null) { final double mainAxisDelta = childMainAxisPosition(child); final double crossAxisDelta = childCrossAxisPosition(child); Offset childOffset = Offset( originOffset.dx + mainAxisUnit.dx * mainAxisDelta + crossAxisUnit.dx * crossAxisDelta, originOffset.dy + mainAxisUnit.dy * mainAxisDelta + crossAxisUnit.dy * crossAxisDelta, ); if (addExtent) childOffset += mainAxisUnit * paintExtentOf(child); // If the child's visible interval (mainAxisDelta, mainAxisDelta + paintExtentOf(child)) // does not intersect the paint extent interval (0, constraints.remainingPaintExtent), it's hidden. if (mainAxisDelta < constraints.remainingPaintExtent && mainAxisDelta + paintExtentOf(child) > 0) { context.paintChild(child, childOffset); } child = childAfter(child); }}... }Copy the code

The childOffset offset is computed by geometry here to plot the Child.

So here we go back to PaintingContext, source code reference. Notice that child here is now “item”. Suddenly, there is a problem! Previously, it was said that child was RepaintBoundary, but I didn’t know it! Well, the system does that for you. ** The ListView item has a RepaintBoundary wrapped around it. ** so we walk _compositeChild. The first one is definitely going to be drawn, so let’s go to repaintCompositedChild.

class PaintingContext extends ClipContext { ... static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) { _repaintCompositedChild( child, debugAlsoPaintedParent: debugAlsoPaintedParent, ); } static void _repaintCompositedChild( RenderObject child, { bool debugAlsoPaintedParent = false, PaintingContext? childContext, }) { OffsetLayer? childLayer = child._layer as OffsetLayer? ; if (childLayer == null) { child._layer = childLayer = OffsetLayer(); } childContext ?? = PaintingContext(child._layer! , child.paintBounds); child._paintWithContext(childContext, Offset.zero); childContext.stopRecordingIfNeeded(); }... }Copy the code

You can see that in repaintCompositedChild, _repaintCompositedChild is called. So it’s going to do that, if the childLayer is null it’s going to create a new layer for the child, so OffsetLayer. Then call child._paintwithContext, notice that _needsPaint is set to false in renderObject._paintwithContext, and then call paint to draw.

All right, summary.

The parent Widget of the ListView, ViewPort, is “draw boundary”. This is where you start every time something in the ListView changes and needs to be redrawn. RenderViewPort.paint -> … -> RenderSliverList.paint -> … – > PaintingContext _compositeChild. Build layer Tree in _compositeChild. When the item(“RepaintBoundary”) needs to draw for the first time, it creates its own layer and sets _needsRepaint to false before calling paint. The subsequent _compositeChild process offsets only the layer(OffsetLayer).

4.3 Question?

What happens when a child Widget of the ListView slides off or onto the screen?

This problem is handled in the 300 lines of performLayout. There child renderObjects are added and removed to calculate the offset of the new RenderObject.

4.4 Important, ListView reuse mechanism analysis

I suddenly realize that this problem has little to do with scrolling, so let’s start another explanation.

4.5 tip

When we talked about ListView’s buildChildLayout earlier, we saw that the source code returned SliverList or SliverFixedExtentList. It is distinguishable by name, one is a “fixed range” List and one is not. When I say “fixed extent,” I mean itemExtent, which is whether the range (width, height) of each item in the list is fixed. SliverFixedExtentList Is optimized to reduce layout calculations because itemExtent specifies the specific size of the item in the main axis. When we present list data, the entries are usually of equal height (width). Therefore, it is highly recommended that we provide the itemExtent parameter when creating the ListView and choose better widgets to improve the rendering speed of our APP.

5. At last.

There’s no summary. That’s it.