Flutter——ListView源码分析之child-view的构建

介绍

在对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 结构图

为了不偏离我们的目标,迷失在源码中,我会只标注相关的参数和方法,其他则忽略不写
image

由上图中可以看到,定义了一些绘制、布局相关的参数。方法则只有一个 :

    //两个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

image

可见,这个只是对我们的 itemBuilder()方法进行一个再封装,并作为sliver构建孩子的代理类。

我们回到buildChildLayout(BuildContext context),即创建sliver的地方。

此方法在哪里调用呢? 根据经验来看,应该是和它的父类有关。

BoxScrollView

image

由注释来看,它是一个使用 单孩子 布局模型的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的结构图

image

官方给了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的调用逻辑就有了一个大致的了解,如下图:

image

根据上图,我们知道了

buildSlivers() 返回的sliverList    这个跟我们的 child-view 息息相关
buildChildLayout()      对sliverList进行了一些装饰
build()     会依次调用上面的方法,并用Scrollable widget进行包裹
            同时创建一个视窗viewport

由此可见,与咱们的child-view构建相关的可能是sliverList,我们来看看

SliverList

image

由上图可见,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了
image

我们发现在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,谢谢大家阅读。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335