【Flutter學習】頁面布局之列表和表格處理


一,概述  

  Flutter中擁有30多種預定義的布局widget,常用的有ContainerPaddingCenterFlexRowColumListViewGridView。按照《Flutter技術入門與實戰》上面來說的話,大概分為四類

  • 基礎布局組件Container(容器布局),Center(居中布局),Padding(填充布局),Align(對齊布局),Colum(垂直布局),Row(水平布局),Expanded(配合Colum,Row使用),FittedBox(縮放布局),Stack(堆疊布局),overflowBox(溢出父視圖容器)。
  • 寬高尺寸處理SizedBox(設置具體尺寸),ConstrainedBox(限定最大最小寬高布局),LimitedBox(限定最大寬高布局),AspectRatio(調整寬高比),FractionallySizedBox(百分比布局)
  • 列表和表格處理ListView(列表),GridView(網格),Table(表格)
  • 其它布局處理:Transform(矩陣轉換),Baseline(基准線布局),Offstage(控制是否顯示組件),Wrap(按寬高自動換行布局)

二,列表和表格處理布局組件

  • ListView(列表)  
    • 介紹
      ListView是一個非常常用的控件,涉及到數據列表展示的,一般情況下都會選用該控件。ListView跟GridView相似,基本上是一個slivers里面只包含一個SliverList的CustomScrollView。
    • 布局行為
      ListView在主軸方向可以滾動,在交叉軸方向,則是填滿ListView。
    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > ListView

      看繼承關系可知,這是一個組合控件。ListView跟GridView類似,都是繼承自BoxScrollView。

    • 構造函數
      ListView({
        Key key,
        Axis scrollDirection = Axis.vertical,
        bool reverse = false,
        ScrollController controller,
        bool primary,
        ScrollPhysics physics,
        bool shrinkWrap = false,
        EdgeInsetsGeometry padding,
        this.itemExtent,
        bool addAutomaticKeepAlives = true,
        bool addRepaintBoundaries = true,
        double cacheExtent,
        List<Widget> children = const <Widget>[],
      })

      同時也提供了如下額外的三種構造方法,方便開發者使用。

      ListView.builder
      ListView.separated
      ListView.custom
    • 使用場景

      ListView使用場景太多了,一般涉及到列表展示的,一般都會選擇ListView。

      但是需要注意一點,ListView的標准構造函數適用於數目比較少的場景,如果數目比較多的話,最好使用ListView.builder

      ListView的標准構造函數會將所有item一次性創建,而ListView.builder會創建滾動到屏幕上顯示的item。

    • 參數解析

      ListView大部分屬性同GridView,想了解的讀者可以看一下下面所寫的GridView相關的內容。這里只介紹一個屬性

      itemExtent:ListView在滾動方向上每個item所占的高度值。

  • GridView(網格)
    • 介紹
      GridView在移動端上非常的常見,就是一個滾動的多列列表,實際的使用場景也非常的多。
    • 布局行為
      GridView的布局行為不復雜,本身是盡量占滿空間區域,布局行為上完全繼承自ScrollView。
    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView
    • 構造函數
      GridView({
        Key key,
        Axis scrollDirection = Axis.vertical,
        bool reverse = false,
        ScrollController controller,
        bool primary,
        ScrollPhysics physics,
        bool shrinkWrap = false,
        EdgeInsetsGeometry padding,
        @required this.gridDelegate,
        bool addAutomaticKeepAlives = true,
        bool addRepaintBoundaries = true,
        double cacheExtent,
        List<Widget> children = const <Widget>[],
      })

      同時也提供了如下額外的四種構造方法,方便開發者使用。

      GridView.builder
      GridView.custom
      GridView.count
      GridView.extent
    • 參數解析

      scrollDirection:滾動的方向,有垂直和水平兩種,默認為垂直方向(Axis.vertical)。

      reverse:默認是從上或者左向下或者右滾動的,這個屬性控制是否反向,默認值為false,不反向滾動。

      controller:控制child滾動時候的位置。

      primary:是否是與父節點的PrimaryScrollController所關聯的主滾動視圖。

      physics:滾動的視圖如何響應用戶的輸入。

      shrinkWrap:滾動方向的滾動視圖內容是否應該由正在查看的內容所決定。

      padding:四周的空白區域。

      gridDelegate:控制GridView中子節點布局的delegate。

      cacheExtent:緩存區域。

  • Table(表格)
    • 介紹
      每一種移動端布局中都會有一種table布局,這種控件太常見了。至於其表現形式,完全可以借鑒其他移動端的,通俗點講,就是表格。
    • 布局行為

      表格的每一行的高度,由其內容決定,每一列的寬度,則由columnWidths屬性單獨控制。 

    • 繼承關系
      Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > Table
    • 構造函數
      Table({
        Key key,
        this.children = const <TableRow>[],
        this.columnWidths,
        this.defaultColumnWidth = const FlexColumnWidth(1.0),
        this.textDirection,
        this.border,
        this.defaultVerticalAlignment = TableCellVerticalAlignment.top,
        this.textBaseline,
      })
    • 參數解析

      columnWidths:設置每一列的寬度。

      defaultColumnWidth:默認的每一列寬度值,默認情況下均分。

      textDirection:文字方向,一般無需考慮。

      border:表格邊框。

      defaultVerticalAlignment:每一個cell的垂直方向的alignment。

      總共包含5種:

      • top:被放置在的頂部;
      • middle:垂直居中;
      • bottom:放置在底部;
      • baseline:文本baseline對齊;
      • fill:充滿整個cell。

      textBaseline:defaultVerticalAlignment為baseline的時候,會用到這個屬性。

