介绍
在对listview的构造原理的分析时所记录的一些笔记,希望对诸位有所帮助。
listview整体构造庞大,故将由多章构成。
阅读以下内容,你需要对 flutter widget的 构建、布局和绘制有所了解
这篇我们主要分析listview构造 child-view的流程。
Listview
listview 有很多构造函数 如:
ListView
ListView.builder
ListView.separated
...等等
我们以ListView.builder 来进行分析。
ListView.builder
首先我们看一个简单的使用代码
ListView.builder(
//多少个item
itemCount: 200,
//构建child-view的方法
itemBuilder: (ctx,index){
return Container(
alignment: Alignment.center,
width: size.width,height: 250,
color: index%2 == 0 ? Colors.red:Colors.blueAccent,
child: Text('$index',style: TextStyle(color: Colors.white,fontSize: 30),),
);
})
以上会构建一个红蓝相间的垂直滚动的list widget。
我们点进去看一下listview的源码。
ListView.builder 结构图
为了不偏离我们的目标,迷失在源码中,我会只标注相关的参数和方法,其他则忽略不写
由上图中可以看到,定义了一些绘制、布局相关的参数。方法则只有一个 :
//两个sliver list 没有太大区别,我们这里只以第二个为准
//返回了一个SliverList(delegate: childrenDelegate)
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return SliverList(delegate: childrenDelegate);
}
在返回的sliverList中传入了一个childrenDelegate,这个参数是在构造函数初始化的时候创建的。
/// 我删除一些无关代码
ListView.builder({
@required IndexedWidgetBuilder itemBuilder,
}) :
childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
);
我们停下脚步,瞅一眼这个 SliverChildBuilderDelegate
SliverChildBuilderDelegate
可见,这个只是对我们的 itemBuilder()方法进行一个再封装,并作为sliver构建孩子的代理类。
我们回到buildChildLayout(BuildContext context),即创建sliver的地方。
此方法在哪里调用呢? 根据经验来看,应该是和它的父类有关。
BoxScrollView
由注释来看,它是一个使用 单孩子 布局模型的scrollView。
由上图来看,除了接收一些子类的参数外,还有两个方法:
这个就是我们上面在listview内实现的方法
/// Subclasses should override this method to build the layout model.
@protected
Widget buildChildLayout(BuildContext context);
呦~看到调用buildChildLayout()的地方了,第一行就是,后面的则是对sliver进行一些装饰型的包裹,我们不用管。
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
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 = SliverPadding(padding: effectivePadding, sliver: sliver);
return <Widget>[ sliver ];
}
那么,这个buildSlivers()在哪里调用呢? 我们看它的父类
ScrollView
abstract class ScrollView extends StatelessWidget{}
终于到了我们熟悉的东西:StatelessWidget 来看一下scrollview的结构图
官方给了scrollview这样的注释:
一个可滚动的wiget,其有三部分组成
1、一个Scrollable widget监听用户的各种手势,并实现用于滚动的交互设计
2、一个viewport(视窗) widget,实现用于滚动时展现部分scroll view内容的视觉设计
3、一个或多个sliver,可以用来组成各种滚动效果的widget
明白了构成,我们回到原路上继续往下走。scrollview 有三个方法:
这个就是boxscrollview 实现的啦
@protected
List<Widget> buildSlivers(BuildContext context);
这个方法则是用于创建视窗 这里我们走 第二个 return
@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
return Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
);
}
我们熟悉的 build()方法
@override
Widget build(BuildContext context) {
//这里就调用了 boxScrollview 的 buildSlivers方法
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController scrollController =
primary ? PrimaryScrollController.of(context) : controller;
//我们用Scrollable 对 sliver进行包裹
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
//这里调用了 buildViewport,
//即,构造了 视窗并将咱们的sliver 传进去了
return buildViewport(context, offset, axisDirection, slivers);
},
);
//无关的我删除掉, 最终会返回scrollable(对它进行一些包裹)
}
至此,我们对Listview的调用逻辑就有了一个大致的了解,如下图:
根据上图,我们知道了
buildSlivers() 返回的sliverList 这个跟我们的 child-view 息息相关
buildChildLayout() 对sliverList进行了一些装饰
build() 会依次调用上面的方法,并用Scrollable widget进行包裹
同时创建一个视窗viewport
由此可见,与咱们的child-view构建相关的可能是sliverList,我们来看看
SliverList
由上图可见,sliverList最终继承自 renderObjectWidget,换言之它就是一个自定义view。
renderObjectWidget 咱们常用的widget不少都继承它,帮助咱们的widget创建 renderObject
你说element干啥的?简单讲,主要负责‘增删改查’
如果不太理解,可以去查阅布局、绘制相关的文章
再回来,这个sliver list 就实现了一个父类方法:
@override
RenderSliverList createRenderObject(BuildContext context) {
final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
return RenderSliverList(childManager: element);
}
在哪里调用的,咱也不知道,看看它的父类
SliverMultiBoxAdaptorWidget
他有一个方法:
当然还有上面子类实现的:createRenderObject()方法
// 看! 创建了element 还把自己这个widget传进去了
@override
SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this);
这样一看,是不是就跟咱们平常的 stateful widget的构建流程一样了?
我们来看看这个element
SliverMultiBoxAdaptorElement
点开内部一看,与element如出一辙。
其内部有一大堆方法,全都与widget构建有关,如:
performRebuild()
update()
updateChild()
didFinishChildLayout()
...等等等等不一而足
简单说一下performRebuild()这个方法
所有的widget其element都会有这个方法,此方法间接受vsync信号驱动,
最终会调用你熟悉的build()方法构建你的widget。
像你平常setState()都会触发这个方法,不然也不会叫XXrebuild了
我们发现在performRebuild方法中,会调用_build方法
Widget _build(int index) {
return widget.delegate.build(this, index);
}
这个delegate熟悉吧(SliverChildBuilderDelegate)? 它的build方法就是对我们的itemBuilder的封装。
还记得开头我们使用的listview的代码吗?
ListView.builder(
itemCount: 200,
itemBuilder: (ctx,index){
return Container(
alignment: Alignment.center,
width: size.width,height: 250,
color: index%2 == 0 ? Colors.red:Colors.blueAccent,
child: Text('$index',style: TextStyle(color: Colors.white,fontSize: 30),),
);
})
这个itemBuilder的方法是不是一样?
注: BuildContext 就是element
至此,我们就分析完了child-view的构建,在稍后的文章将介绍视窗(Viewport)和Scrollable widget,谢谢大家阅读。