Flutter - 掌握ListView

原文在這裏git

介紹

若是你瞭解Android或者iOS的開發,你會喜歡Flutter ListView的簡潔。本文中,咱們就是用幾個簡單的例子來實現一些很經常使用的情景。github

首先,來看看ListView的幾種類型。以後介紹如何處理每一個item的style。最後,如何添加和刪除item。數組

準備工做

我(做者)假設你已經把Flutter的開發環境都搭建好了。並且你也對Flutter有基本的瞭解。若是不是,那麼如下的鏈接能夠幫助你:app

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

開始

新建一個叫作flutter_listview的項目。dom

打開main.dart文件,使用下面的代碼替換掉以前的:ide

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的代碼:ui

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的方方面面。你已經能夠本身寫一個知足本身須要的了。

代碼在這裏

相關文章
相關標籤/搜索