Flutter 滾動控制元件篇-->ListView

夜夕i發表於2019-10-04

Flutter 中的ListView是最常用的可滾動的元件之一,
可以沿一個方向線性排布所有子元件,並且它也支援基於Sliver的延遲構建模型。

基於Sliver的延遲構建

什麼是基於Sliver的延遲構建模型呢?

通常可滾動元件的子元件可能會非常多、佔用的總高度也會非常大;如果要一次性將子元件全部構建出將會非常昂貴!為此,Flutter中提出一個Sliver(中文為“薄片”的意思)概念,如果一個可滾動元件支援Sliver模型,那麼該滾動可以將子元件分成好多個“薄片”(Sliver),只有當Sliver出現在視口中時才會去構建它,這種模型也稱為“基於Sliver的延遲構建模型”。

原始碼示例

建構函式如下:

ListView({
	...  
	//可滾動元件的公共引數
	Axis scrollDirection = Axis.vertical,
	bool reverse = false,
	ScrollController controller,
	bool primary,
	ScrollPhysics physics,	
	EdgeInsetsGeometry padding,	
	
	//ListView各個建構函式的共同引數
	this.itemExtent,
	bool shrinkWrap = false,
	bool addAutomaticKeepAlives = true,
	bool addRepaintBoundaries = true,
	bool addSemanticIndexes = true,
	double cacheExtent,
	
	//子元件列表
	List<Widget> children = const <Widget>[],
	int semanticChildCount,
	DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})
複製程式碼

屬性解釋

scrollDirection

決定子元件的滾動方向(排列方向),預設是垂直方向

scrollDirection:Axis.horizontal,水平方向
scrollDirection:Axis.vertical,垂直方向

reverse

決定滾動方向是否與閱讀方向一致

圖片載入失敗!

primary

當內容不足以滾動時,是否支援滾動;

值為true或者false,我試了一下,好像沒什麼卵用,不知道是理解錯了,還是怎麼的,先寫上吧

controller

此屬性接收一個ScrollController物件。ScrollController的主要作用是控制滾動位置和監聽滾動事件
有關ScrollController的使用及詳情,請參考Flutter 滾動控制元件篇-->滾動監聽及控制(ScrollController)

預設情況下,Widget樹中會有一個預設的PrimaryScrollController,如果子樹中的可滾動元件沒有顯式的指定controller,並且primary屬性值為true時(預設就為true),可滾動元件會使用這個預設的PrimaryScrollController。這種機制帶來的好處是父元件可以控制子樹中可滾動元件的滾動行為

physics

此屬性接受一個ScrollPhysics型別的物件,它決定可滾動元件如何響應使用者操作,比如使用者滑動完抬起手指後,繼續執行動畫;或者滑動到邊界時,如何顯示。

在iOS上會出現彈性效果,而在Android上會出現微光效果。

shrinkWrap

該屬性表示是否根據子元件的總長度來設定ListView的長度,預設值為false
預設情況下,ListView會在滾動方向儘可能多的佔用空間。當ListView在一個無邊界(滾動方向上)的容器中時,shrinkWrap必須為true,否則會報錯。

itemExtent

該引數如果不為null,則會強制children的“長度”為itemExtent的值;

這裡的“長度”是指滾動方向上子元件的長度,也就是說如果滾動方向是垂直方向,則itemExtent代表子元件的高度;如果滾動方向為水平方向,則itemExtent就代表子元件的寬度。

addAutomaticKeepAlives

該屬性表示是否將列表項(子元件)包裹在AutomaticKeepAlive元件中;

在一個懶載入列表中,如果將列表項包裹在AutomaticKeepAlive中,在該列表項滑出視口時也不會被回收,它會使用KeepAliveNotification來儲存其狀態。如果列表項自己維護其KeepAlive狀態,那麼此引數必須置為false

addRepaintBoundaries

該屬性表示是否將列表項(子元件)包裹在RepaintBoundary元件中。
當可滾動元件滾動時,將列表項包裹在RepaintBoundary中可以避免列表項重繪,但是當列表項重繪的開銷非常小(如一個顏色塊,或者一個較短的文字)時,不新增RepaintBoundary反而會更高效。和addAutomaticKeepAlive一樣,如果列表項自己維護其KeepAlive狀態,那麼此引數必須置為false

addSemanticIndexes

該屬性表示是否把子控制元件包裝在IndexedSemantics裡,用來提供無障礙語義

cacheExtent

可見區域的前後會有一定高度的空間去快取子控制元件,當滑動時就可以迅速呈現

簡單的就是說,當你快要滑到載入資料的時候,他已經提前一步載入好了,等到你滑到的時候就會顯示出來,而不至於使用者滑到的時候還需要等待一會兒。

semanticChildCount

有含義的子控制元件的數量

如:ListView會用children的長度,而ListView.separated會用children長度的一半

