flutter系列之:flutter中常用的ListView layout詳解

簡介

ListView是包含多個child組件的widget,在ListView中所有的child widget都是以list的形式來呈現的,你可以自定義List的方向,但是和GridView不同的是ListView中的每一個List裏面都只包含一個widget。

今天我們來詳細了解一下ListView的底層實現和具體的應用。

ListView詳解

和GridView一樣,ListView也是繼承自ScrollView,表示它是一個可以滾動的View。

具體而言,ListView首先繼承自BoxScrollView:

class ListView extends BoxScrollView

然後BoxScrollView又繼承自ScrollView:

abstract class BoxScrollView extends ScrollView 

ListView中的特有屬性

首先我們來看下ListView中的特有屬性,ListView和它的父類相比,多了三個屬性,分別是itemExtent,prototypeItem和childrenDelegate。

其中itemExtent是一個double類型的數據,如果給定的是一個非空值,那麼表示的是child在scroll方向的extent大小。這個屬性主要用來控制children的extend信息,這樣每個child就不需要自行來判斷自己的extend。

使用itemExtent的好處在於,ListView可以統一的在滾動機制上進行優化,從而提升性能表現。

prototypeItem是一個widget,從名字就可以看出,這個一個prototype的widget,也就是說是一個原型,其他的child可以參照這個原型widget的大小進行extent的設置。

ListView中的最後一個自定義屬性是childrenDelegate,這個childrenDelegate和GridView中的含義是一樣的,用來生成ListView中child。

之前我們在講解GirdView的時候有提到過,GirdView中還有一個自定義的屬性叫做gridDelegate,這個gridDelegate是一個SliverGridDelegate的實例,用來控制子組件在GridView中的布局。

因為ListView的子組件的布局是已經確定的,所以就不再需要gridDelegate了,這是ListView和GridView的一大區別。

ListView作為一個繼承的類,需要實現一個buildChildLayout的方法:

  @override
  Widget buildChildLayout(BuildContext context) {
    if (itemExtent != null) {
      return SliverFixedExtentList(
        delegate: childrenDelegate,
        itemExtent: itemExtent!,
      );
    } else if (prototypeItem != null) {
      return SliverPrototypeExtentList(
        delegate: childrenDelegate,
        prototypeItem: prototypeItem!,
      );
    }
    return SliverList(delegate: childrenDelegate);
  }

這個方法的實現邏輯和我們之前講到的三個屬性是相關聯的,在buildChildLayout中,如果itemExtent有值的話,因為itemExtent本身就是一個固定值,所以返回的是SliverFixedExtentList。

如果itemExtent沒有設置,並且prototypeItem有值的話,返回的是一個SliverPrototypeExtentList。

最後,如果itemExtent和prototypeItem都沒有設置的話,返回的是一個SliverList對象。

ListView的構造函數

和GridView一樣,為了滿足我們的多樣性的設計需求,ListView也提供了多個構造函數。

首先我們來看下ListView的最基本的構造函數:

ListView({
    Key? key,
    Axis scrollDirection = Axis.vertical,
    bool reverse = false,
    ScrollController? controller,
    bool? primary,
    ScrollPhysics? physics,
    bool shrinkWrap = false,
    EdgeInsetsGeometry? padding,
    this.itemExtent,
    this.prototypeItem,
    bool addAutomaticKeepAlives = true,
    bool addRepaintBoundaries = true,
    bool addSemanticIndexes = true,
    double? cacheExtent,
    List<Widget> children = const <Widget>[],
    int? semanticChildCount,
    DragStartBehavior dragStartBehavior = DragStartBehavior.start,
    ScrollViewKeyboardDismissBehavior keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
    String? restorationId,
    Clip clipBehavior = Clip.hardEdge,
  })

這裡itemExtent和prototypeItem這兩個屬性是外部傳入的,childrenDelegate是通過其他的參數構造而來的:

 childrenDelegate = SliverChildListDelegate(
         children,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),

