Flutter基础——ListView的用法

Posted by lingjye on July 18, 2019

ListView

在Flutter中使用ListView来实现一个列表,类似于iOS中的UITableView或者UICollectionView。与iOS不同的是,ListView需要传递一个widget列表,Flutter会保证它的流畅度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class _ListViewSampleAppPageState extends State<ListViewSampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('List View'),
       ),
       body: ListView(
         children: _getListData(),
       ),
    );
  }

  List<Widget> _getListData() {
    List<Widget> widgets = [];
    for (int i = 0; i< 100; i++) {
      widgets.add(
        Padding(padding: EdgeInsets.all(10), child: Text('Row at index: $i'),)
      );
    }
    return widgets;
  }
}

ListView默认是没有分割线的。

在Flutter中采用传递进来的widget的touch handle来处理点击事件,该事件可以获取点击的元素位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
List<Widget> _getListData() {
  List<Widget> widgets = [];
  for (int i = 0; i< 100; i++) {
    widgets.add(
      GestureDetector(
        child: Padding(
          padding: EdgeInsets.all(10),
          child: Text('Row at index: $i'),
        ),
        onTap: () {
          print('row tapped');
        },
      )
    );
  }
  return widgets;
}

Flutter中可以通过setState()来更新ListView,当前后的ListView相同时,不会更新。最简单的方法就是在setState()方法中创建一个新的list,并把旧list的数据拷贝给新的list,这种做法仅限于数据量很小时。当数据量大时,可以使用ListView.Builder来构建列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class _ListViewBuilderSampleAppPageState extends State<ListViewBuilderSampleAppPage> {
  List widgets = [];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _getWidgets();
  }

  _getWidgets() {
    for (var i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  getRow(int i) {
    return GestureDetector(
      child: Padding(
        padding: EdgeInsets.all(10),
        child: Text('row at inde: $i'),
      ),
      onTap: () {
      	 // 点击时新增一个列表子元素
        setState(() {
          widgets.add(
            getRow(widgets.length + 1)
          );
          print('tap row at $i');
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('ListView Builder'),
       ),
       body: ListView.builder(
         itemCount: widgets.length,
         itemBuilder: (BuildContext context, int position) {
           return getRow(position);
         }
       ),
    );
  }
}

使用ListView.builder有两个主要参数,一个是指定列表初始化长度的itemCount,另一个是返回每一个位置上item的itemBuilder,分别对应iOS中的以下方法:

1
2
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

当希望两个列表子元素间有分割时,可以使用ListView.separated,相比ListView.builder,多了一个主要参数separatorBuilder,它主要用来构建例如分割线等子Widget。它一般用于具有固定数量的子控件的列表视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('List View SeparatorBuilder'),
       ),
       body: ListView.separated(
         itemCount: widgets.length,
         itemBuilder: (BuildContext content, int position) {
           return widgets[position];
         },
         separatorBuilder: (BuildContext context, int position) {
          // 分割线 
          // return Container(height: 0.5, color: Colors.grey[300], );
          // indent 左侧缩进宽度
          return Divider(height: 1.0, indent: 10, color: Colors.grey[300]);
          // 不带child Widget 的装饰容盒子,只使用边框参数
          // return DecoratedBox( decoration: BoxDecoration(border: Border.all(color: Colors.red, width: 0.5),));
         },
       ),
    );
  }

GridView

GridView可以构建一个二维网格列表,它更类似iOS中的UICollectionView,它有一个必须参数gridDelegate,它是控制子widget layout的委托,用来控制子控件布局,每个子元素的大小默认是相同的。gridDelegate的类型是抽象类SliverGridDelegate,其中定义了GridView Layout相关接口,子类实现具体的布局算法必,Flutter中提供了两个SliverGridDelegate的子类SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent。

SliverGridDelegateWithFixedCrossAxisCount类用来提供纵轴方向的item的属性,四个主要参数如下:

  • crossAxisCount, 纵轴子元素数量
  • mainAxisSpacing, 主轴方向子元素间距
  • crossAxisSpacing, 纵轴方向子元素的间距
  • childAspectRatio, 宽度与高度的比例,例如:1.0代表宽高相等,2.0代表高度为宽度的一半,依次类推,它和crossAxisCount基本确定了子元素的大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
lass _GridViewSampleAppPageState extends State<GridViewSampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('GridView'),
       ),
       body: GridView(
         gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
           crossAxisCount: 3,		  //纵轴子元素数量
           mainAxisSpacing: 10.0,	  // 主轴方向子元素间距
           crossAxisSpacing: 10.0, // 纵轴方向子元素的间距
           childAspectRatio: 1.0   // 宽度与高度的比例,例如:1.0代表宽高相等,2.0代表高度为宽度的一半,依次类推
         ),
         children: <Widget>[
           Icon(Icons.add),
           Icon(Icons.update),
           Icon(Icons.edit),
           Icon(Icons.favorite),
           Icon(Icons.face),
           FlutterLogo(style: FlutterLogoStyle.horizontal,),
         ],
       ),
    );
  }
}

