Flutter-掌握ListView

5次阅读

共计 9736 个字符,预计需要花费 25 分钟才能阅读完成。

原文在这里。

介绍

如果你了解 Android 或者 iOS 的开发,你会喜欢 Flutter ListView 的简洁。本文中,我们就是用几个简单的例子来实现一些很常用的情景。

首先,来看看 ListView 的几种类型。之后介绍如何处理每个 item 的 style。最后,如何添加和删除 item。

准备工作

我(作者)假设你已经把 Flutter 的开发环境都搭建好了。而且你也对 Flutter 有基本的了解。如果不是,那么以下的连接可以帮助你:

  • 配置 Flutter
  • Flutter 入门:了解 Widget
  • Flutter 入门:构建布局
  • Flutter 入门:处理输入

我在使用的是 Android Studio,如果你用的是其他的 IDE 也 OK。

开始

新建一个叫做 flutter_listview 的项目。

打开 main.dart 文件,使用下面的代码替换掉之前的:

import 'package:flutter/material.dart';

    void main() => runApp(MyApp());

    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'ListViews',
          theme: ThemeData(primarySwatch: Colors.teal,),
          home: Scaffold(appBar: AppBar(title: Text('ListViews')),
            body: BodyLayout(),),
        );
      }
    }

    class BodyLayout extends StatelessWidget {
      @override
      Widget build(BuildContext context) {return _myListView(context);
      }
    }

    // replace this function with the code in the examples
    Widget _myListView(BuildContext context) {return ListView();
    }

注意最后的 _myListView 方法,这里的代码就是我们后面要替换掉的。

ListView 的基本类型

静态 ListView

如果你有一列数据,而且不会发生太大的更改,那么静态 ListView 就是最好的选择了。尤其是对于设置这样的页面来说最合适不过。

替换 _myListView 的代码:

Widget _myListView(BuildContext context) {
      return ListView(
        children: <Widget>[
          ListTile(title: Text('Sun'),
          ),
          ListTile(title: Text('Moon'),
          ),
          ListTile(title: Text('Star'),
          ),
        ],
      );
    }

运行代码,会是这个样子的。(虽然 hot reload 一般没什么问题,不过偶尔还是需要用 hot restart 甚至关掉重新运行才行)。

代码的三层关系就是 ListView 的 children 是一个包含了三个 ListTile 的数组。ListTile是定义好的,专门处理 ListView 的 item 的布局的。我们上面的例子里面只包含了一个 title 属性。下面的例子会包含一些样式。

如果要给 ListView 添加分割线,那么可以使用ListTile.divideTiles

Widget _myListView(BuildContext context) {
    return ListView(
        children: ListTile.divideTiles(
          context: context,
          tiles: [
            ListTile(title: Text('Sun'),
            ),
            ListTile(title: Text('Moon'),
            ),
            ListTile(title: Text('Star'),
            ),
          ],
        ).toList(),);
}

仔细看,你就会发现分割线已经在了。

动态 ListView

静态 ListView 的所有元素都一起和 ListView 创建好了。这对于很少数据的处理是可以的。下面就来介绍一下处理很多数据的时候使用的 ListView.builder()。这个方法只会处理要在屏幕上显示的数据,就和 Android 的RecyclerView 很类似,不过用起来更简单。

使用以下的代码替换 _myListView 方法:

Widget _myListView(BuildContext context) {

      // backing data
      final europeanCountries = ['Albania', 'Andorra', 'Armenia', 'Austria', 
        'Azerbaijan', 'Belarus', 'Belgium', 'Bosnia and Herzegovina', 'Bulgaria',
        'Croatia', 'Cyprus', 'Czech Republic', 'Denmark', 'Estonia', 'Finland',
        'France', 'Georgia', 'Germany', 'Greece', 'Hungary', 'Iceland', 'Ireland',
        'Italy', 'Kazakhstan', 'Kosovo', 'Latvia', 'Liechtenstein', 'Lithuania',
        'Luxembourg', 'Macedonia', 'Malta', 'Moldova', 'Monaco', 'Montenegro',
        'Netherlands', 'Norway', 'Poland', 'Portugal', 'Romania', 'Russia',
        'San Marino', 'Serbia', 'Slovakia', 'Slovenia', 'Spain', 'Sweden', 
        'Switzerland', 'Turkey', 'Ukraine', 'United Kingdom', 'Vatican City'];

      return ListView.builder(
        itemCount: europeanCountries.length,
        itemBuilder: (context, index) {
          return ListTile(title: Text(europeanCountries[index]),
          );
        },
      );

    }

