Flutter(十)之Flutter的滾動Widget

coderwhy發表於2019-10-06

前言一:接下來一段時間我會陸續更新一些列Flutter文字教程

更新進度: 每週至少兩篇;

更新地點: 首發於公眾號,第二天更新於掘金、思否、開發者頭條等地方;

更多交流: 可以新增我的微信 372623326,關注我的微博:coderwhy

希望大家可以 幫忙轉發,點選在看,給我更多的創作動力。

前言二:列表是移動端經常使用的一種檢視展示方式,在Flutter中提供了ListView和GridView。

為了可能展示出更好的效果,我這裡提供了一段Json資料,所以我們可以先學習一下Json解析。

一. JSON讀取和解析

在開發中,我們經常會使用本地JSON或者從伺服器請求資料後回去到JSON,拿到JSON後通常會將JSON轉成Model物件來進行後續的操作,因為這樣操作更加的方便,也更加的安全。

所以學習JSON的相關操作以及讀取JSON後如何轉成Model物件對於Flutter開發也非常重要。

1.1. JSON資源配置

JSON也屬於一種資源,所以在使用之前需要先進行相關的配置

我們之前在學習使用Image元件時,用到了本地圖片,本地圖片必須在pubspec.yaml中進行配置:

image-20190904144032396

1.2. JSON讀取解析

JSON資源讀取

如果我們希望讀取JSON資源,可以使用package:flutter/services.dart包中的rootBundle

rootBundle中有一個loadString方法,可以去載入JSON資源

  • 但是注意,檢視該方法的原始碼,你會發現這個操作是一個非同步的。
  • 關於Future和async,這裡就不再展開講解,可以去檢視之前的dart語法。
Future<String> loadString(String key, { bool cache = true }) async {
  ...省略具體程式碼,可以自行檢視原始碼
}
複製程式碼

程式碼如下:(不要試圖拷貝這個程式碼去執行,是沒辦法執行的)

import 'package:flutter/services.dart' show rootBundle;

// 列印讀取的結果是一個字串
rootBundle.loadString("assets/yz.json").then((value) => print(value));
複製程式碼

JSON字串轉化

拿到JSON字串後,我們需要將其轉成成我們熟悉的List和Map型別。

我們可以通過dart:convert包中的json.decode方法將其進行轉化

程式碼如下:

// 1.讀取json檔案
String jsonString = await rootBundle.loadString("assets/yz.json");

// 2.轉成List或Map型別
final jsonResult = json.decode(jsonString);
複製程式碼

物件Model定義

將JSON轉成了List和Map型別後,就可以將List中的一個個Map轉成Model物件,所以我們需要定義自己的Model

class Anchor {
  String nickname;
  String roomName;
  String imageUrl;

  Anchor({
    this.nickname,
    this.roomName,
    this.imageUrl
  });

  Anchor.withMap(Map<String, dynamic> parsedMap) {
    this.nickname = parsedMap["nickname"];
    this.roomName = parsedMap["roomName"];
    this.imageUrl = parsedMap["roomSrc"];
  }
}
複製程式碼

1.3. JSON解析程式碼

上面我們給出瞭解析的一個個步驟,下面我們給出完整的程式碼邏輯

這裡我單獨建立了一個anchor.dart的檔案,在其中定義了所有的相關程式碼:

  • 之後外界只需要呼叫我內部的getAnchors就可以獲取到解析後的資料了
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';

class Anchor {
  String nickname;
  String roomName;
  String imageUrl;

  Anchor({
    this.nickname,
    this.roomName,
    this.imageUrl
  });

  Anchor.withMap(Map<String, dynamic> parsedMap) {
    this.nickname = parsedMap["nickname"];
    this.roomName = parsedMap["roomName"];
    this.imageUrl = parsedMap["roomSrc"];
  }
}

Future<List<Anchor>> getAnchors() async {
  // 1.讀取json檔案
  String jsonString = await rootBundle.loadString("assets/yz.json");

  // 2.轉成List或Map型別
  final jsonResult = json.decode(jsonString);

  // 3.遍歷List,並且轉成Anchor物件放到另一個List中
  List<Anchor> anchors = new List();
  for (Map<String, dynamic> map in jsonResult) {
    anchors.add(Anchor.withMap(map));
  }
  return anchors;
} 
複製程式碼

二. ListView元件

移動端資料量比較大時,我們都是通過列表來進行展示的,比如商品資料、聊天列表、通訊錄、朋友圈等。