children

這裡的children需要說一下,和別的元件裡的不一樣。

這裡的children引數,他就收一個列表,但是這種方式適合只有少量的子元件的情況。因為這種方式需要將所有children都提前建立好(這需要做大量工作),而不是等到子元件真正顯示的時候再建立,也就是說通過預設建構函式構建的ListView沒有應用基於Sliver的懶載入模型

再次強調,可滾動元件通過一個List來作為其children屬性時,只適用於子元件較少的情況,這是一個通用規律。

ListView.builder

上面的children只適合資料較少的情況下使用

ListView.builder則適合列表項比較多(或者無限)的情況下使用,因為只有當子元件真正顯示的時候才會被建立,也就說通過該建構函式建立的ListView是支援基於Sliver的懶載入模型的。

原始碼示例

建構函式如下:

ListView.builder({
  // ListView公共引數已省略  
  ...
  @required IndexedWidgetBuilder itemBuilder,
  int itemCount,
  ...
})
複製程式碼

屬性解釋

itemBuilder

它是列表項的構建器,型別為IndexedWidgetBuilder,返回值為一個widget(就是一個元件)。當列表滾動到具體的index位置時,會呼叫該構建器構建列表項,也就是所謂的基於Sliver的懶載入模型。

itemCount

該屬性表示列表項的數量,如果為null,則表示無限列表

注:可滾動元件的建構函式如果需要一個列表項Builder,那麼通過該建構函式構建的可滾動元件通常就是支援基於Sliver的懶載入模型的,反之則不支援,這是個一般規律。

程式碼示例:

ListView.builder(
	itemCount: 100,
	itemExtent: 50.0, //強制高度為50.0,如果這個值越來越小的話,那麼顯示的值是會重疊的
	itemBuilder: (BuildContext context, int index) {
	  return ListTile(title: Text("$index"));
})
複製程式碼

執行效果:

圖片載入失敗!

ListView.separated

ListView.separated可以在生成的列表項之間新增一個分割元件。

它比ListView.builder多了一個separatorBuilder引數,該引數是一個分割元件生成器。

程式碼示例:

在奇數行新增一條藍色下劃線,偶數行新增一條紅色下劃線。

import 'package:flutter/material.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    //下劃線widget預定義以供複用。
    Widget Lineblue = Divider(color: Colors.blue);
    Widget Linered = Divider(color: Colors.red);
    return Scaffold(
        appBar: AppBar(
          title: Text(
            "ListView.separated",
            // style: TextStyle(color: Color(0xFF1E88E5)),
          ),
        ),
        body: ListView.separated(
          itemCount: 100,
          //列表項構造器
          itemBuilder: (BuildContext context, int index) {
            return ListTile(title: Text("$index"));
          },
          //分割器構造器
          separatorBuilder: (BuildContext context, int index) {
            return index % 2 == 0 ? Lineblue : Linered;
          },
        ));
  }
}
複製程式碼

執行效果:

圖片載入失敗!

無限載入列表

假設我們要從資料來源非同步分批拉取一些資料,然後用ListView展示。
當我們滑動到列表末尾時,判斷是否需要再去拉取資料,如果是,則去拉取,拉取過程中在表尾顯示一個轉著的小圓圈,拉取成功後將資料插入列表;如果不需要再去拉取,則在表尾提示"沒有更多了"。

這裡我們需要安裝一個包english_words: ^3.1.5(在pubspec.yaml檔案中的dependencies下安裝),可以給我們自動的生成英語單詞

程式碼示例:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  static const loadingTag = "##loading##"; //表尾標記
  var _words = <String>[loadingTag];

  @override
  void initState() {
    super.initState();
    _retrieveData();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: _words.length,
      itemBuilder: (context, index) {
        //如果到了表尾
        if (_words[index] == loadingTag) {
          //不足100條,繼續獲取資料
          if (_words.length - 1 < 100) {
            //獲取資料
            _retrieveData();
            //載入時顯示loading
            return Container(
              padding: const EdgeInsets.all(16.0),
              alignment: Alignment.center,
              child: SizedBox(
                  width: 24.0,
                  height: 24.0,
                  child: CircularProgressIndicator(strokeWidth: 2.0)),
            );
          } else {
            //已經載入了100條資料,不再獲取資料。
            return Container(
                alignment: Alignment.center,
                padding: EdgeInsets.all(16.0),
                child: Text(
                  "沒有更多了",
                  style: TextStyle(color: Colors.grey),
                ));
          }
        }
        //顯示單詞列表項
        return ListTile(title: Text(_words[index]));
      },
      separatorBuilder: (context, index) => Divider(height: .0),
    );
  }

  void _retrieveData() {
    Future.delayed(Duration(seconds: 2)).then((e) {
      _words.insertAll(
          _words.length - 1,
          //每次生成20個單詞
          generateWordPairs().take(20).map((e) => e.asPascalCase).toList());
      setState(() {
        //重新構建列表
      });
    });
  }
}
複製程式碼