SliverGridDelegateWithMaxCrossAxisExtent类用来声明纵轴子元素的最大长度,也是用于子元素的布局,确定子元素大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class _GridViewSampleAppPageState extends State<GridViewSampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('GridView'),
       ),
       body: GridView(
        gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
          maxCrossAxisExtent: 150,	// 最大长度
          mainAxisSpacing: 10.0,		// 主轴间距
          crossAxisSpacing: 10.0,		// 纵轴子元素间距
          childAspectRatio: 1.0,		// 纵轴与主轴长度比
        ),
         children: <Widget>[
           Icon(Icons.add),
           Icon(Icons.update),
           Icon(Icons.edit),
           Icon(Icons.favorite),
           Icon(Icons.face),
           FlutterLogo(style: FlutterLogoStyle.horizontal,),
         ],
       ),
    );
  }
}

还可以使用GridView.count和GridView.extent快速创建GridView。GridView.count构造函数内部已经使用了SliverGridDelegateWithFixedCrossAxisCount,GridView.extent构造函数内部已经使用了SliverGridDelegateWithMaxCrossAxisExtent,所以直接使用即可。

GridView.count

1
2
3
4
5
body: GridView.count(
	crossAxisCount: 3,
	childAspectRatio: 1.0,
	children: widgets,
),

GridView.extent

1
2
3
4
5
body: GridView.extent(
	maxCrossAxisExtent: 150,
	childAspectRatio: 1.0,
	children: widgets,
),

类似于ListView,GridView也有GridView.builder方法,用于数据量较多的情况。GridView.builder有两个必须指定的参数:

  1. gridDelegate,用来确定怎么布局
  2. itemBuilder,用来确定每一个元素应该展示什么样的子widget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
List widgets = [];

@override
void initState() {
	// TODO: implement initState
	super.initState();
	_getWidgets();
}

_getWidgets() {
	for (var i = 0; i < 100; i++) {
	  widgets.add(getRow(i));
	}
}

getItem(int i) {
	return GestureDetector(
	  child: Padding(
	    padding: EdgeInsets.all(10),
	    child: Text('row at inde: $i'),
	  ),
	  onTap: () {
	    setState(() {
	      widgets.add(
	        getRow(widgets.length + 1)
	      );
	      print('tap row at $i');
	    });
	  },
	);
}

@override
Widget build(BuildContext context) {
return Scaffold(
   appBar: AppBar(
     title: Text('GridView'),
   ),
   body: GridView.builder(
     gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
       crossAxisCount: 3, // 三列
       childAspectRatio: 1.0 //宽高相等
     ),
     itemCount: widgets.length,
     itemBuilder: (context, index) {
       return getItem(index);
     },
   ),
);
}

ScrollView

另外使用ListView还可以用来代替iOS中的UIScrollView,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class _ScrollViewSampleAppPageState extends State<ScrollViewSampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('ScrollView'),
       ),
       body: ListView(
         padding: EdgeInsets.only(left: 20, top: 20),
         children: <Widget>[
           Text('Row One'),
           Text('Row Two'),
           Text('Row Three'),
           Text('Row Four'),
           Text('Row Five')
         ],
       ),
    );
  }
}

使用CustomScrollView

CustomScrollView的子widget必须使用sliver来自定义滚动模型(效果)的widget。它可以包含多种滚动模型。借助它可以实现多种混合风格的ListView,例如常见的卡片风格与列表风格混合在一起的效果。在Flutter中,Sliver通常指具有特定滚动效果的可滚动块。ListView和GridView都有对应的Sliver实现,例如SliverList,SliverGrid。Sliver本身不包含滚动交互模型(Scrollable Widget),所以在CustomScrollView中使用多个Sliver,CustomScrollView最终只会用一种统一的滑动效果,不会有iOS中常见的手势冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class _CustomScrollViewSampleAppPageState extends State<CustomScrollViewSampleAppPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
       appBar: AppBar(
         title: Text('Custom ScrollView'),
       ),
       body: CustomScrollView(
         // 相当于iOS中的bunces属性,当设置为true时,未超过屏幕高度,不能滚动
         shrinkWrap: false,
         slivers: <Widget>[
           SliverPadding(
             padding: EdgeInsets.all(10),
             sliver: SliverList(
               delegate: SliverChildListDelegate(<Widget>[
                 Text('first line'),
                 Text('Second line'),
                 Text('Third line'),
                 Text('Fourth line'),
                 Text('Fifth line'),
                 Text('Sixth line'),
               ]),
             ),
           )
         ],
       ),
    );
  }
}

本文Demo