How Slivers are made
The best way to learn how to write your own Sliver
is to know how existing Slivers
are made.
I would split Slivers
into groups by their complexity:
Easy
Single child Slivers
SliverToBoxAdapter
— enables inserting a normal boxWidgets
as aSliver
SliverFillRemaining
— fills remaining available spaceSliverPadding
— allows adding padding to existingSlivers
Medium
Slivers
with one child that provide a special effect
SliverPersistentHeader
— display floatingWidget
on top of all the otherSlivers
Hard
Slivers
containing multiple children
SliverList
—Sliver
containing multipleWidgets
To understand Slivers
, you need to understand Sliver
constraints and geometry.
SliverConstraints & SliverGeometry
Usually, you are using Widgets that are based on RenderBox
— to layout them on-screen, BoxConstraints
are passed to RenderBox
and its size is calculated.
Slivers
are more complicated — RenderBox
widgets do not care where there are on the screen, but slivers do. They need to know where they are in the scrollable space.
Slivers use SliverConstraints
(RenderBox
uses BoxConstraints
) as inputs and calculate SliverGeometry
(RenderBox
calculates size).
Writing your first sliver: SliverToBoxAdapter
SliverToBoxAdapter
simply adapts Widgets
that are based on RenderBox
, to a Sliver
that you can use in a scroll view.
Every Sliver
consists of aWidget
and aRenderSliver
.
For SliverToBoxAdapter
, the widget part is pretty simple — the crucial part is creating RenderSliver
, in this case, RenderSliverToBoxAdapter
.
class SliverToBoxAdapter extends SingleChildRenderObjectWidget { const SliverToBoxAdapter({
Key key,
Widget child,
}) : super(key: key, child: child); @override
RenderSliverToBoxAdapter createRenderObject(BuildContext context) => RenderSliverToBoxAdapter();}
The interesting part is RenderSliverToBoxAdapter
which has only one method performLayout
. This is the full source code, but let’s go thru it line by line later:
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
final SliverConstraints constraints = this.constraints; child.layout(constraints.asBoxConstraints(), parentUsesSize: true); final double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child!.size.width;
break;
case Axis.vertical:
childExtent = child!.size.height;
break;
}
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
); setChildParentData(child!, constraints, geometry!);
}
Let’s diagnose it line by line:
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
In case there is no child, we skip all the calculations.
child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
We perform layout on the child using SliverConstraints
, and they are converted into BoxConstraints
.
We specify that the parent uses a child’s size — if the child resizes, then the sliver itself will redo layout calculation.
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child.size.width;
break;
case Axis.vertical:
childExtent = child.size.height;
break;
}
Depending on the orientation of the sliver, we calculate childExtent
which is the space that the child should take — if the list is vertical, then it’s the child’s height.
final double paintedChildSize = calculatePaintOffset(constraints, from: 0.0, to: childExtent);
Here we are calculating the visible child’s size. In case the child is only 50% visible, this value will be 50% of childExtend
’s.
This value is assigned to Geometry.paintExtent
(interactive example)
final double cacheExtent = calculateCacheOffset(constraints, from: 0.0, to: childExtent);
To understand cacheExtent
we need to talk about viewport caching.
In the image
- The yellow space within the black frame is an area populated by all the Slivers.
- The red space is a visible area — to see what is in the yellow space, you need to scroll.
- The green space is the caching area.
Slivers
in the yellow area are not rendered as that would be wasteful.
When scrolling the visible area (red) we need to layout and paint not visible Slivers
— we need to give them enough time for the necessary calculation — therefore we start to load them a bit earlier. Whenever a Slivers
enters the cache area (green), it will be laid out even if they are not yet visible.
The Geometry.cacheExtent
(interactive example) property states how much of the sliver is in the cache (green) area.
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,
);
scrollExtent
(interactive example) — is the size of the child, it does not change, it our case when scrollingpainExtent
(interactive example) — the visible size of the childcacheExtent
(interactive example) — as we mentioned previously, this is the number of pixels of the child within the cache areamaxPaintExtent
(interactive example)- property used when using shrink-wrapping on a scroll view, in our case, it’s always the size of the childhitTestExtent
(interactive example) — specifies where the child accepts touch eventshasVisualOverflow
(interactive example) — specifies if the child is fully visible — if the whole child is visible then it returnsfalse
. If even a pixel is outside the viewport, then returnstrue
setChildParentData(child, constraints, geometry);
Applies geometry to the underlying render object.
Coming up
In the next article, we are implementing more complex SliverFillRemaining
.
* In RenderBox
you need to return intrinsic min/max dimensions but that we can simplify the concept for the purpose of this article