_retrieveData()的功能是模擬從資料來源非同步獲取資料,
english_words包的generateWordPairs()方法可以每次生成20個單詞。

執行效果:

圖片載入失敗!

ListTile

這裡我們說一下ListTile
ListTile通常用於在 Flutter 中填充 ListView

原始碼示例:

建構函式如下:

const ListTile({
	Key key,
	this.leading,
	this.title,
	this.subtitle,
	this.trailing,
	this.isThreeLine = false,
	this.dense,
	this.contentPadding,
	this.enabled = true,
	this.onTap,
	this.onLongPress,
	this.selected = false,
})
複製程式碼

屬性解釋

title

title引數可以接受任何小元件,但通常是文字小元件

程式碼示例:

ListTile(
  title: Text('我喜歡你!'),
)
複製程式碼

subtitle

他是一個副標題,顯示在標題(title)下面較小的文字

程式碼示例:

ListTile(
  title: Text('我喜歡你!'),
  subtitle: Text('你喜歡我嗎?'),
)
複製程式碼

dense

使文字更小,並將所有內容打包在一起

程式碼示例:

ListTile(
  title: Text('我喜歡你!'),
  subtitle: Text('你喜歡我嗎?'),
  dense:true,
)
複製程式碼

leading

將影象或圖示新增到列表的開頭。

程式碼示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜歡你'),
  subtitle: Text('你喜歡我嗎?'),
  dense:true,
)
複製程式碼

trailing

在列表的末尾放置一個影象。

程式碼示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜歡你'),
  subtitle: Text('你喜歡我嗎?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
)
複製程式碼

contentPadding

設定內容邊距,預設是 16
我這裡設定30

程式碼示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜歡你'),
  subtitle: Text('你喜歡我嗎?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
  contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
)
複製程式碼

selected

如果選中列表的 item 項,那麼文字和圖示的顏色將成為主題的主顏色。

程式碼示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜歡你'),
  subtitle: Text('你喜歡我嗎?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
  contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
  selected: true,
)
複製程式碼

onTap、onLongPress

onTap 為單擊,onLongPress 為長按。

程式碼示例:

ListTile(
  leading: CircleAvatar(
    backgroundImage: NetworkImage(imageUrl),
  ),
  title: Text('我喜歡你'),
  subtitle: Text('你喜歡我嗎?'),
  dense:true,
  trailing: Icon(Icons.keyboard_arrow_right),
  contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
  selected: true,
  onTap: () {
    // do something
  },
  onLongPress: (){
    // do something else
  },
)
複製程式碼

enabled

通過將 enable 設定為 false,來禁止點選事件

這裡就不寫程式碼了,比較簡單。

屬性demo示例

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

class CategoryPage extends StatefulWidget {
  @override
  _CategoryPageState createState() => _CategoryPageState();
}

class _CategoryPageState extends State<CategoryPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('ListTile'),),
        body: Column(
      children: <Widget>[
        ListTile(
          leading: CircleAvatar(
            backgroundImage: AssetImage('images/Test.jpg'),
          ),
          title: Text('我喜歡你'),
          subtitle: Text('你喜歡我嗎?'),
          dense: true,
          trailing: Icon(Icons.keyboard_arrow_right),
          contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
          // selected: true,
          onTap: () {
            // do something
          },
          onLongPress: () {
            // do something else
          },
        ),
        ListTile(
          leading: CircleAvatar(
            backgroundImage: AssetImage('images/Test.jpg'),
          ),
          title: Text('我喜歡你'),
          subtitle: Text('你喜歡我嗎?'),
          dense: true,
          trailing: Icon(Icons.keyboard_arrow_right),
          contentPadding: EdgeInsets.symmetric(horizontal: 30.0),
          selected: true,
          onTap: () {
            // do something
          },
          onLongPress: () {
            // do something else
          },
        )
      ],
    ));
  }
}
複製程式碼

執行效果:

圖片載入失敗!

新增固定列表頭

很多時候我們需要給列表新增一個固定表頭。

我們需要讓ListView自動拉伸以適應螢幕,這個時候就需要我們使用到彈性佈局Flex,如果不知道的話,請移步Flutter 佈局控制元件篇-->Flex、Expanded

我們可以使用Expanded自動拉伸元件大小,並且我們也說過Column是繼承自Flex的,所以我們可以直接使用Column+Expanded來實現,

程式碼示例:

Column(children: <Widget>[
  ListTile(title: Text("數字列表")),
  Expanded(
	child: ListView.builder(itemBuilder: (BuildContext context, int index) {
	  return ListTile(title: Text("$index"));
	}),
  ),
]);
複製程式碼

執行效果:

圖片載入失敗!


T_T

相關文章