The list component is particularly important on mobile devices. Sliver is an important part of the Flutter list component. It is important for developers to understand the principles and usage of Sliver.
Two types of layouts
The layout of Flutter can be divided into two types:
- Box (RenderBox): Draws layouts in 2D
- RenderSliver: Scroll layout
Important concepts
Sliver
A Sliver is a concept within a Flutter that represents part of a scrollable layout, and its child can be a normal Box Widget.
ViewPort
- A ViewPort is a display window that can contain multiple slivers;
- The width and height of a ViewPort is determined, and the sum of the width and height of its internal Slivers can be greater than its own width and height;
- ViewPort uses lazy loading to improve performance and only draws visual area content widgets.
ViewPort has some important properties:
class Viewport extends MultiChildRenderObjectWidget {
/// main axis
final AxisDirection axisDirection;
/// vertical direction
final AxisDirection crossAxisDirection;
/// Center determines the zero base of the viewport, i.e. where the viewport is drawn from
/// Center must be the key of a member of the viewport slivers
final Key center;
/// the anchor point, which takes the value [0,1], is relative to zero. For example, 0.5 means zero is placed at viewport.height / 2
final double anchor;
// The cumulative value of scrolling, exactly where does viewPort start
final ViewportOffset offset;
/// Cache area, i.e. the height of the preload relative to the head and tail
final double cacheExtent;
/// children widget
ListThe < widgets > slivers; }Copy the code
A picture is worth a thousand words:
In the figure above, assuming that the height of each sliver is the same and equal to ⅕ of the screen height, so that center = sliver1, the first screen display should be sliver1, but since anchor = 0.2, Viewport. height is exactly equal to the height of sliver1, so the first one shown on the screen is Sliver 2.
ScrollPostion
ScrollPosition determines which areas of the Viewport are visible. It contains the scrolling information of the Viewport. Its main member variables are as follows:
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
// Roll offset
double _pixels;
// Set the scrolling response effect, such as animation after sliding stops
final ScrollPhysics physics;
// Save the current scrolling offset to PageStore, which can be restored to the current offset after Scrollable is rebuilt
final bool keepScrollOffset;
// Minimum scroll value
double _minScrollExtent;
// Maximum scroll value
double_maxScrollExtent; . }Copy the code
The class inheritance of ScrollPosition is as follows:
|-- Listenable
|---- ChangeNotifier
|------ ScrollPosition
|-------- ScrollPositionWithSingleContext
Copy the code
So the ScrollPosition can act as the observed, notifying the observer when data changes.
Scrollable
Scrollable is a Scrollable Widget that is responsible for:
- Monitor the gesture of the user, calculate the scrolling state and send Notification
- Offset notices are calculated
Scrollable itself does not have the ability to draw content. It creates a Viewport to display content by constructing an injected viewportBuilder. When the scrolling state changes, Scrollable will constantly update the Viewport offset. The Viewport is constantly updating the display.
The main structure of Scrollable is as follows:
Widget result = _ScrollableScope( scrollable: this, position: position, child: Listener( onPointerSignal: _receivedPointerSignal, child: RawGestureDetector( gestures: _gestureRecognizers, ... , child: Semantics( ... child: IgnorePointer( ... child: widget.viewportBuilder(context, position), ), ), ), ), );Copy the code
- _ScrollableScope inheritedWidgets so that its children can easily access scrollable and Position;
- The RawGestureDetector is responsible for monitoring gestures. When gestures change, the RawGestureDetector can call back _gestureRecognizers.
- ViewportBuilder generates viewPort;
SliverConstraints
Similar to Box layout, which uses BoxConstraints as constraints, Sliver layout uses SliverConstraints as constraints, but is much more complex than Box. It can be understood that SliverConstraints describes the layout information between a Viewport and its internal Slivers:
class SliverConstraints extends Constraints {
// in the main direction
final AxisDirection axisDirection;
// Window growth direction
final GrowthDirection growthDirection;
/ / if the Direction is AxisDirection down, scrollOffset represent silver top slide over the value of the top viewport, ScrollOffset is 0 if you haven't slid over the top of the viewport.
final double scrollOffset;
// the last sliver covers the size of the next sliver (only valid if the last sliver is pinned/floating)
final double overlap;
// It's the sliver's turn to draw, and the viewport tells the sliver how much space is left to draw, depending on the size of the viewport
final double remainingPaintExtent;
// viewport the size on the main axis
final double viewportMainAxisExtent;
// cacheOrigin starts (relative to scrolloffSET). If cacheExtent is set to 0, cacheOrigin remains 0
final double cacheOrigin;
// The size of the remaining cache
final doubleremainingCacheExtent; . }Copy the code
SliverGeometry
Viewport uses SliverConstraints to tell its internal Sliver about its constraints, such as how much space is available, offsets, etc. Sliver then feeds back to the Viewport how much space it needs to occupy through SliverGeometry.
class SliverGeometry extends Diagnosticable {
// The extent to which sliver can be scrolled, which can be considered the height of the sliver (if axisdierction.down)
final double scrollExtent;
// The starting point (default: 0.0) is relative to the starting point of the sliver layout. It does not affect the layoutExtent of the next sliver, but the paintExtent of the next sliver
final double paintOrigin;
// Draw the range
final double paintExtent;
// Layout range, the distance from the top of the current sliver to the top of the next sliver. The range is [0,paintExtent]. The default is paintExtent, which affects the layout of the next sliver
final double layoutExtent;
// Maximum drawing size, must be >= paintExtent
final double maxPaintExtent;
// If the sliver is pinned to the boundary, this size is the height of the sliver, in other cases it is 0, such as the app bar
final double maxScrollObstructionExtent;
// Click on the size of the valid area. The default is paintExtent
final double hitTestExtent;
// Whether visible = (paintExtent > 0)
final bool visible;
// Do you need to do clip to prevent chidren overflow
final bool hasVisualOverflow;
. / / the current silver occupies SliverConstraints remainingCacheExtent how many pixels
final doublecacheExtent; . }Copy the code
Sliver layout process
RenderViewport in layout its internal slivers process is as follows:
The layout process is a top-down linear process:
- Type SliverConstrains1 to sliver1 and get the output (SliverGeometry1),
- Generate a new SliverConstrains2 input to sliver2 from SliverGeometry1 to get SliverGeometry2
- …
- See RenderViewport’s layoutChildSequence method for details up to the last sliver.
ScrollView
Using ScrollView as an example, let’s concatenate the relationships between the widgets described above. ScrollView build method
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController scrollController = primary
? PrimaryScrollController.of(context)
: controller;
final Scrollable scrollable = Scrollable(
...
controller: scrollController,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
returnbuildViewport(context, offset, axisDirection, slivers); });returnprimary && scrollController ! =null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
Copy the code
You can see that ScrollView creates a Scrollable and passes in the buildViewPort method that builds the ViewPort. Scrollable builds views with buildViewPort, and updates the ViewPort as gestures change.
The custom in silver
CustomPinnedHeader is hard to understand just by looking at some of the concepts, but the best way is to debug it. We can copy the SliverToBoxAdapter code and customize a Sliver to debug the parameters.
class CustomSliverWidget extends SingleChildRenderObjectWidget {
const CustomSliverWidget({Key key, Widget child})
: super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
returnCustomSliver(); }}/ / / a StickPinWidget
This section focuses on the effects of the Sliveronstraints and the SliverGeometry parameter
class CustomSliver extends RenderSliverSingleBoxAdapter {
@override
void performLayout() {
...
// Convert SliverConstraints to BoxConstraints to layout the child
child.layout(constraints.asBoxConstraints(), parentUsesSize: true); .// Calculate the drawing size
final double paintedChildSize =
calculatePaintOffset(constraints, from: 0.0, to: childExtent);
// Calculate the cache size
final double cacheExtent =
calculateCacheOffset(constraints, from: 0.0, to: childExtent); ./ / output SliverGeometry
geometry = SliverGeometry(
scrollExtent: childExtent,
paintExtent: paintedChildSize,
cacheExtent: cacheExtent,
maxPaintExtent: childExtent,
paintOrigin: 0,
hitTestExtent: paintedChildSize,
hasVisualOverflow: childExtent > constraints.remainingPaintExtent || constraints.scrollOffset > 0.0,); setChildParentData(child, constraints, geometry); }}Copy the code
We put it in the CustomScrollView:
eturn Scaffold(
body: CustomScrollView(
slivers: <Widget>[
CustomSliverWidget(
child: Container(
color: Colors.red,
height: 100,
child: Center(
child: Text("CustomSliver"),
),
)),
_buildListView(),
],
));
Copy the code
The effect is as follows:
geometry = SliverGeometry(
...
paintOrigin: constraints.scrollOffset,
visible: true,);Copy the code
At this point you will notice that the CustomSliver can be fixed in the header:
We tried to modify the paintExtrent as follows:
geometry = SliverGeometry(
// Change the drawing range to the height of the sliverpaintExtent: childExtent, ... ) ;Copy the code
CustomRefreshWidget
Next, we create a simple drop-down refresh Widget that displays when pulled down and retracted when released:
class CustomRefreshWidget extends SingleChildRenderObjectWidget {
const CustomRefreshWidget({Key key, Widget child})
: super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
returnSimpleRefreshSliver(); }}/// A simple drop-down refresh Widget
class SimpleRefreshSliver extends RenderSliverSingleBoxAdapter {
@override
void performLayout() {
...
final bool active = constraints.overlap < 0.0;
/// The sliding distance of the head
final double overscrolledExtent =
constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
double layoutExtent = child.size.height;
print("overscrolledExtent:${overscrolledExtent - layoutExtent}");
child.layout(
constraints.asBoxConstraints(
maxExtent: layoutExtent + overscrolledExtent,
),
parentUsesSize: true,);if (active) {
geometry = SliverGeometry(
scrollExtent: layoutExtent,
/// Draw the starting position
paintOrigin: min(overscrolledExtent - layoutExtent, 0),
paintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
maxPaintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
/// layout placeholder
layoutExtent: min(overscrolledExtent, layoutExtent),
);
} else {
// If you don't want to display it, you can set it to zerogeometry = SliverGeometry.zero; } setChildParentData(child, constraints, geometry); }}Copy the code
You can see that there are three key parameters
- Overlap: List the distance between the top of the first Sliver and the top of the screen
- PaintOrigin: The initial drawing position of the RefreshWidget
- LayoutExtent: specifies the height of the RefreshWidget
The distance between the top of the items and the top of the screen is known as constraints. Overlap, which is a value less than or equal to 0.
- No operation overlap == 0, directly returns an empty Widget (SliverGeometry. Zero)
- Dropdown overlap < 0, then paintOrigin = min(overscrolledExtent – refreshWidget. height, 0) can pull down the RefreshWidget slowly.
- After the Paint is done, don’t forget to deal with the layout. The layoutExtent of the SliverGeometry will affect the layout of the next Sliver. LayoutExtent = min(-overlap, refreshWidget.height)
Scrolling Widget
Commonly used lists are as follows, which are divided into 3 categories according to the contents of packages:
ListView
ListView.builder(
itemCount: 50,
itemBuilder: (context,index) {
return Container(
color: ColorUtils.randomColor(),
height: 50,); }Copy the code
CustomScrollView
CustomScrollView(
slivers: <Widget>[
SliverAppBar(...),
SliverToBoxAdapter(
child:ListView(...),
),
SliverList(...),
SliverGrid(...),
],
)
Copy the code
NestedScrollView
The NestedScrollView is actually a CustomScrollView, and the headers is an array of slivers, and the body is wrapped in SliverFillRemaining, and the body accepts the Box.
NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 100,
pinned: true,
title: Text("Nest"),
),
SliverToBoxAdapter(
child: Text("second bar"),
)
];
},
body: ListView.builder(
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return Text("item: $index"); }));Copy the code
Why you designed the CustomScrollView
Complex list nesting
If you nested a ListView directly with a ListView, you will get an error:
Vertical viewport was given unbounded height.
During the Layout stage, the parent Listview cannot determine the height of the child Listview. This error can be fixed by setting inner Listview’s shrinkWrap = true (shrinkWrap = true means that the height of the Listview is equal to its content height).
ListView.builder(
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return ListView.builder(
shrinkWrap: true,
itemCount: 5,
itemBuilder: (BuildContext context, int index) {
return Text("item: $index");
});
});
Copy the code
However, this may cause poor performance because the internal list computs the height of all content each time, so using CustomScrollView is more appropriate:
CustomScrollView(
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => Container(...),
childCount: 50) ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => Container(...) , childCount:50)));Copy the code
Sliding effects
CustomScrollView allows its internal Slivers to be linked, such as a scalable TitleBar, headers that can be fixed in the middle area, drop-down refresh components, and so on.
Slivers
Flutter provides a number of Sliver components. Here’s what they do:
SliverAppBar
CollapsingToolbarLayout similar to Android CollapsingToolbarLayout, CollapsingToolbarLayout is collapsible according to slides and provides actions, bottom and other attributes to improve efficiency.
SliverList / SliverGrid
The usage is basically the same as ListView/GridView. In addition, ListView = SliverList + Scrollable, which means SliverList does not have the ability to handle sliding events, so it must be used in conjunction with CustomScrollView.
SliverFixedExtentList
It has more FixedExtent than SliverList, meaning that its items have a fixed height/width along the main axis.
It is designed to be used in scenarios where all items are the same height/width. It is more efficient than SliverList because it knows the scope of each item without going through the Layout procedure for the item.
ItemExtent must be passed when used:
SliverFixedExtentList(
itemExtent: 50.0, delegate: SliverChildBuilderDelegate( ... ) ; },),)Copy the code
SliverPersistentHeader
SliverPersistentHeader(
pinned: true,
delegate: ...,
)
Copy the code
SliverPersistentHeaderDelegate is an abstract class, and we need to implement it, its implementation is very simple, only four members must be implemented:
class CustomDelegate extends SliverPersistentHeaderDelegate {
/// Maximum height
@override
double get maxExtent => 100;
/// Minimum height
@override
double get minExtent => 50;
/// shrinkOffset: The distance between the top of the current sliver and the top of the screen
/// overlapsContent: Whether the content is still displayed below
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return your widget
);
}
/// Whether to refresh
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
returnmaxExtent ! = oldDelegate.maxExtent || minExtent ! = oldDelegate.minExtent; }}Copy the code
The design of the immersion in the actual application is very common, use SliverPersistentHeaderDelegate can easily achieve the effect of immersion:
It works by dynamically adjusting the style of the status bar and the color of the title bar with shrinkOffset, as shown in the immersive Header below.
SliverToBoxAdapter
Convert BoxWidget to Sliver: Because CustomScrollView can only accept children of type Sliver, many commonly used widgets cannot be added directly to CustomScrollView. All you need to do is wrap the Widget with a SliverToBoxAdapter. The most common use is when SliverList doesn’t support landscape mode, but you can’t add the ListView directly to the CustomScrollView. In this case, wrap the SliverToBoxAdapter:
CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: _buildHorizonScrollView(),
),
],
));
Widget _buildHorizonScrollView() {
return Container(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
primary: false,
shrinkWrap: true,
itemCount: 15,
itemBuilder: (context, index) {
return Container(
color: ColorUtils.randomColor(),
width: 50,
height: 50,); })); }Copy the code
SliverPadding
Padding can be used in CustomScrollView. Be careful not to wrap the SliverPersistentHeader as it will make the SliverPersistentHeader pinned, If SliverPersistentHeader must use a Padding effect, use the Padding inside the Delegate.
- Wrong code:
SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 16),
sliver: SliverPersistentHeader(
pinned: true,
floating: false,
delegate: Delegate(),
),
)
Copy the code
- Correct code:
class Delegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) =>
Padding(
padding: EdgeInsets.symmetric(horizontal: 16), child: Container( color: Colors.yellow, ), ); . }Copy the code
SliverSafeArea
The usage is consistent with SafeArea.
SliverFillRemaining
A Sliver that can fill the remaining controls on the screen.
Part of the example code:
Immersive Header
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class GradientSliverHeaderDelegate extends SliverPersistentHeaderDelegate {
final double collapsedHeight;
final double expandedHeight;
final double paddingTop;
final String coverImgUrl;
final String title;
GradientSliverHeaderDelegate({
this.collapsedHeight,
this.expandedHeight,
this.paddingTop,
this.coverImgUrl,
this.title,
});
@override
double get minExtent => this.collapsedHeight + this.paddingTop;
@override
double get maxExtent => this.expandedHeight;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
Color makeStickyHeaderBgColor(shrinkOffset) {
final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255)
.clamp(0.255)
.toInt();
return Color.fromARGB(alpha, 255.255.255);
}
Color makeStickyHeaderTextColor(shrinkOffset) {
if (shrinkOffset <= 50) {
return Colors.white;
} else {
final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255)
.clamp(0.255)
.toInt();
return Color.fromARGB(alpha, 0.0.0);
}
}
Brightness getStatusBarTheme(shrinkOffset) {
return shrinkOffset <= 50 ? Brightness.light : Brightness.dark;
}
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: getStatusBarTheme(shrinkOffset));
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
return Container(
height: this.maxExtent,
width: MediaQuery.of(context).size.width,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
/ / background
Container(
child: Image.asset(
coverImgUrl,
fit: BoxFit.cover,
)),
// Put your head back
Positioned(
left: 0,
right: 0,
top: 0,
child: Container(
color: this.makeStickyHeaderBgColor(shrinkOffset), // Background color
child: SafeArea(
bottom: false,
child: Container(
height: this.collapsedHeight,
child: Center(
child: Text(
this.title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: this
.makeStickyHeaderTextColor(shrinkOffset), // Title color),),),),),),,,); }}Copy the code
class CustomRefreshWidget extends SingleChildRenderObjectWidget {
const CustomRefreshWidget({Key key, Widget child})
: super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
returnSimpleRefreshSliver(); }}/// A simple drop-down refresh Widget
class SimpleRefreshSliver extends RenderSliverSingleBoxAdapter {
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
double childExtent;
switch (constraints.axis) {
case Axis.horizontal:
childExtent = child.size.width;
break;
case Axis.vertical:
childExtent = child.size.height;
break;
}
assert(childExtent ! =null);
final double paintedChildSize =
calculatePaintOffset(constraints, from: 0.0, to: childExtent);
assert(paintedChildSize.isFinite);
assert(paintedChildSize >= 0.0);
final bool active = constraints.overlap < 0.0;
final double overscrolledExtent =
constraints.overlap < 0.0 ? constraints.overlap.abs() : 0.0;
double layoutExtent = child.size.height;
print("overscrolledExtent:${overscrolledExtent - layoutExtent}");
child.layout(
constraints.asBoxConstraints(
maxExtent: layoutExtent
// Plus only the overscrolled portion immediately preceding this
// sliver.
+
overscrolledExtent,
),
parentUsesSize: true,);if (active) {
geometry = SliverGeometry(
scrollExtent: layoutExtent,
/// Draw the starting position
paintOrigin: min(overscrolledExtent - layoutExtent, 0),
paintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
maxPaintExtent: max(max(child.size.height, layoutExtent) ,0.0,),
/// layout placeholder
layoutExtent: min(overscrolledExtent, layoutExtent),
);
} else {
// If you don't want to display it, you can set it to zerogeometry = SliverGeometry.zero; } setChildParentData(child, constraints, geometry); }}Copy the code
Use:
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
// android needs to set spring effect overlap to work
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
CustomRefreshWidget(
child: Container(
height: 100,
color: Colors.purple,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
"RefreshWidget",
style: TextStyle(color: Colors.white),
),
Padding(
padding: EdgeInsets.only(left: 10.0),
child: CupertinoActivityIndicator(),
)
],
),
),
),
...
_buildListView(),
],
));
}
Copy the code