ListView中所有的child組件都在List Widget的children中。

這個默認的構造函數,適用於child比較少的情況,因為需要一次傳入所有的child組件到list中,所以對性能的影響還是挺大的,並且傳入的child是不可變的。

如果child比較多的情況下,就需要使用到其他的構造函數了,比如 ListView.builder。

ListView.builder使用的是builder模式來構建child組件,具體而言他的childrenDelegate實現如下:

childrenDelegate = SliverChildBuilderDelegate(
         itemBuilder,
         childCount: itemCount,
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
       ),

這裡的childrenDelegate是一個SliverChildBuilderDelegate,通過傳入itemBuilder和總的itemCount就可以實現動態創建child的功能。

在ListView的實際使用過程中,為了頁面好看或者更有區分度,我們一般會在list的item中添加一些分隔符separator,為了自動化實現這個功能,ListView提供了一個ListView.separated的構造函數,用來提供list item中間的分隔符。

ListView.separated需要傳入兩個IndexedWidgetBuilder,分別是itemBuilder和separatorBuilder。

下面是childrenDelegate的具體實現:

 childrenDelegate = SliverChildBuilderDelegate(
         (BuildContext context, int index) {
           final int itemIndex = index ~/ 2;
           final Widget widget;
           if (index.isEven) {
             widget = itemBuilder(context, itemIndex);
           } else {
             widget = separatorBuilder(context, itemIndex);
             assert(() {
               if (widget == null) {
                 throw FlutterError('separatorBuilder cannot return null.');
               }
               return true;
             }());
           }
           return widget;
         },
         childCount: _computeActualChildCount(itemCount),
         addAutomaticKeepAlives: addAutomaticKeepAlives,
         addRepaintBoundaries: addRepaintBoundaries,
         addSemanticIndexes: addSemanticIndexes,
         semanticIndexCallback: (Widget _, int index) {
           return index.isEven ? index ~/ 2 : null;
         },
       ),

可以看到,如果index是even的話就會使用itemBuilder生成一個widget,如果index是odd的話,就會使用separatorBuilder來生成一個separator的widget。

最後,ListView還有一個更加開放的構造函數ListView.custom,custom和其他構造函數不同的地方在於他可以自定義childrenDelegate,從而提供了更多的擴展空間。

ListView的使用

有了上面的構造函數,我們可以很方便的根據自己的需要來使用ListView,下面是一個簡單的使用圖片做child的例子:

class ListViewApp extends StatelessWidget{
  const ListViewApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 5,
      itemBuilder: (BuildContext context, int index) {
        return Container(
            constraints: const BoxConstraints(maxWidth:100,maxHeight: 100),
            child: Image.asset('images/head.jpg')
        );
      },
    );
  }
}

上面的例子中,我們使用的是ListView.builder構造函數,返回的Widget中,中的widget個數是5,每個item是由itemBuilder來生成的。

這裡我們把Image封裝在一個Container中,並且為Container設置了一個constraints來控制圖片的大小。

最終生成的界面如下:

上面的例子中,item之間是沒有分隔符的,我們可以講上面的例子進行稍微的修改一下,使用ListView.separated來構造ListView,如下所示:

class ListViewSeparatedApp extends StatelessWidget{

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
           itemCount: 10,
           separatorBuilder: (BuildContext context, int index) => const Divider(),
           itemBuilder: (BuildContext context, int index) {
             return Container(
                 constraints: const BoxConstraints(maxWidth:50,maxHeight: 50),
               child: Image.asset('images/head.jpg')
             );
           },
         );
  }
}

這裡我們需要傳入separatorBuilder來作為分隔符,為了簡單起見,我們直接使用了Divider這個widget。

最後生成的界面如下:

總結

以上就是ListView的介紹和基本的使用。

本文的例子://github.com/ddean2009/learn-flutter.git