Flutter
中, 當內容超過顯示視圖時,若是沒有特殊處理,Flutter
則會提示Overflow
錯誤Flutter
提供了多種可滾動(Scrollable Widget
)用於顯示列表和長佈局Widget
都直接或間接包含一個Scrollable
, 下面是經常使用的幾個可滾動的Widget
SingleChildScrollView
ListView
GridView
CustomScrollView
ScrollController
Scrollbar
是一個Material
風格的滾動指示器(滾動條),若是要給可滾動widget
添加滾動條,只需將Scrollbar
做爲可滾動widget
的父widget
便可CupertinoScrollbar
是iOS
風格的滾動條,若是你使用的是Scrollbar
,那麼在iOS
平臺它會自動切換爲CupertinoScrollbar
Scrollbar
和CupertinoScrollbar
都是經過ScrollController
來監聽滾動事件來肯定滾動條位置,關於ScrollController
詳細的內容咱們將在後面專門一節介紹Scrollbar
和CupertinoScrollbar
的構造函數, 都只有一個child
屬性, 用於接受一個可滾動的Widget
const Scrollbar({ Key key, @required this.child, }) const CupertinoScrollbar({ Key key, @required this.child, }) 複製代碼
主軸和縱軸git
widget
的座標描述中,一般將滾動方向稱爲主軸,非滾動方向稱爲縱軸。widget
的默認方向通常都是沿垂直方向,因此默認狀況下主軸就是指垂直方向,水平方向同理SingleChildScrollView
相似於開發中經常使用的ScrollView
, 再也不詳細介紹了, 下面看一下具體使用介紹吧github
const SingleChildScrollView({ Key key, // 設置滾動的方向, 默認垂直方向 this.scrollDirection = Axis.vertical, // 設置顯示方式 this.reverse = false, // 內邊距 this.padding, // 是否使用默認的controller bool primary, // 設置可滾動Widget如何響應用戶操做 this.physics, this.controller, this.child, }) 複製代碼
設置視圖的滾動方向(默認垂直方向), 須要對應的設置其子Widget
是Column
或者Row
, 不然會報Overflow
錯誤算法
scrollDirection: Axis.vertical, // 枚舉值 enum Axis { /// 水平滾動 horizontal, /// 垂直滾動 vertical, } 複製代碼
reverse: false
,則滾動內容頭部和左側對其, 那麼滑動方向就是從左向右reverse: true
時,則滾動內容尾部和右側對其, 那麼滑動方向就是從右往左。widget
的初始滾動位置是在頭仍是尾,取false
時,初始滾動位置在頭,反之則在尾ScrollPhysics
對象,它決定可滾動Widget
如何響應用戶操做Flutter
會根據具體平臺分別使用不一樣的ScrollPhysics
對象,應用不一樣的顯示效果,如當滑動到邊界時,繼續拖動的話,在iOS
上會出現彈性效果,而在Android
上會出現微光效果。Flutter SDK
中包含了兩個ScrollPhysics
的子類能夠直接使用:
ClampingScrollPhysics
:安卓下微光效果。BouncingScrollPhysics
:iOS
下彈性效果。ScrollController
對象ScrollController
的主要做用是控制滾動位置和監聽滾動事件。widget
中會有一個默認的PrimaryScrollController
,若是子widget
中的可滾動widget
沒有顯式的指定controller
而且primary
屬性值爲true
時(默認就爲true
),可滾動widget
會使用這個默認的PrimaryScrollController
widget
能夠控制子樹中可滾動widget
的滾動,例如,Scaffold
使用這種機制在iOS
中實現了"回到頂部"的手勢class ScrollView extends StatelessWidget { @override Widget build(BuildContext context) { String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; return Scrollbar( child: SingleChildScrollView( scrollDirection: Axis.vertical, reverse: true, padding: EdgeInsets.all(0.0), physics: BouncingScrollPhysics(), child: Center( child: Column( //動態建立一個List<Widget> children: str.split("") //每個字母都用一個Text顯示,字體爲原來的兩倍 .map((c) => Text(c, textScaleFactor: 2.0)) .toList(), ), ), ), ); } } 複製代碼
ListView
是最經常使用的可滾動widget
,它能夠沿一個方向線性排布全部子widget
, 相似於ReactNative
中的ListView
ListView
共有四種構造函數
ListView()
默認構造函數ListView.builder()
ListView.separated()
ListView custom()
ListView({ // 公共參數上面都介紹過了 Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, EdgeInsetsGeometry padding, // 是否根據子widget的總長度來設置ListView的長度,默認值爲false bool shrinkWrap = false, // cell高度 this.itemExtent, // 子widget是否包裹在AutomaticKeepAlive中 bool addAutomaticKeepAlives = true, // 子widget是否包裹在RepaintBoundary中 bool addRepaintBoundaries = true, bool addSemanticIndexes = true, // 設置預加載的區域, moren 0.0 double cacheExtent, //子widget列表 List<Widget> children = const <Widget>[], // 子widget的個數 int semanticChildCount, }) 複製代碼
widget
的總長度來設置ListView
的長度,默認值爲false
。ListView
的會在滾動方向儘量多的佔用空間ListView
在一個無邊界(滾動方向上)的容器中時,shrinkWrap
必須爲true
null
,則會強制children
的"長度"爲itemExtent
的值widget
的長度,即若是滾動方向是垂直方向,則表明子widget
的高度,若是滾動方向爲水平方向,則表明子widget
的長度ListView
中,指定itemExtent
比讓子widget
本身決定自身長度會更高效,這是由於指定itemExtent
後,滾動系統能夠提早知道列表的長度,而不是老是動態去計算,尤爲是在滾動位置頻繁變化時AutomaticKeepAlive
中AutomaticKeepAlive
中,在該列表項滑出視口時該列表項不會被GC,它會使用KeepAliveNotification
來保存其狀態KeepAlive
狀態,那麼此參數必須置爲false
RepaintBoundary
中widget
滾動時,將列表項包裹在RepaintBoundary
中能夠避免列表項重繪,可是當列表項重繪的開銷很是小(如一個顏色塊,或者一個較短的文本)時,不添加RepaintBoundary
反而會更高效addAutomaticKeepAlive
同樣,若是列表項本身維護其KeepAlive
狀態,那麼此參數必須置爲false
class ScrollView extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( itemExtent: 60, cacheExtent: 100, addAutomaticKeepAlives: false, children: renderCell(), ); } List<Widget> renderCell() { String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; return str.split("") .map((item) => ListTile( title: Text('字母--$item'), subtitle: Text('這是字母列表'), leading: Icon(Icons.wifi), )).toList(); } } 複製代碼
ListTile
是Flutter
給咱們準備好的用於建立ListView
的子widget
const ListTile({ Key key, // 前置(左側)圖標, Widget類型 this.leading, // 標題, Widget類型 this.title, // 副標題, Widget類型 this.subtitle, // 後置(右側)圖標, Widget類型 this.trailing, // 是否三行顯示, subtitle不爲空時才能使用 this.isThreeLine = false, // 設置爲true後字體變小 this.dense, // 內容的內邊距 this.contentPadding, // 是否可被點擊 this.enabled = true, // 點擊事件 this.onTap, // 長按操做事件 this.onLongPress, // 是不是選中狀態 this.selected = false, }) // 使用示例 return ListTile( title: Text('index--$index'), subtitle: Text('我是一隻小鴨子, 咿呀咿呀喲; 我是一隻小鴨子, 咿呀咿呀喲; 我是一隻小鴨子, 咿呀咿呀喲;'), leading: Icon(Icons.wifi), trailing: Icon(Icons.keyboard_arrow_right), isThreeLine: true, dense: false, contentPadding: EdgeInsets.all(10), enabled: index % 3 != 0, onTap: () => print('index = $index'), onLongPress: () => print('long-Index = $index'), selected: index % 2 == 0, ); 複製代碼
ListView.builder
適合列表項比較多(或者無限)的狀況,由於只有當子Widget
真正顯示的時候纔會被建立Widget
且全部子Widget
的樣式同樣ListView.builder({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, this.itemExtent, // @required IndexedWidgetBuilder itemBuilder, // 列表項的數量,若是爲null,則爲無限列表 int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double cacheExtent, int semanticChildCount, }) 複製代碼
列表項的數量,若是爲null,則爲無限列表數組
IndexedWidgetBuilder
,返回值爲一個widget
index
位置時,會調用該構建器構建列表項class ListBuild extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( itemCount: 30, itemBuilder: (content, index) { return ListTile( title: Text('index--$index'), subtitle: Text('數字列表'), leading: Icon(Icons.wifi), ); }, ); } } 複製代碼
ListView.separated
能夠生成列表項之間的分割器,它除了比ListView.builder
多了一個separatorBuilder
參數外, 其餘參數都同樣微信
ListView.separated({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required IndexedWidgetBuilder itemBuilder, // 一個分割生成器 @required IndexedWidgetBuilder separatorBuilder, @required int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double cacheExtent, }) 複製代碼
該參數是一個分割生成器, 一樣是一個IndexedWidgetBuilder
類型的參數markdown
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index); 複製代碼
奇數行添加一條紅色下劃線,偶數行添加一條藍色下劃線。app
lass SeparatedList extends StatelessWidget { //下劃線widget預約義以供複用。 Widget lineView1 = Divider(color: Colors.red, height: 2, indent: 10,); Widget lineView2 = Divider(color: Colors.blue, height: 5, indent: 30); @override Widget build(BuildContext context) { // TODO: implement build return ListView.separated( itemCount: 30, itemBuilder: (content, index) { return ListTile( title: Text('index--$index'), subtitle: Text('數字列表'), leading: Icon(Icons.wifi), ); }, separatorBuilder: (context, index) { return index % 2 == 0 ? lineView1 : lineView2; }, ); } } 複製代碼
設置每個子WIdget
的分割線less
const Divider({ Key key, // 分割線所在的SizedBox的高度, 除內邊距以外的距離上面的間距 this.height = 16.0, // 分割線左側間距 this.indent = 0.0, // 分割線顏色 this.color }) 複製代碼
custom
方式的「快捷方式」ListView
內部是靠這個childrenDelegate
屬性動態初始化子元素的builder
和separated
比較多,這個custom
相對來講就比較少了const ListView.custom({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, this.itemExtent, // 動態初始化子元素 @required this.childrenDelegate, double cacheExtent, int semanticChildCount, }) 複製代碼
其實在ListView
的前面幾種構造函數中, 都默認設置了childrenDelegate
這個屬性, 更多可參考官方文檔ide
// ListView ListView({ // ... }) : childrenDelegate = SliverChildListDelegate( children, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, addSemanticIndexes: addSemanticIndexes, ), super(); // ListView.builder ListView.builder({ // ... }) : childrenDelegate = SliverChildBuilderDelegate( itemBuilder, childCount: itemCount, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, addSemanticIndexes: addSemanticIndexes, ), super(); // ListView.separated ListView.separated({ // ... }) : childrenDelegate = SliverChildBuilderDelegate( // ... ), super(); 複製代碼
SliverChildListDelegate
的實例SliverChildListDelegate
是抽象類SliverChildDelegate
的子類SliverChildListDelegate
中主要邏輯就是實現了SliverChildDelegate
中定義的build
方法Widget build(BuildContext context, int index) { assert(builder != null); if (index < 0 || (childCount != null && index >= childCount)) return null; Widget child; try { child = builder(context, index); } catch (exception, stackTrace) { child = _createErrorWidget(exception, stackTrace); } if (child == null) return null; if (addRepaintBoundaries) child = RepaintBoundary.wrap(child, index); if (addSemanticIndexes) { final int semanticIndex = semanticIndexCallback(child, index); if (semanticIndex != null) child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child); } if (addAutomaticKeepAlives) child = AutomaticKeepAlive(child: child); return child; } 複製代碼
children
列表中對應的元素ListView
的底層實現須要加載一個元素時,就會把該元素的索引傳遞給SliverChildDelegate
的build
方法,由該方法返回具體的元素SliverChildDelegate
內部,除了定義了build
方法外,還定義了 一個名爲didFinishLayout
的方法void didFinishLayout() { assert(debugAssertChildListLocked()); final int firstIndex = _childElements.firstKey() ?? 0; final int lastIndex = _childElements.lastKey() ?? 0; widget.delegate.didFinishLayout(firstIndex, lastIndex); } 複製代碼
ListView
完成一次layout
以後都會調用該方法, 同時傳入兩個索引值layout
中第一個元素和最後一個元素在ListView
全部子元素中的索引值, 也就是可視區域內的元素在子元素列表中的位置SliverChildListDelegate
仍是SliverChildBuilderDelegate
的代碼中,都沒有didFinishLayout
的具體實現。因此咱們須要編寫一個它們的子類class MySliverBuilderDelegate extends SliverChildBuilderDelegate { MySliverBuilderDelegate( Widget Function(BuildContext, int) builder, { int childCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, }) : super(builder, childCount: childCount, addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries); @override void didFinishLayout(int firstIndex, int lastIndex) { print('firstIndex: $firstIndex, lastIndex: $lastIndex'); } } 複製代碼
而後咱們建立一個ListView.custom
的列表視圖函數
class CustomList extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return ListView.custom( childrenDelegate: MySliverBuilderDelegate( (BuildContext context, int index) { return ListTile( title: Text('index--$index'), subtitle: Text('數字列表'), leading: Icon(Icons.wifi), ); }, childCount: 30, ), ); } } 複製代碼
GridView
能夠構建二維網格列表, 系統給出了五中構造函數
GridView()
GridView.count
GridView.extent
GridView.builder
GridView.custom
// 默認構造函數 GridView({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required this.gridDelegate, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double cacheExtent, List<Widget> children = const <Widget>[], int semanticChildCount, }) 複製代碼
gridDelegate
屬性外, 其餘屬性和ListView
的屬性都同樣, 含義也都相同gridDelegate
參數的類型是SliverGridDelegate
,它的做用是控制GridView
子widget
如何排列SliverGridDelegate
是一個抽象類,定義了GridView
排列相關接口,子類須要經過實現它們來實現具體的佈局算法Flutter
中提供了兩個SliverGridDelegate
的子類SliverGridDelegateWithFixedCrossAxisCount
和SliverGridDelegateWithMaxCrossAxisExtent
, 下面咱們分別介紹該子類實現了一個橫軸爲固定數量子元素的排列算法,其構造函數爲:
const SliverGridDelegateWithFixedCrossAxisCount({ // 橫軸子元素的數量,此屬性值肯定後子元素在橫軸的長度就肯定了,即ViewPort橫軸長度/crossAxisCount。 @required this.crossAxisCount, // 主軸方向的間距 this.mainAxisSpacing = 0.0, // 側軸方向子元素的間距 this.crossAxisSpacing = 0.0, // 子元素在側軸長度和主軸長度的比例, 因爲crossAxisCount指定後子元素橫軸長度就肯定了,而後經過此參數值就能夠肯定子元素在主軸的長度 this.childAspectRatio = 1.0, }) 複製代碼
從上面的個屬性能夠發現,子元素的大小是經過crossAxisCount
和childAspectRatio
兩個參數共同決定的。注意,這裏的子元素指的是子widget
的最大顯示空間,注意確保子widget
的實際大小不要超出子元素的空間, 代碼示例以下
class ScrollView extends StatelessWidget { @override Widget build(BuildContext context) { return GridView( padding: EdgeInsets.all(10), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 1, mainAxisSpacing: 10, crossAxisSpacing: 10 ), children: <Widget>[ Container(color: Colors.orange), Container(color: Colors.blue), Container(color: Colors.orange), Container(color: Colors.yellow), Container(color: Colors.pink) ], ); } } 複製代碼
GridView.count
構造函數內部使用了SliverGridDelegateWithFixedCrossAxisCount
,咱們經過它能夠快速的建立橫軸固定數量子元素的GridView
GridView.count({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required int crossAxisCount, double mainAxisSpacing = 0.0, double crossAxisSpacing = 0.0, double childAspectRatio = 1.0, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double cacheExtent, List<Widget> children = const <Widget>[], int semanticChildCount, }) 複製代碼
上面SliverGridDelegateWithFixedCrossAxisCount
中給出的示例代碼等價於:
class CountGridView extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return GridView.count( padding: EdgeInsets.all(10), crossAxisCount: 3, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1, children: <Widget>[ Container(color: Colors.orange), Container(color: Colors.blue), Container(color: Colors.orange), Container(color: Colors.yellow), Container(color: Colors.pink) ], ); } } 複製代碼
該子類實現了一個側軸子元素爲固定最大長度的排列算法,其構造函數爲:
const SliverGridDelegateWithMaxCrossAxisExtent({ @required this.maxCrossAxisExtent, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, this.childAspectRatio = 1.0, }) 複製代碼
maxCrossAxisExtent
爲子元素在側軸上的最大長度,之因此是「最大」長度,是由於橫軸方向每一個子元素的長度仍然是等分的Widget
的個數, 也是由該屬性決定SliverGridDelegateWithFixedCrossAxisCount
相同class ExtentScrollView extends StatelessWidget { @override Widget build(BuildContext context) { return GridView( padding: EdgeInsets.all(10), gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 100, childAspectRatio: 1, mainAxisSpacing: 10, crossAxisSpacing: 10 ), children: <Widget>[ Container(color: Colors.orange), Container(color: Colors.blue), Container(color: Colors.orange), Container(color: Colors.yellow), Container(color: Colors.pink) ], ); } } 複製代碼
一樣GridView.extent
構造函數內部使用了SliverGridDelegateWithMaxCrossAxisExtent
,咱們經過它能夠快速的建立側軸子元素爲固定最大長度的的GridView
GridView.extent({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required double maxCrossAxisExtent, double mainAxisSpacing = 0.0, double crossAxisSpacing = 0.0, double childAspectRatio = 1.0, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, List<Widget> children = const <Widget>[], int semanticChildCount, }) 複製代碼
上面SliverGridDelegateWithMaxCrossAxisExtent
中給出的示例代碼等價於:
class ExtentScrollView extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.extent( padding: EdgeInsets.all(10), maxCrossAxisExtent: 100, childAspectRatio: 1, mainAxisSpacing: 10, crossAxisSpacing: 10, children: <Widget>[ Container(color: Colors.orange), Container(color: Colors.blue), Container(color: Colors.orange), Container(color: Colors.yellow), Container(color: Colors.pink) ], ); } } 複製代碼
GridView
都須要一個Widget
數組做爲其子元素,這些方式都會提早將全部子widget
都構建好,因此只適用於子Widget
數量比較少時widget
比較多時,咱們能夠經過GridView.builder
來動態建立子Widget
GridView.builder({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required this.gridDelegate, @required IndexedWidgetBuilder itemBuilder, int itemCount, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, double cacheExtent, int semanticChildCount, }) 複製代碼
GridView.builder
必須指定的參數有兩個,其中gridDelegate
以前已經介紹過了itemBuilder
在以前ListView
中也有介紹過相似的, 用於構建子Widget
class BuilderGridView extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.builder( itemCount: 50, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 10, crossAxisSpacing: 10 ), itemBuilder: (content, index) { return Container( color: Colors.orange, child: Center( child: Text('$index'), ), ); }, ); } } 複製代碼
和ListView.custom
同樣, 用於構建自定義子Widget
, 有兩個必須指定的參數, 這裏就不在贅述了
const GridView.custom({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required this.gridDelegate, @required this.childrenDelegate, double cacheExtent, int semanticChildCount, }) 複製代碼
CustomScrollView
使用sliver
來自定義滾動模型(效果, 它能夠包含多種滾動模型GridView
,底部須要一個ListView
,而要求整個頁面的滑動效果是統一的,即它們看起來是一個總體GridView+ListView
來實現的話,就不能保證一致的滑動效果,由於它們的滾動效果是分離的,因此這時就須要一個"膠水",把這些彼此獨立的可滾動widget
"粘"起來,而CustomScrollView
的功能就至關於「膠水」const CustomScrollView({ Key key, // 滑動方向 Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, double cacheExtent, this.slivers = const <Widget>[], int semanticChildCount, }) 複製代碼
上述屬性除了slivers
以外, 前面都有提到過, 接受一個Widget
數組, 可是這裏的Widget
必須是Sliver
類型的, 至於緣由, 下面會詳解
什麼是`Sliver` ??
Flutter
中,Sliver
一般指具備特定滾動效果的可滾動塊widget
,如ListView
、GridView
等都有對應的Sliver
實現如SliverList
、SliverGrid
等Sliver
來講,它們和可滾動Widget
最主要的區別是Sliver
不會包含Scrollable
,也就是說Sliver
自己不包含滾動交互模型CustomScrollView
才能夠將多個Sliver
"粘"在一塊兒,這些Sliver
共用CustomScrollView
的Scrollable
,最終實現統一的滑動效果Sliver
都和可滾動Widget
對應,是因爲還有一些如SliverPadding
、SliverAppBar
等是和可滾動Widget
無關的CustomScrollView
一塊兒使用,這是由於CustomScrollView
的子widget
必須都是Sliver
AppBar
和SliverAppBar
是Material Design
中的導航欄AppBar
和SliverAppBar
都是繼承StatefulWidget
類,兩者的區別在於AppBar
位置的固定的應用最上面的;而SliverAppBar
是能夠跟隨內容滾動的AppBar
都同樣const SliverAppBar({ Key key, // 導航欄左側weidget this.leading, // 若是leading爲null,是否自動實現默認的leading按鈕 this.automaticallyImplyLeading = true, // 導航欄標題 this.title, // 導航欄右側按鈕, 接受一個數組 this.actions, // 一個顯示在AppBar下方的控件,高度和AppBar高度同樣,能夠實現一些特殊的效果,該屬性一般在SliverAppBar中使用 this.flexibleSpace, // 一個AppBarBottomWidget對象, 設置TabBar this.bottom, //中控件的z座標順序,默認值爲4,對於可滾動的SliverAppBar,當 SliverAppBar和內容同級的時候,該值爲0,當內容滾動 SliverAppBar 變爲 Toolbar 的時候,修改elevation的值 this.elevation = 4.0, // 背景顏色,默認值爲 ThemeData.primaryColor。改值一般和下面的三個屬性一塊兒使用 this.backgroundColor, // 狀態欄的顏色, 黑白兩種, 取值: Brightness.dark this.brightness, // 設置導航欄上圖標的顏色、透明度、和尺寸信息 this.iconTheme, // 設置導航欄上文字樣式 this.textTheme, // 導航欄的內容是否顯示在頂部, 狀態欄的下面 this.primary = true, // 標題是否居中顯示,默認值根據不一樣的操做系統,顯示方式不同 this.centerTitle, // 標題間距,若是但願title佔用全部可用空間,請將此值設置爲0.0 this.titleSpacing = NavigationToolbar.kMiddleSpacing, // 展開的最大高度 this.expandedHeight, // 是否隨着華東隱藏標題 this.floating = false, // 是否固定在頂部 this.pinned = false, // 只跟floating相對應,若是爲true,floating必須爲true,也就是向下滑動一點兒,整個大背景就會動畫顯示所有,網上滑動整個導航欄的內容就會消失 this.snap = false, }) 複製代碼
class CustomScrollViewTestRoute extends StatelessWidget { @override Widget build(BuildContext context) { //由於本路由沒有使用Scaffold,爲了讓子級Widget(如Text)使用 //Material Design 默認的樣式風格,咱們使用Material做爲本路由的根。 return Material( child: CustomScrollView( slivers: <Widget>[ //AppBar,包含一個導航欄 SliverAppBar( pinned: true, expandedHeight: 250.0, flexibleSpace: FlexibleSpaceBar( title: const Text('Demo'), background: Image.asset( "./images/avatar.png", fit: BoxFit.cover,), ), ), SliverPadding( padding: const EdgeInsets.all(8.0), sliver: new SliverGrid( //Grid gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, //Grid按兩列顯示 mainAxisSpacing: 10.0, crossAxisSpacing: 10.0, childAspectRatio: 4.0, ), delegate: new SliverChildBuilderDelegate( (BuildContext context, int index) { //建立子widget return new Container( alignment: Alignment.center, color: Colors.cyan[100 * (index % 9)], child: new Text('grid item $index'), ); }, childCount: 20, ), ), ), //List new SliverFixedExtentList( itemExtent: 50.0, delegate: new SliverChildBuilderDelegate( (BuildContext context, int index) { //建立列表項 return new Container( alignment: Alignment.center, color: Colors.lightBlue[100 * (index % 9)], child: new Text('list item $index'), ); }, childCount: 50 //50個列表項 ), ), ], ), ); } } 複製代碼
ScrollController
用於控制可滾動widget
的滾動位置,這裏以ListView
爲例,展現一下ScrollController
的具體用法ScrollController
的構造函數ScrollController({ // 初始滾動位置 double initialScrollOffset = 0.0, // 是否保存滾動位置 this.keepScrollOffset = true, // 調試使用的輸出標籤 this.debugLabel, }) 複製代碼
可滾動Widget
當前滾動的位置
跳轉到指定的位置, 沒有動畫效果
void jumpTo(double value) { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); for (ScrollPosition position in List<ScrollPosition>.from(_positions)) position.jumpTo(value); } 複製代碼
跳轉到指定的位置, 跳轉時會有一個動畫效果
Future<void> animateTo(double offset, { @required Duration duration, @required Curve curve, }) { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); final List<Future<void>> animations = List<Future<void>>(_positions.length); for (int i = 0; i < _positions.length; i += 1) animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve); return Future.wait<void>(animations).then<void>((List<void> _) => null); } 複製代碼
ScrollController
能夠同時被多個Scrollable
使用,ScrollController
會爲每個Scrollable
建立一個ScrollPosition
對象,這些ScrollPosition
保存在ScrollController
的positions
屬性中(是一個數組)ScrollPosition
是真正保存滑動位置信息的對象,offset
只是一個便捷屬性, 其餘更多屬性可查看相關官方文檔ScrollController
雖然能夠對應多個Scrollable
,可是有一些操做,如讀取滾動位置offset
,則須要一對一,可是咱們仍然能夠在一對多的狀況下,經過其它方法讀取滾動位置// controller的offset屬性 double get offset => position.pixels; // 讀取相關的滾動位置 controller.positions.elementAt(0).pixels controller.positions.elementAt(1).pixels 複製代碼
ScrollController
間接繼承自Listenable
,咱們能夠根據ScrollController
來監聽滾動事件。如:
controller.addListener(()=>print(controller.offset)) 複製代碼
先看一下ScrollController
另外幾個方法的實現
// 建立一個存儲位置信息的ScrollPosition ScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition, ) { return ScrollPositionWithSingleContext( physics: physics, context: context, initialPixels: initialScrollOffset, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); } // 註冊位置信息 void attach(ScrollPosition position) { assert(!_positions.contains(position)); _positions.add(position); position.addListener(notifyListeners); } // 註銷位置信息 void detach(ScrollPosition position) { assert(_positions.contains(position)); position.removeListener(notifyListeners); _positions.remove(position); } // 銷燬ScrollController @override void dispose() { for (ScrollPosition position in _positions) position.removeListener(notifyListeners); super.dispose(); } 複製代碼
ScrollController
和Scrollable
關聯時,Scrollable
首先會調用ScrollController
的createScrollPosition()
方法來建立一個ScrollPosition
來存儲滾動位置信息Scrollable
會調用attach()
方法,將建立的ScrollPosition
添加到ScrollController
的positions
屬性中,這一步稱爲「註冊位置」,只有註冊後animateTo()
和jumpTo()
才能夠被調用Scrollable
銷燬時,會調用ScrollController
的detach()
方法,將其ScrollPosition
對象從ScrollController
的positions
屬性中移除,這一步稱爲「註銷位置」,註銷後animateTo()
和jumpTo()
將不能再被調用ScrollController
的animateTo()
和jumpTo()
內部會調用全部ScrollPosition
的animateTo()
和jumpTo()
,以實現全部和該ScrollController
關聯的Scrollable
都滾動到指定的位置建立一個ListView
,當滾動位置發生變化時,咱們先打印出當前滾動位置,而後判斷當前位置是否超過1000像素,若是超過則在屏幕右下角顯示一個「返回頂部」的按鈕,該按鈕點擊後可使ListView
恢復到初始位置;若是沒有超過1000像素,則隱藏「返回頂部」按鈕。代碼以下
class ScrollControllerTestRoute extends StatefulWidget { @override ScrollControllerTestRouteState createState() { return new ScrollControllerTestRouteState(); } } class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> { ScrollController _controller = new ScrollController(); bool showToTopBtn = false; //是否顯示「返回到頂部」按鈕 @override void initState() { //監聽滾動事件,打印滾動位置 _controller.addListener(() { print(_controller.offset); //打印滾動位置 if (_controller.offset < 1000 && showToTopBtn) { setState(() { showToTopBtn = false; }); } else if (_controller.offset >= 1000 && showToTopBtn == false) { setState(() { showToTopBtn = true; }); } }); } @override void dispose() { //爲了不內存泄露,須要調用_controller.dispose _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("滾動控制")), body: Scrollbar( child: ListView.builder( itemCount: 100, itemExtent: 50.0, //列表項高度固定時,顯式指定高度是一個好習慣(性能消耗小) controller: _controller, itemBuilder: (context, index) { return ListTile(title: Text("$index"),); } ), ), floatingActionButton: !showToTopBtn ? null : FloatingActionButton( child: Icon(Icons.arrow_upward), onPressed: () { //返回到頂部時執行動畫 _controller.animateTo(.0, duration: Duration(milliseconds: 200), curve: Curves.ease ); } ), ); } } 複製代碼