运行之后:

itemCount会告诉 ListView 有多少数据要显示,itemBuilder来动态的处理每一个要显示在 ListView 上的数据。这个方法的参数 contextBuildContext类型的,另一个参数 index 则告诉用户第几个数据要显示在屏幕上了。

无限 ListView

很多人都有过在 Android 或者 iOS 上构建无限滚动 ListView 的痛苦经历。Flutter 也让这个更加简单。只要删除 itemCount 就可以。我们改造一下代码,让每一个 ListTile 显示出当前的 index 值。

    Widget _myListView(BuildContext context) {
        return ListView.builder(itemBuilder: (context, index) {
                return ListTile(title: Text('row $index'),
                );
            },
        );
    }

你可以一直滚动,不会有终点。

如果你要显示分割先,只需要 ListView.separated 构造方法。

    Widget _myListView(BuildContext context) {
      return ListView.separated(
        itemCount: 1000,
        itemBuilder: (context, index) {
          return ListTile(title: Text('row $index'),
          );
        },
        separatorBuilder: (context, index) {return Divider();
        },
      );
    }

ListView 里再次显示除了一条模糊不清的分割线。如果要修改的话可以使用 Divider 来更改分割线的高度颜色等参数。

横向 ListView

也很容易可以新建一个横向滚动的 ListView。只需要给定 scrollDirection 是横向的。不过还需要搭配一点定制的布局。

   Widget _myListView(BuildContext context) {
      return ListView.builder(
        scrollDirection: Axis.horizontal,
        itemBuilder: (context, index) {
          return Container(margin: const EdgeInsets.symmetric(horizontal: 1.0),
            color: Colors.tealAccent,
            child: Text('$index'),
          );
        },
      );
    }

样式

我们上面已经了解了所有的 ListView 类型。但是都不好看。Flutter 提供了很多的选项可以让 ListView 好看。

定制 ListTile

ListTile基本可以覆盖常规使用的全部定制内容。比如副标题,图片和 icon 等。

    Widget _myListView(BuildContext context) {
      return ListView(
        children: <Widget>[
          ListTile(leading: Icon(Icons.wb_sunny),
            title: Text('Sun'),
          ),
          ListTile(leading: Icon(Icons.brightness_3),
            title: Text('Moon'),
          ),
          ListTile(leading: Icon(Icons.star),
            title: Text('Star'),
          ),
        ],
      );
    }

leading是用来在 ListTile 的开始添加 icon 或者图片的

对应的还有 tailing 属性

   ListTile(leading: Icon(Icons.wb_sunny),
      title: Text('Sun'),
      trailing: Icon(Icons.keyboard_arrow_right),
    ),

tailing的箭头图标让人们以为可以点击。其实还不能点击。我们来看看如何响应用户的点击。也很简单。替换 _myListView() 方法的代码:

    Widget _myListView(BuildContext context) {
      return ListView(
        children: <Widget>[
          ListTile(
            leading: CircleAvatar(backgroundImage: AssetImage('assets/sun.jpg'),
            ),
            title: Text('Sun'),
          ),
          ListTile(
            leading: CircleAvatar(backgroundImage: AssetImage('assets/moon.jpg'),
            ),
            title: Text('Moon'),
          ),
          ListTile(
            leading: CircleAvatar(backgroundImage: AssetImage('assets/stars.jpg'),
            ),
            title: Text('Star'),
          ),
        ],
      );
    }

现在还不能用,我们先添加一些图片。

这里也可以使用 NetworkImage(imageUrl) 代替 AssetImage(path)。暂时先用 AssetImage,这样内容都在 app 里面了。在项目更目录下新建一个assets 目录,把下面的图片都加进去。

pubspec.yaml 文件注册这个目录

flutter:
    assets:
        - assets/

重新运行 app(停止了再运行),会看到这样的界面:

最后再来看看副标题:

    ListTile(
      leading: CircleAvatar(backgroundImage: AssetImage('assets/sun.jpg'),
      ),
      title: Text('Sun'),
      subtitle: Text('93 million miles away'), //           <-- subtitle
    ),

运行结果:

卡片(Card)

Card 是让你的列表看起来酷炫最简单的方法了。只需要让 Card 包裹 ListTile。使用下面的代码替换 _myListView 方法

    Widget _myListView(BuildContext context) {

      final titles = ['bike', 'boat', 'bus', 'car',
      'railway', 'run', 'subway', 'transit', 'walk'];

      final icons = [Icons.directions_bike, Icons.directions_boat,
      Icons.directions_bus, Icons.directions_car, Icons.directions_railway,
      Icons.directions_run, Icons.directions_subway, Icons.directions_transit,
      Icons.directions_walk];

      return ListView.builder(
        itemCount: titles.length,
        itemBuilder: (context, index) {
          return Card( //                           <-- Card widget
            child: ListTile(leading: Icon(icons[index]),
              title: Text(titles[index]),
            ),
          );
        },
      );
    }

你可以修改 elevation 属性来修改阴影,也可以试一下 shapemargin看看有什么效果。

定制列表条目

如果一个 ListTile 不能满足你的要求,你完全可以定制自己的。ListView 需要的只不过是一组组件(widget)。任何组件都可以。我最近处理的每个条目多列的需求可以拿来做一个例子。

    Widget _myListView(BuildContext context) {

      // the Expanded widget lets the columns share the space
      Widget column = Expanded(
        child: Column(
          // align the text to the left instead of centered
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[Text('Title', style: TextStyle(fontSize: 16),),
            Text('subtitle'),
          ],
        ),
      );

      return ListView.builder(itemBuilder: (context, index) {
          return Card(
            child: Padding(padding: const EdgeInsets.all(8.0),
              child: Row(
                children: <Widget>[
                  column,
                  column,
                ],
              ),
            ),
          );
        },
      );
    }

触摸检测

如果你想要 ListTile,只需要添加 onTap 或者 onLongPress 回调。

替换 _myListViw 方法代码:

    Widget _myListView(BuildContext context) {
      return ListView(
        children: <Widget>[
          ListTile(title: Text('Sun'),
            trailing: Icon(Icons.keyboard_arrow_right),
            onTap: () {print('Sun');
            },
          ),
          ListTile(title: Text('Moon'),
            trailing: Icon(Icons.keyboard_arrow_right),
            onTap: () {print('Moon');
            },
          ),
          ListTile(title: Text('Star'),
            trailing: Icon(Icons.keyboard_arrow_right),
            onTap: () {print('Star');
            },
          ),
        ],
      );
    }

有了 onTap 方法,我们就可以响应用户的点击了。这里我们 print 一些字符串。

在实际开发中,更有可能是点击了一行就跳转到别的页面了。可以参考响应用户输入。

如果你也没有使用 ListTile,而是使用了自己定制的一套组件。那么最好是做一个重构,比如本利就把他们放在一个 InkWell 的定制组件里了。

     return ListView.builder(itemBuilder: (context, index) {
          return Card(
            child: InkWell(onTap: () {print('tapped');
              },
              child: Padding(padding: const EdgeInsets.all(8.0),
                child: Row(
                  children: <Widget>[
                    column,
                    column,
                  ],
                ),
              ),
            ),
          );
        },
      );

当然如何重构的选项很多,上栗也不是唯一的标准。

更新数据

添加、删除 ListView 的行

很容易可以在 ListView 里更新数据。只需要把 ListView 放在一个 StatefulWidget 里,并在需要更新的时候调用 setState 方法。

比如下面的例子里有一个 BodyLayout_myListViw()

    class BodyLayout extends StatefulWidget {
      @override
      BodyLayoutState createState() {return new BodyLayoutState();
      }
    }

    class BodyLayoutState extends State<BodyLayout> {List<String> titles = ['Sun', 'Moon', 'Star'];

      @override
      Widget build(BuildContext context) {return _myListView();
      }

      Widget _myListView() {
        return ListView.builder(
          itemCount: titles.length,
          itemBuilder: (context, index) {final item = titles[index];
            return Card(
              child: ListTile(title: Text(item),

                onTap: () { //                                  <-- onTap
                  setState(() {titles.insert(index, 'Planet');
                  });
                },

                onLongPress: () { //                            <-- onLongPress
                  setState(() {titles.removeAt(index);
                  });
                },

              ),
            );
          },
        );
      }
    }

点击一行,就在那一行的 index 上添加一行,长按就删除一行。

在 AnimatedList 里添加、删除行

BodyLayoutState 的代码替换为下面的内容:

    class BodyLayoutState extends State<BodyLayout> {

      // The GlobalKey keeps track of the visible state of the list items
      // while they are being animated.
      final GlobalKey<AnimatedListState> _listKey = GlobalKey();

      // backing data
      List<String> _data = ['Sun', 'Moon', 'Star'];

      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            SizedBox(
              height: 300,
              child: AnimatedList(
                // Give the Animated list the global key
                key: _listKey,
                initialItemCount: _data.length,
                // Similar to ListView itemBuilder, but AnimatedList has
                // an additional animation parameter.
                itemBuilder: (context, index, animation) {
                  // Breaking the row widget out as a method so that we can
                  // share it with the _removeSingleItem() method.
                  return _buildItem(_data[index], animation);
                },
              ),
            ),
            RaisedButton(child: Text('Insert item', style: TextStyle(fontSize: 20)),
              onPressed: () {_insertSingleItem();
              },
            ),
            RaisedButton(child: Text('Remove item', style: TextStyle(fontSize: 20)),
              onPressed: () {_removeSingleItem();
              },
            )
          ],
        );
      }

      // This is the animated row with the Card.
      Widget _buildItem(String item, Animation animation) {
        return SizeTransition(
          sizeFactor: animation,
          child: Card(
            child: ListTile(
              title: Text(
                item,
                style: TextStyle(fontSize: 20),
              ),
            ),
          ),
        );
      }

      void _insertSingleItem() {
        String newItem = "Planet";
        // Arbitrary location for demonstration purposes
        int insertIndex = 2;
        // Add the item to the data list.
        _data.insert(insertIndex, newItem);
        // Add the item visually to the AnimatedList.
        _listKey.currentState.insertItem(insertIndex);
      }

      void _removeSingleItem() {
        int removeIndex = 2;
        // Remove item from data list but keep copy to give to the animation.
        String removedItem = _data.removeAt(removeIndex);
        // This builder is just for showing the row while it is still
        // animating away. The item is already gone from the data list.
        AnimatedListRemovedItemBuilder builder = (context, animation) {return _buildItem(removedItem, animation);
        };
        // Remove the item visually from the AnimatedList.
        _listKey.currentState.removeItem(removeIndex, builder);
      }
    }

在代码的注释中添加了很多说明。可以总结为一下几点

  • AnimatedList 需要用到GlobalKey。每次动画的时候都需要更新 AnimatedList 用到的数据和 GlobalKey。
  • 行组件是 stateless 的。如果是有状态的,那么就需要安排一个 Key 给他们。这样可以让 Flutter 快速的发现哪里发生了更新。这个来自 Flutter 团队的视频可以帮你了解更多。
  • 本例我是用了 SizedTransition 动画,文档里还有更多的可以用。

最后

我们已经了解了 ListView 的方方面面。你已经可以自己写一个满足自己需要的了。

代码在这里。

正文完
 0