[译]Flutter - 掌握ListView

原文在这里git

介绍

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

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

准备工做

我(做者)假设你已经把Flutter的开发环境都搭建好了。并且你也对Flutter有基本的了解。若是不是,那么如下的链接能够帮助你:markdown

我在使用的是Android Studio,若是你用的是其余的IDE也OK。app

开始

新建一个叫作flutter_listview的项目。less

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

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方法,这里的代码就是咱们后面要替换掉的。ide

ListView的基本类型

静态ListView

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

替换_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的方方面面。你已经能够本身写一个知足本身须要的了。

代码在这里

相关文章
相关标签/搜索