在Android中,我們可以使用ListView或RecyclerView來實現,在iOS中,我們可以通過UITableView來實現。

在Flutter中,我們也有對應的列表Widget,就是ListView。

2.1. ListView基礎

2.1.1 ListView基本使用

ListView可以沿一個方向(垂直或水平方向,預設是垂直方向)來排列其所有子Widget。

一種最簡單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可。

我們來看一下直接使用ListView的程式碼演練:

  • 為了讓文字之間有一些間距,我使用了Padding Widget
class MyHomeBody extends StatelessWidget {
  final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("人的一切痛苦,本質上都是對自己無能的憤怒。", style: textStyle),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("人活在世界上,不可以有偏差;而且多少要費點勁兒,才能把自己保持到理性的軌道上。", style: textStyle),
        ),
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text("我活在世上,無非想要明白些道理,遇見些有趣的事。", style: textStyle),
        )
      ],
    );
  }
}
複製程式碼

2.2.2. ListTile的使用

在開發中,我們經常見到一種列表,有一個圖示或圖片(Icon),有一個標題(Title),有一個子標題(Subtitle),還有尾部一個圖示(Icon)。

這個時候,我們可以使用ListTile來實現:

class MyHomeBody extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: <Widget>[
        ListTile(
          leading: Icon(Icons.people, size: 36,),
          title: Text("聯絡人"),
          subtitle: Text("聯絡人資訊"),
          trailing: Icon(Icons.arrow_forward_ios),
        ),
        ListTile(
          leading: Icon(Icons.email, size: 36,),
          title: Text("郵箱"),
          subtitle: Text("郵箱地址資訊"),
          trailing: Icon(Icons.arrow_forward_ios),
        ),
        ListTile(
          leading: Icon(Icons.message, size: 36,),
          title: Text("訊息"),
          subtitle: Text("訊息詳情資訊"),
          trailing: Icon(Icons.arrow_forward_ios),
        ),
        ListTile(
          leading: Icon(Icons.map, size: 36,),
          title: Text("地址"),
          subtitle: Text("地址詳情資訊"),
          trailing: Icon(Icons.arrow_forward_ios),
        )
      ],
    );
  }
}
複製程式碼

image-20190904154142133

2.2.3. 垂直方向滾動

我們可以通過設定 scrollDirection 引數來控制檢視的滾動方向。

我們通過下面的程式碼實現一個水平滾動的內容:

  • 這裡需要注意,我們需要給Container設定width,否則它是沒有寬度的,就不能正常顯示。
  • 或者我們也可以給ListView設定一個itemExtent,該屬性會設定滾動方向上每個item所佔據的寬度。
class MyHomeBody extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return ListView(
      scrollDirection: Axis.horizontal,
      itemExtent: 200,
      children: <Widget>[
        Container(color: Colors.red, width: 200),
        Container(color: Colors.green, width: 200),
        Container(color: Colors.blue, width: 200),
        Container(color: Colors.purple, width: 200),
        Container(color: Colors.orange, width: 200),
      ],
    );
  }
}
複製程式碼

image-20190904155511007

2.2. ListView.build

通過建構函式中的children傳入所有的子Widget有一個問題:預設會建立出所有的子Widget。

但是對於使用者來說,一次性構建出所有的Widget並不會有什麼差異,但是對於我們的程式來說會產生效能問題,而且會增加首屏的渲染時間。

我們可以ListView.build來構建子Widget,提供效能。

2.2.1. ListView.build基本使用

ListView.build適用於子Widget比較多的場景,該建構函式將建立子Widget交給了一個抽象的方法,交給ListView進行管理,ListView會在真正需要的時候去建立子Widget,而不是一開始就全部初始化好。

該方法有兩個重要引數:

  • itemBuilder:列表項建立的方法。當列表滾動到對應位置的時候,ListView會自動呼叫該方法來建立對應的子Widget。型別是IndexedWidgetBuilder,是一個函式型別。
  • itemCount:表示列表項的數量,如果為空,則表示ListView為無限列表。

我們還是通過一個簡單的案例來認識它:

class MyHomeBody extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 100,
      itemExtent: 80,
      itemBuilder: (BuildContext context, int index) {
        return ListTile(title: Text("標題$index"), subtitle: Text("詳情內容$index"));
      }
    );
  }
}
複製程式碼

image-20190904201433097

2.2.2. ListView.build動態資料

在之前,我們搞了一個yz.json資料,我們現在動態的來通過JSON資料展示一個列表。

思考:這個時候是否依然可以使用StatelessWidget:

答案:不可以,因為當前我們的資料是非同步載入的,剛開始介面並不會展示資料(沒有資料),後面從JSON中載入出來資料(有資料)後,再次展示載入的資料。

  • 這裡是有狀態的變化的,從無資料,到有資料的變化。
  • 這個時候,我們需要使用StatefullWidget來管理元件。

展示程式碼如下:

import 'model/anchor.dart';

...省略中間程式碼
class MyHomeBody extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return MyHomeBodyState();
  }
}

class MyHomeBodyState extends State<MyHomeBody> {
  List<Anchor> anchors = [];

  // 在初始化狀態的方法中載入資料
  @override
  void initState() {
    getAnchors().then((anchors) {
      setState(() {
        this.anchors = anchors;
      });
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemBuilder: (BuildContext context, int index) {
        return Padding(
          padding: EdgeInsets.all(8),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Image.network(
                anchors[index].imageUrl,
                fit: BoxFit.fitWidth,
                width: MediaQuery.of(context).size.width,
              ),
              SizedBox(height: 8),
              Text(anchors[index].nickname, style: TextStyle(fontSize: 20),),
              SizedBox(height: 5),
              Text(anchors[index].roomName)
            ],
          ),
        );
      },
    );
  }
}
複製程式碼

image-20190904202722900

2.2.3. ListView.separated

ListView.separated可以生成列表項之間的分割器,它除了比ListView.builder多了一個separatorBuilder引數,該引數是一個分割器生成器。

下面我們看一個例子:奇數行新增一條藍色下劃線,偶數行新增一條紅色下劃線:

class MySeparatedDemo extends StatelessWidget {
  Divider blueColor = Divider(color: Colors.blue);
  Divider redColor = Divider(color: Colors.red);

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemBuilder: (BuildContext context, int index) {
        return ListTile(
          leading: Icon(Icons.people),
          title: Text("聯絡人${index+1}"),
          subtitle: Text("聯絡人電話${index+1}"),
        );
      },
      separatorBuilder: (BuildContext context, int index) {
        return index % 2 == 0 ? redColor : blueColor;
      },
      itemCount: 100
    );
  }
}
複製程式碼

image-20190904203539235

三. GridView元件

GridView用於展示多列的展示,在開發中也非常常見,比如直播App中的主播列表、電商中的商品列表等等。

在Flutter中我們可以使用GridView來實現,使用方式和ListView也比較相似。

3.1. GridView建構函式

我們先學習GridView建構函式的使用方法

一種使用GridView的方式就是使用建構函式來建立,和ListView對比有一個特殊的引數:gridDelegate

gridDelegate用於控制交叉軸的item數量或者寬度,需要傳入的型別是SliverGridDelegate,但是它是一個抽象類,所以我們需要傳入它的子類:

SliverGridDelegateWithFixedCrossAxisCount

SliverGridDelegateWithFixedCrossAxisCount({
  @required double crossAxisCount, // 交叉軸的item個數
  double mainAxisSpacing = 0.0, // 主軸的間距
  double crossAxisSpacing = 0.0, // 交叉軸的間距
  double childAspectRatio = 1.0, // 子Widget的寬高比
})
複製程式碼

程式碼演練:

class MyGridCountDemo extends StatelessWidget {

  List<Widget> getGridWidgets() {
    return List.generate(100, (index) {
      return Container(
        color: Colors.purple,
        alignment: Alignment(0, 0),
        child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return GridView(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        mainAxisSpacing: 10,
        crossAxisSpacing: 10,
        childAspectRatio: 1.0
      ),
      children: getGridWidgets(),
    );
  }
}
複製程式碼

image-20190905150128115

SliverGridDelegateWithMaxCrossAxisExtent

SliverGridDelegateWithMaxCrossAxisExtent({
  double maxCrossAxisExtent, // 交叉軸的item寬度
  double mainAxisSpacing = 0.0, // 主軸的間距
  double crossAxisSpacing = 0.0, // 交叉軸的間距
  double childAspectRatio = 1.0, // 子Widget的寬高比
})
複製程式碼

程式碼演練:

class MyGridExtentDemo extends StatelessWidget {

  List<Widget> getGridWidgets() {
    return List.generate(100, (index) {
      return Container(
        color: Colors.purple,
        alignment: Alignment(0, 0),
        child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return GridView(
      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 150,
        mainAxisSpacing: 10,
        crossAxisSpacing: 10,
        childAspectRatio: 1.0
      ),
      children: getGridWidgets(),
    );
  }
}
複製程式碼

image-20190905150313598

前面兩種方式也可以不設定delegate

可以分別使用:GridView.count建構函式GridView.extent建構函式實現相同的效果,這裡不再贅述。

3.2. GridView.build

和ListView一樣,使用建構函式會一次性建立所有的子Widget,會帶來效能問題,所以我們可以使用GridView.build來交給GridView自己管理需要建立的子Widget。

我們直接使用之前的資料來進行程式碼演練:

class _GridViewBuildDemoState extends State<GridViewBuildDemo> {
  List<Anchor> anchors = [];

  @override
  void initState() {
    getAnchors().then((anchors) {
      setState(() {
        this.anchors = anchors;
      });
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: GridView.builder(
        shrinkWrap: true,
        physics: ClampingScrollPhysics(),
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          mainAxisSpacing: 10,
          crossAxisSpacing: 10,
          childAspectRatio: 1.2
        ),
        itemCount: anchors.length,
        itemBuilder: (BuildContext context, int index) {
          return Container(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Image.network(anchors[index].imageUrl),
                SizedBox(height: 5),
                Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
                Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
              ],
            ),
          );
        }
      ),
    );
  }
}
複製程式碼

image-20190905153201888

四. Slivers

我們考慮一個這樣的佈局:一個滑動的檢視中包括一個標題檢視(HeaderView),一個列表檢視(ListView),一個網格檢視(GridView)。

我們怎麼可以讓它們做到統一的滑動效果呢?使用前面的滾動是很難做到的。

Flutter中有一個可以完成這樣滾動效果的Widget:CustomScrollView,可以統一管理多個滾動檢視。

在CustomScrollView中,每一個獨立的,可滾動的Widget被稱之為Sliver。

補充:Sliver可以翻譯成裂片、薄片,你可以將每一個獨立的滾動檢視當做一個小裂片。

4.1. Slivers的基本使用

因為我們需要把很多的Sliver放在一個CustomScrollView中,所以CustomScrollView有一個slivers屬性,裡面讓我們放對應的一些Sliver:

  • SliverList:類似於我們之前使用過的ListView;
  • SliverFixedExtentList:類似於SliverList,知識可以設定滾動的高度;
  • SliverGrid:類似於我們之前使用過的GridView;
  • SliverPadding:設定Sliver的內邊距,因為可能要單獨給Sliver設定內邊距;
  • SliverAppBar:新增一個AppBar,通常用來作為CustomScrollView的HeaderView;
  • SliverSafeArea:設定內容顯示在安全區域(比如不讓齊劉海擋住我們的內容)

我們簡單演示一下:SliverGrid+SliverPadding+SliverSafeArea的組合

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: <Widget>[
        SliverSafeArea(
          sliver: SliverPadding(
            padding: EdgeInsets.all(8),
            sliver: SliverGrid(
              gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
                  return Container(
                    alignment: Alignment(0, 0),
                    color: Colors.orange,
                    child: Text("item$index"),
                  );
                },
                childCount: 20
              ),
            ),
          ),
        )
      ],
    );
  }
}

複製程式碼

image-20191001180054605

4.2. Slivers的組合使用

這裡我使用官方的示例程式,將SliverAppBar+SliverGrid+SliverFixedExtentList做出如下介面:

class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return showCustomScrollView();
  }

  Widget showCustomScrollView() {
    return new CustomScrollView(
      slivers: <Widget>[
        const SliverAppBar(
          expandedHeight: 250.0,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('Coderwhy Demo'),
            background: Image(
              image: NetworkImage(
                "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
              ),
              fit: BoxFit.cover,
            ),
          ),
        ),
        new SliverGrid(
          gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 200.0,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 4.0,
          ),
          delegate: new SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return new Container(
                alignment: Alignment.center,
                color: Colors.teal[100 * (index % 9)],
                child: new Text('grid item $index'),
              );
            },
            childCount: 10,
          ),
        ),
        SliverFixedExtentList(
          itemExtent: 50.0,
          delegate: SliverChildBuilderDelegate(
                (BuildContext context, int index) {
              return new Container(
                alignment: Alignment.center,
                color: Colors.lightBlue[100 * (index % 9)],
                child: new Text('list item $index'),
              );
            },
            childCount: 20
          ),
        ),
      ],
    );
  }
}

複製程式碼

image-20191001181959972

五. 監聽滾動事件

對於滾動的檢視,我們經常需要監聽它的一些滾動事件,在監聽到的時候去做對應的一些事情。

比如檢視滾動到底部時,我們可能希望做上拉載入更多;

比如滾動到一定位置時顯示一個回到頂部的按鈕,點選回到頂部的按鈕,回到頂部;

比如監聽滾什麼時候開始,什麼時候結束;

在Flutter中監聽滾動相關的內容由兩部分組成:ScrollController和ScrollNotification。

5.1. ScrollController

在Flutter中,Widget並不是最終渲染到螢幕上的元素(真正渲染的是RenderObject),因此通常這種監聽事件以及相關的資訊並不能直接從Widget中獲取,而是必須通過對應的Widget的Controller來實現。

ListView、GridView的元件控制器是ScrollController,我們可以通過它來獲取檢視的滾動資訊,並且可以呼叫裡面的方法來更新檢視的滾動位置。

另外,通常情況下,我們會根據滾動的位置來改變一些Widget的狀態資訊,所以ScrollController通常會和StatefullWidget一起來使用,並且會在其中控制它的初始化、監聽、銷燬等事件。

我們來做一個案例,當滾動到1000位置的時候,顯示一個回到頂部的按鈕:

  • jumpTo(double offset)animateTo(double offset,...):這兩個方法用於跳轉到指定的位置,它們不同之處在於,後者在跳轉時會執行一個動畫,而前者不會。
  • ScrollController間接繼承自Listenable,我們可以根據ScrollController來監聽滾動事件。
class MyHomePage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  ScrollController _controller;
  bool _isShowTop = false;
  
  @override
  void initState() {
    // 初始化ScrollController
    _controller = ScrollController();
    
    // 監聽滾動
    _controller.addListener(() {
      var tempSsShowTop = _controller.offset >= 1000;
      if (tempSsShowTop != _isShowTop) {
        setState(() {
          _isShowTop = tempSsShowTop;
        });
      }
    });
    
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ListView展示"),
      ),
      body: ListView.builder(
        itemCount: 100,
        itemExtent: 60,
        controller: _controller,
        itemBuilder: (BuildContext context, int index) {
          return ListTile(title: Text("item$index"));
        }
      ),
      floatingActionButton: !_isShowTop ? null : FloatingActionButton(
        child: Icon(Icons.arrow_upward),
        onPressed: () {
          _controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
        },
      ),
    );
  }
}
複製程式碼

image-20190905182638821

5.2. NotificationListener

如果我們希望監聽什麼時候開始滾動,什麼時候結束滾動,這個時候我們可以通過NotificationListener

  • NotificationListener是一個Widget,模板引數T是想監聽的通知型別,如果省略,則所有型別通知都會被監聽,如果指定特定型別,則只有該型別的通知會被監聽。
  • NotificationListener需要一個onNotification回撥函式,用於實現監聽處理邏輯。
  • 該回撥可以返回一個布林值,代表是否阻止該事件繼續向上冒泡,如果為true時,則冒泡終止,事件停止向上傳播,如果不返回或者返回值為false 時,則冒泡繼續。

案例: 列表滾動, 並且在中間顯示滾動進度

class MyHomeNotificationDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MyHomeNotificationDemoState();
}

class MyHomeNotificationDemoState extends State<MyHomeNotificationDemo> {
  int _progress = 0;

  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: (ScrollNotification notification) {
        // 1.判斷監聽事件的型別
        if (notification is ScrollStartNotification) {
          print("開始滾動.....");
        } else if (notification is ScrollUpdateNotification) {
          // 當前滾動的位置和總長度
          final currentPixel = notification.metrics.pixels;
          final totalPixel = notification.metrics.maxScrollExtent;
          double progress = currentPixel / totalPixel;
          setState(() {
            _progress = (progress * 100).toInt();
          });
          print("正在滾動:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
        } else if (notification is ScrollEndNotification) {
          print("結束滾動....");
        }
        return false;
      },
      child: Stack(
        alignment: Alignment(.9, .9),
        children: <Widget>[
          ListView.builder(
            itemCount: 100,
            itemExtent: 60,
            itemBuilder: (BuildContext context, int index) {
              return ListTile(title: Text("item$index"));
            }
          ),
          CircleAvatar(
            radius: 30,
            child: Text("$_progress%"),
            backgroundColor: Colors.black54,
          )
        ],
      ),
    );
  }
}
複製程式碼

image-20190905215240084

備註:所有內容首發於公眾號,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注

公眾號

相關文章