三,常用示例

  • ListView(列表)
    /**
     * ListView
     * 第一個展示四行文字
     */
    class MyListView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return new ListView(
          shrinkWrap: true,
          padding: EdgeInsets.all(20.0),
          children: <Widget>[
            new Text('I\m dedicationg every day to you'),
            new Text('Domestic life was never quite my style'),
            new Text('When you smile, you knock me out, I fall apart'),
            new Text('And I thought I was so smart')
          ],
        );
      }
    }

    效果圖:

    源碼解析:

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

    ListView標准構造布局代碼如上所示,底層是用到的SliverList去實現的。ListView是一個slivers里面只包含一個SliverList的CustomScrollView。源碼這塊兒可以參考GridView,在此不做更多的說明。

  • GridView(網格)  
    /**
     * GridView
     * 代碼直接用了Creating a Grid List中的例子,創建了一個2列總共100個子節點的列表。
     */
    class  MyGridView extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // TODO: implement build
        return new GridView.count(
          crossAxisCount: 2,
          children: List.generate(100, (index){
             return new Center(
               child: new Text(
                 'Item $index',
                 style: Theme.of(context).textTheme.headline,
               ),
             );
          },
          ),
        );
      } 
    }

    效果圖

    源碼解析:

    @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 = new Scrollable(
     axisDirection: axisDirection,
     controller: scrollController,
     physics: physics,
     viewportBuilder: (BuildContext context, ViewportOffset offset) {
    return buildViewport(context, offset, axisDirection, slivers);
     },
    );
    return primary && scrollController != null
    ? new PrimaryScrollController.none(child: scrollable)
    : scrollable;
    }

    上面這段代碼是ScrollView的build方法,GridView就是一個特殊的ScrollView。GridView本身代碼沒有什么,基本上都是ScrollView上的東西,主要會涉及到Scrollable、Sliver、Viewport等內容,這些內容比較多,因此源碼就先略了,后面單獨出一篇文章對ScrollView進行分析吧。

  • Table(表格)
    Table(
      columnWidths: const <int, TableColumnWidth>{
        0: FixedColumnWidth(50.0),
        1: FixedColumnWidth(100.0),
        2: FixedColumnWidth(50.0),
        3: FixedColumnWidth(100.0),
      },
      border: TableBorder.all(color: Colors.red, width: 1.0, style: BorderStyle.solid),
      children: const <TableRow>[
        TableRow(
          children: <Widget>[
            Text('A1'),
            Text('B1'),
            Text('C1'),
            Text('D1'),
          ],
        ),
        TableRow(
          children: <Widget>[
            Text('A2'),
            Text('B2'),
            Text('C2'),
            Text('D2'),
          ],
        ),
        TableRow(
          children: <Widget>[
            Text('A3'),
            Text('B3'),
            Text('C3'),
            Text('D3'),
          ],
        ),
      ],
    )

    效果圖:


    (1)樣例其實並不復雜,FlowDelegate需要自己實現child的繪制,其實大多數時候就是位置的擺放。上面例子中,對每個child按照給定的margin值,進行排列,如果超出一行,則在下一行進行布局。
    (2)另外,對這個例子多做一個說明,對於上述child寬度的變化,這個例子是沒問題的,如果每個child的高度不同,則需要對代碼進行調整,具體的調整是換行的時候,需要根據上一行的最大高度來確定下一行的起始y坐標。

    源碼解析:

    我們直接來看其布局源碼:

    第一步,當行或者列為0的時候,將自身尺寸設為0x0。

    if (rows * columns == 0) {
    size = constraints.constrain(const Size(0.0, 0.0));
    return;
    }

    第二步,根據textDirection值,設置方向,一般在阿拉伯語系中,一些文本都是從右往左現實的,平時使用時,不需要去考慮這個屬性。

    switch (textDirection) {
     case TextDirection.rtl:
           positions[columns - 1] = 0.0;
           for (int x = columns - 2; x >= 0; x -= 1)
              positions[x] = positions[x+1] + widths[x+1];
              _columnLefts = positions.reversed;
              tableWidth = positions.first + widths.first;
          break;
    case TextDirection.ltr:
              positions[0] = 0.0;
           for (int x = 1; x < columns; x += 1)
              positions[x] = positions[x-1] + widths[x-1];
              _columnLefts = positions;
              tableWidth = positions.last + widths.last;
              break;
           }

    第三步,設置每一個cell的尺寸。

    for (int x = 0; x < columns; x += 1) {
      final int xy = x + y * columns;
      final RenderBox child = _children[xy];
    if (child != null) {
      final TableCellParentData childParentData = child.parentData;
      childParentData.x = x;
      childParentData.y = y;
     switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
        case TableCellVerticalAlignment.baseline:
             child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
             final double childBaseline = child.getDistanceToBaseline(textBaseline, onlyReal: true);
             if (childBaseline != null) {
               beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline);
               afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline);
               baselines[x] = childBaseline;
               haveBaseline = true;
             } else {
               rowHeight = math.max(rowHeight, child.size.height);
               childParentData.offset = new Offset(positions[x], rowTop);
     }
         break;
    case TableCellVerticalAlignment.top:
    case TableCellVerticalAlignment.middle:
    case TableCellVerticalAlignment.bottom:
         child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true);
         rowHeight = math.max(rowHeight, child.size.height);
    break;
    case TableCellVerticalAlignment.fill:
    break;
    }
    }
    }

    第四步,如果有baseline則進行相關設置。

    if (haveBaseline) {
      if (y == 0) _baselineDistance = beforeBaselineDistance;
      rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance);
    }

    第五步,根據alignment,調整child的位置。

    for (int x = 0; x < columns; x += 1) {
      final int xy = x + y * columns;
      final RenderBox child = _children[xy];
     if (child != null) {
      final TableCellParentData childParentData = child.parentData;
     switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) {
       case TableCellVerticalAlignment.baseline:
         if (baselines[x] != null)
           childParentData.offset = new Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]);
         break;
       case TableCellVerticalAlignment.top:
           childParentData.offset = new Offset(positions[x], rowTop);
         break;
       case TableCellVerticalAlignment.middle:
           childParentData.offset = new Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0);
         break;
       case TableCellVerticalAlignment.bottom:
           childParentData.offset = new Offset(positions[x], rowTop + rowHeight - child.size.height); 
         break;
       case TableCellVerticalAlignment.fill:
           child.layout(new BoxConstraints.tightFor(width: widths[x], height: rowHeight));
           childParentData.offset = new Offset(positions[x], rowTop);
         break;
      }
     }
    }

    最后一步,則是根據每一行的寬度以及每一列的高度,設置Table的尺寸。

    size = constraints.constrain(new Size(tableWidth, rowTop));

    最后梳理一下整個的布局流程:

    當行或者列為0的時候,將自身尺寸設為0x0;
    根據textDirection進行相關設置;
    設置cell的尺寸;
    如果設置了baseline,則進行相關設置;
    根據alignment設置cell垂直方向的位置;
    設置Table的尺寸。
    如果經常關注系列文章的讀者,可能會發現,布局控件的布局流程基本上跟上述流程是相似的。  

四,參考  

Flutter學習之認知基礎組件
Flutter布局

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM