未完~
列表到哪裡都是一個麻煩的東西,因為現在開發出來的玩法太多啦,像:基本的多型別 item
、介面卡設計
、複雜item 檢視設計
、item 更新粒度層次優化
、列表快取設計原理和優化
、列表顯示優化
、列表巢狀
、滑動衝突
、巢狀滾動
、和其他 view 的聯動
等等方面,不管到哪個平臺都得搞懂這些才能玩的轉
Flutter 中的列表我個人覺得比 Android 設計的更好一些,首先名字迴歸 listview
,見名知已,簡單明瞭。其次 Flutter 使用 build 介面來代替 adapter,並且分隔線之類的也看做 item 使用 build來處理,這就比 android 好寫,好明白多了,也更靈活,對列表本身的破壞更小
4種構建方式
Flutter 的 listview
有4種構建方式,也就是有4個 構造方法
,核心思路就是把生成 item 的部分看成一個 物件函式(各種build)
傳進構造方法中。 Flutter 的 listview
真的喜歡 build 設計模式,不光 item,分割線,多型別都是包裝成 build
listview
- 這種構建方式是把固定資料變成 item 傳給 childen
Widget build(BuildContext context) {
// TODO: implement build
return ListView(
padding: EdgeInsets.all(20),
children: <Widget>[
Text("AA"),
Text("BB"),
Text("CC"),
],
);
}
複製程式碼
ListView.builder
- 構建一種 item 型別的列表,當然在 itemBuilder 使用 if、slse 也是能支援多型別 item 的,itemCount 是列表數量
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
padding: EdgeInsets.all(20),
itemCount: 50,
itemBuilder: (context, index) {
return Text("item:${index}");
},
);
}
複製程式碼
ListView.separated
- 帶分割線的列表, Flutter中把分割線也是看成一個 widget 來處理的,這裡新增了separatorBuilder
來返回分割線 widget,不過分割線的特點是:不包含最後一項
,也就是分割線不會出列表的範圍。這中設計我很喜歡,把分割線或是分隔符看成列表的 item,可以極大的方便我們設計列表,可玩性靈活性可以大大增加,至少我們可以根據 index 找到前後 item 的型別,然後考慮可以採取不同型別的分隔 item,最典型的應用就是插入廣告了,不用對列表有任何修改
Widget build(BuildContext context) {
// TODO: implement build
return ListView.separated(
padding: EdgeInsets.all(20),
itemCount: 50,
itemBuilder: (context, index) {
return Text("item:${index}");
},
separatorBuilder: (context,index){
return Container(
height: 1,
color: Colors.pink,
);
},
);
}
複製程式碼
ListView.custom()
- 需要傳入一個實現了 SliverChildDelegate 的元件,如 SliverChildListDelegate 和 SliverChildBuilderDelegate。這裡我不詳細說,因為沒必要,SliverChildListDelegate 就是 listview,SliverChildBuilderDelegate 就是 ListView.builder。這個 ListView.custom 用的也比較少
常用屬性
ScrollDirection
- 滾動方向,支援橫向滾動,預設是縱向滾動
scrollDirection: Axis.horizontal,
複製程式碼
reverse
- 決定滾動方向是否與閱讀方向一致,true
表示不一樣,顯示時是列表直接滾動到最底下,最後一個 item 顯示第一個資料scrollController
- 主要作用是控制滾動位置和監聽滾動事件,下面我會單開一節詳細的解釋下primary
- 當內容不足以滾動時,是否支援滾動;對於iOS系統還有一個效果:當使用者點選狀態列時是否滑動到頂部。這個挺重要的,尤其是使用列表構建頁面時配合下拉重新整理,要是資料量小於螢幕高度的話,這個就管用了。此時系統會自動給 listview 設定一個預設的滾動控制器 PrimaryScrollController,好處是父元件可以控制子樹中可滾動元件的滾動行為itemExtent
- 可以直接設定列表項高度,可以提高列表效能shrinkWrap
- 是否根據子元件的總長度來設定 ListView 的長度,預設值為 false,所以能滾動。滾動元件相互巢狀時,shrinkWrap 屬性要設定 true 才行,和 NeverScrollableScrollPhysics 配合就能解決滾動衝突addAutomaticKeepAlives
- 該屬性表示是否將列表項(子元件)包裹在AutomaticKeepAlive元件中,在一個懶載入列表中,如果將列表項包裹在AutomaticKeepAlive中,在該列表項滑出視口時也不會被回收,它會使用KeepAliveNotification來儲存其狀態。如果列表項自己維護其KeepAlive狀態,那麼此引數必須置為falseaddRepaintBoundaries
- 該屬性表示是否將列表項(子元件)包裹在RepaintBoundary元件中。當可滾動元件滾動時,將列表項包裹在RepaintBoundary中可以避免列表項重繪,但是當列表項重繪的開銷非常小(如一個顏色塊,或者一個較短的文字)時,不新增RepaintBoundary反而會更高效。和addAutomaticKeepAlive一樣,如果列表項自己維護其KeepAlive狀態,那麼此引數必須置為falsecacheExtent
- 列表在你快要滑到載入資料的時候,會提前一步載入好,等到你滑到的時候就會顯示出來,而不至於使用者滑到的時候還需要等待一會兒,cacheExtent 就是列表顯示的 item 數量,包活預載入的 item,我們可以根據列表長度和 item' 高度自己計算下,合理的配置
scrollController 滾動控制
大家一看這個名字裡帶 Controller
那就說明是對列表進行控制的功能的。scrollController
主要功能就是監聽滾動狀態,提供方法滾動到列表指定位置
1.
scrollController 建構函式
ScrollController({
double initialScrollOffset = 0.0, //初始滾動位置
this.keepScrollOffset = true,//是否儲存滾動位置
...
})
複製程式碼
scrollController 需要我們 new 一個物件出來,通過建構函式中的這2個引數,我們可以設定選擇列表從哪裡開始顯示,配合 itemExtent
每列固定高度設定是個不錯的思路
2.
核心引數、方法:
offset
- 當前滾動到的位置,注意這個資料是累計值,不是每次滾動的量jumpTo(double offset)
- 滾動到指定位置,不帶動畫animateTo(double offset,...)
- 滾動到指定位置,帶動畫,可以指定時間
3.
使用
- 先 new 一個 scrollController 物件出來
- 在 initState 中給 scrollController 新增監聽
- 在 佈局中把 scrollController 設定給 listview
class TestWidgetState extends State<TestWidget> {
var scrollController = ScrollController();
@override
void initState() {
super.initState();
scrollController.addListener(() {
print("當前滾動位置:${scrollController.offset}");
});
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.builder(
padding: EdgeInsets.all(20),
physics: BouncingScrollPhysics(),
itemCount: 50,
controller: scrollController,
itemBuilder: (context, index) {
return Container(
width: 50,
height: 30,
alignment: Alignment.center,
child: Text("item:${index}"),
);
},
);
}
void dispose() {
//為了避免記憶體洩露,需要呼叫 dispose
scrollController.dispose();
super.dispose();
}
}
複製程式碼
4.
特點
- scrollController 可以新增多個 Listener 呢的,從其方法 scrollController.addListener 就能看的出來,走進原始碼中 _listeners 是一個集合型別
ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>();
複製程式碼
- scrollController.offset 滾動的數值是從 0 開始的,可見和 Android 一樣,滾動數值,可滾動的最大數值,螢幕可見高度 這3者是相互分開的
I/flutter ( 7435): 當前滾動位置:1.1666666666666667
I/flutter ( 7435): 當前滾動位置:2.621212121212163
I/flutter ( 7435): 當前滾動位置:3.7121212121212848
I/flutter ( 7435): 當前滾動位置:4.803030303030293
I/flutter ( 7435): 當前滾動位置:5.530303030303041
I/flutter ( 7435): 當前滾動位置:6.621212121212163
I/flutter ( 7435): 當前滾動位置:7.348484848484911
I/flutter ( 7435): 當前滾動位置:8.07575757575766
複製程式碼
5.
jumpTo、animateTo
這2個方法都是指定列表滾動帶指定位置,目前只能滾動到指定 px,正在研究沒有沒方法指定到指定 index 的 item
jumpTo
- 這個方法好說,只有一個 offset 的引數就完了
scrollController.jumpTo(500);
複製程式碼
animateTo
- 這可就不一樣了,是帶動畫的,可以指定時間,插值器,不過這2者要設定必須一起設定,單個不行
scrollController.animateTo(500,duration: Duration(milliseconds: 300), curve: Curves.ease);
複製程式碼
下面是一個 GIF 演示:
6.
ScrollPosition
ScrollController 可是能設定給多個可滾動元件的,其原理就是 ScrollController 會給每一個設定到其中的可滾動元件生成一個 ScrollPosition,ScrollController positions 屬性儲存這些資料
自然所有關於滾動的資料都儲存在 ScrollPosition 裡面,比如滾動數值:
double get offset => position.pixels;
複製程式碼
但是 offset 這個數值返回的處於顯示的或是最上層位置的滾動元件的滾動資料,ScrollController 若是繫結了多個可滾動元件的話這個 offset 就不準了,並且 jumpTo、animateTo 方法會對 ScrollController 中所有已繫結的可滾動元件進行形同數值的滾動,這點要注意啦
當然我們也不是一點辦法都沒有:
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
複製程式碼
這樣可以拿到不通滾動元件的滾動值,但是我們得明確的知道可滾動元件當初的插入順序
NotificationListener 滾動監聽
上文我們用 scrollController
監聽列表滾動,這裡我們還有另一種思路,傳承與 Android
的巢狀滾動思路
Android 中有 NestedScrollingParent、NestedScrollingChild
這一對介面,NestedScrollingChild 傳送滾動事件,NestedScrollingParent 控制滾動事件的傳播和數值
Flutter 集繼承這一思路。在 widget 樹中,可滾動 widget 滾動時會逐次向上傳遞滾動事件 notification,我們可以通過 NotificationListener 可以監控到該 notification,NotificationListener 也是一個 widget,只要 NotificationListener 的佈局層級比 listview 高就行,隔幾層都沒關係,一樣可以監聽的到
- android 中的經典應用的是:
Behavior CoordinatorLayout RecycleView
- flutter 中則是:
NotificationListener listview
1.
監聽方法
NotificationListener 中 onNotification(ScrollNotification notification)
方法可以拿到滾動事件,數值包裹在 ScrollNotification 這個引數總。需要我們返回一個 boolean 值:
true
- 那麼 notifcation 到此為止,我們攔截了滾動事件,不會繼續上傳滾動事件,但是不影響 listview 自身 widget 的滾動false
- 那 麼notification 會繼續向更外層 widget 傳遞
2.
監聽到的資料
滾動事件的資料是 ScrollNotification
型別的,具體引數都在其 metrics
引數中:
metrics.pixels
- 當前位置,以 0 開始,預設是0metrics.atEdge
- 是否在頂部或底部metrics.axis
- 垂直或水平滾動metrics.axisDirection
- 滾動方向是 down 還是 up,測試了下,都是 downmetrics.extentAfter
- widget 底部距離列表底部有多大metrics.extentBefore
- widget 頂部距離列表頂部有多大metrics.extentInside
- widget 範圍內的列表長度metrics.maxScrollExtent
- 最大滾動距離,列表長度 - widget 長度metrics.minScrollExtent
- 最小滾動距離metrics.viewportDimension
- widget 長度metrics.outOfRange
- 是否越過邊界
3.
經測試
文件的解釋不一定準,還得我們自己試下才行的
axisDirection
- 一直都是 down 的,我們還是根據數值的變化自己判斷更準確pixels
- 上拉載入更多數值越來越大
,下拉重新整理數值越來越小,一直到0
atEdge
- 的確可以判斷是否到頂或是到底,到頂或是底時,的確會變成 true 的滾動數值這塊
- extentAfter + extentBefore 的確= maxScrollExtent,說明數值這塊大家不用擔心widget 長度
- viewportDimension 和 extentInside 數值是一樣的outOfRange
- 預設到底或是到頂是 false 的,只有我們配合 physics 屬性繼續滾動時才會變成 true,大家注意臨界值
4.
示例:
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
print("pixels:${notification.metrics.pixels}");
print("atEdge:${notification.metrics.atEdge}");
print("axis:${notification.metrics.axis}");
print("axisDirection:${notification.metrics.axisDirection}");
print("extentAfter:${notification.metrics.extentAfter}");
print("extentBefore:${notification.metrics.extentBefore}");
print("extentInside:${notification.metrics.extentInside}");
print("maxScrollExtent:${notification.metrics.maxScrollExtent}");
print("minScrollExtent:${notification.metrics.minScrollExtent}");
print("viewportDimension:${notification.metrics.viewportDimension}");
print("outOfRange:${notification.metrics.outOfRange}");
print("____________________________________________");
return true;
},
child: Container(
child: Stack(
children: <Widget>[
ListView.builder(
padding: EdgeInsets.all(20),
physics: BouncingScrollPhysics(),
itemCount: 50,
itemExtent: 35,
controller: scrollController,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
child: Text("item:${index}"),
);
},
),
Positioned(
left: 20,
top: 20,
child: RaisedButton(
child: Text("點選滾動"),
onPressed: () {
scrollController.animateTo(500,
duration: Duration(milliseconds: 300),
curve: Curves.ease);
print("AAA");
},
),
),
],
),
),
);
}
複製程式碼
physics
physics
這就是 Flutter 上的 Behavior,依託與上面講的巢狀滾動,他可以讓我們在列表劃到頂或是底時對整個列表進行額外的操作,最典型的就是下拉到頂然後回彈了
程式碼:
physics: BouncingScrollPhysics(),
複製程式碼
系統提供幾個預設實現,目前不清楚是或否可以自定義:
NeverScrollablePhysics
- 列表不可以滾動BouncingScrollPhysics
- 回彈效果,就是上面的那個 gif 的效果ClampingScrollPhysics
- 系統預設的,到頭了顯示水波紋FixedExtentScrollPhysics
- 這個必須配合響應的 widget 才行,listview 是不能用,具體的以後再寫
滾動資料快取
listview 繼承自:Scrollable
,本身是一個 StatefulWidget,自然也能儲存資料,滾動偏移量自然也能儲存,所以主要 listview 不從 widget 樹上移出,那麼滾動狀態一直都在
但是有的時候因 widget 樹的變化 listview 會被移除,比如:TabBarView 在切換 Tab 時可滾動元件的 State 會被銷燬
,此時我們要是想要在 tab 來回切換時能顯示上次滾動到的位置,那麼必須能快取滾動偏移量才行
這裡我們藉助 PageStorage
,他是一個用於儲存頁面(路由)相關資料的元件,PageStorage
的宣告週期是整個 app,不管有多少頁面都可以儲存資料不丟失,核心就是通過設定 PageStorageKey 到 wigdet 建構函式的 可以就行
我們給每個 tab 設定自己字串的 PageStorageKey 就成了
new TabBarView(
children: myTabs.map((Tab tab) {
new MyScrollableTabView(
key: new PageStorageKey<String>(tab.text), // like 'Tab 1'
tab: tab,
),
}),
)
複製程式碼
目前這塊就這麼多
固定式頭部 widget
列表中我們總是有固定頭部
這個需求,頭部 view 不順著列表滾動。這裡說一個最簡便的實現方法:column + expanded
。Column 繼承自 Flex,可以自動實現 widget 長度適應,Expanded 可以自動拉伸元件的大小,所以用它倆來做很適合
Widget build(BuildContext context) {
// TODO: implement build
return Column(
children: <Widget>[
Text("我是頭部"),
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(20),
physics: BouncingScrollPhysics(),
cacheExtent: 10,
itemCount: 50,
itemExtent: 35,
controller: scrollController,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
child: Text("item:${index}"),
);
},
),
),
],
);
}
}
複製程式碼
常用設計思路
- 用
ListView.separated
插入廣告