1.ListView
- 類似于iOS的UITableview;
| 屬性(ListView) | 類型 | 可選? | 作用 |
|---|---|---|---|
| children | List<Widget> | 命名可選 | 列表內(nèi)顯示的子組件 |
| scrollDirection | Axis(enum) | 命名可選(默認(rèn):Axis.vertical) | 滾動(dòng)方向 |
| itemExtent | double | 命名可選 | 行高(如果設(shè)置橫向滾動(dòng),則表示列寬) |
| 屬性(ListTile),類似于tableviewcell | 類型 | 可選? | 作用 |
| leading | Widget | 命名可選 | 左側(cè)圖標(biāo)(不一定是圖標(biāo),只要是Widget) |
| title | Widget | 命名可選 | 主標(biāo)題 |
| subtitle | Widget | 命名可選 | 副標(biāo)題 |
| trailing | Widget | 命名可選 | 右側(cè)圖標(biāo)(不一定是圖標(biāo),只要是Widget) |
| onTap | 閉包 | 命名可選 | 點(diǎn)擊回調(diào) |
1.1 ListView基本使用
ListView可以沿一個(gè)方向(垂直或水平方向,默認(rèn)是垂直方向)來排列其所有子Widget。
一種最簡單的使用方式是直接將所有需要排列的子Widget放在ListView的children屬性中即可。
我們來看一下直接使用ListView的代碼演練:
class MyHomeBody extends StatelessWidget {
const MyHomeBody({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: List.generate(39, (index) {
//ListTile類似于tableviewcell
return ListTile(
leading: Icon(Icons.pets),
trailing: IconButton(icon: Icon(Icons.favorite,color: Colors.red,),),
title: Text('第 ${index+1} 行'),
subtitle: Text('副標(biāo)題'),
onTap: () {
print('點(diǎn)擊了tableview ${index+1}');
},
);
}),
);
}
}
1.2 ListView的滾動(dòng)方向--scrollDirection
class MyHomeBody2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
//children 在滾動(dòng)方向的高度(垂直滾動(dòng))或者寬度(橫向滾動(dòng))
itemExtent: 100,
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),
],
);
}
}
1.3 ListView的構(gòu)造方法
| 構(gòu)造方法 | 使用場(chǎng)景 |
|---|---|
| ListView();默認(rèn)的構(gòu)造方法 | 當(dāng)已經(jīng)有固定個(gè)數(shù)的chidren,且chidren的個(gè)數(shù)不是十分龐大的時(shí)候可以使用這個(gè)方法,因?yàn)檫@個(gè)方法默認(rèn)會(huì)盡可能多地創(chuàng)建出子組件,以方便ListView進(jìn)行展示 |
| ListView.builder(); | 當(dāng)有不確定數(shù)量,或者大量的cell顯示的時(shí)候,可以使用這個(gè)方式,系統(tǒng)會(huì)自動(dòng)在ListView將要顯示某個(gè)cell的時(shí)候,才把它build出來; |
| ListView.separated(); | 有3個(gè)@required參數(shù): 前面兩個(gè)和buider方法一樣,第三個(gè)參數(shù)是separatorBuilder,用來添加一個(gè)cell與cell之間的分割線的,這個(gè)方法創(chuàng)建ListView不可以給cell設(shè)置高度 |
//使用builder構(gòu)造器創(chuàng)建ListView
class MyHomeBody2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
//左側(cè)圖標(biāo)
leading: Icon(Icons.pets),
//右側(cè)圖標(biāo)
trailing: IconButton(
icon: Icon(
Icons.favorite,
color: Colors.red,
),
),
//主標(biāo)題
title: Text('第 ${index + 1} 行'),
//副標(biāo)題
subtitle: Text('副標(biāo)題'),
onTap: () {
print('點(diǎn)擊了tableview ${index + 1}');
});
},
itemCount: 10,
scrollDirection: Axis.vertical,
);
}
}
//separated
class MyHomeBody3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
//左側(cè)圖標(biāo)
leading: Icon(Icons.pets),
//右側(cè)圖標(biāo)
trailing: IconButton(
icon: Icon(
Icons.favorite,
color: Colors.red,
),
),
//主標(biāo)題
title: Text('第 ${index + 1} 行'),
//副標(biāo)題
subtitle: Text('副標(biāo)題'),
onTap: () {
print('點(diǎn)擊了tableview ${index + 1}');
});
},
separatorBuilder: (BuildContext context, int index){
return Divider(
color: Colors.red,
//Divider的高度
height: 1,
//左側(cè)邊距
indent: 10,
//右側(cè)邊距
endIndent: 30,
//分割線的高度
thickness: 1,
);
},
itemCount: 100);
}
}

image.png
2. GridView
| 構(gòu)造方法 | 意義 |
|---|---|
| GridView() | 默認(rèn)構(gòu)造方法中有個(gè)感覺參數(shù):gridDelegate,用來指定構(gòu)造的方式的 |
| GridView.builder() | 作用類似于ListView的builder();方法 |
| GridView.count() | 在默認(rèn)構(gòu)造方法的基礎(chǔ)上選擇了SliverGridDelegateWithFixedCrossAxisCount作為delegate |
| GridView.extent() | 這個(gè)構(gòu)造方法是在默認(rèn)構(gòu)造器方法的基礎(chǔ)上,指定了代理模式為: SliverGridDelegateWithMaxCrossAxisExtent |
2.1 Demo
//GridView_builder_Demo
class GridView_builder_Demo extends StatelessWidget {
const GridView_builder_Demo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
return GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: width/3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.8,
),
itemBuilder: (BuildContext context, int index) {
return Container(
color:Color.fromARGB(100, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
},
itemCount: 10,
);
}
}
//GridView 的默認(rèn)構(gòu)造器案例 代理2
class GridViewDemo_Constructor2 extends StatelessWidget {
const GridViewDemo_Constructor2({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final width = (MediaQuery.of(context).size.width)/2;
return Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 8),
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: width,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.8,
),
children: List.generate(30, (index) {
return Container(
color: Color.fromARGB(100, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
width: 20,
height: 20,
);
}),
),
);
}
}
//GridView 的默認(rèn)構(gòu)造器案例 代理1
class GridViewDemo_Constructor1 extends StatelessWidget {
const GridViewDemo_Constructor1({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 8),
child: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//指定交叉軸上,可以擺放多少個(gè)item
crossAxisCount: 3,
//指定item的 寬/高 比例
childAspectRatio: 1,
//設(shè)定交叉軸上 item 之間的間隔
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
children: List.generate(30, (index) {
return Container(
color: Color.fromARGB(100, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
width: 20,
height: 20,
);
}),
),
);
}
}

image.png
3.Sliver
- ListView 繼承于 BoxScrollView ,
- BoxScrollView 繼承于 ScrollView ,且BoxScrollView是abstract(抽象類)
- ScrollView 繼承于 StatelessWidget , ScrollView 是 abstract(抽象類)
直接繼承于Widget的類才會(huì)
@override(重寫)Widget build(BuildContext context);方法
看一下ScrollView源碼中重寫的build方法
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
//這個(gè)才是滾動(dòng)視圖內(nèi),真正可以滾動(dòng)的內(nèi)容
// 繼續(xù)查源碼,可以發(fā)現(xiàn)buildSlivers();方法在ScrollView內(nèi)沒有實(shí)現(xiàn),需要它的子類去實(shí)現(xiàn)
// Build the list of widgets to place inside the viewport.Subclasses should override this method to build the slivers for the inside of the viewport.
// 翻譯: 構(gòu)建要放置在視窗內(nèi)的小部件列表。子類應(yīng)該重寫這個(gè)方法來為視窗內(nèi)部構(gòu)建切片。
// 那么ScrollView 的子類有哪些呢?
//1.CustomScrollView 2.BoxScrollView(abstract)
// BoxScrollVeiw的子類 : 1. ListView 2.GridView
//后續(xù)實(shí)現(xiàn)部分省略,可自行查看Flutter源碼
}
BoxScrollVeiw中重寫的 buildSlivers();
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
// 最后返回的是一個(gè)Widget數(shù)組,數(shù)組元素是sliver,
// sliver的初始化在上面:
/// Subclasses should override this method to build the layout model.
// 子類需要重寫此方法
// Widget sliver = buildChildLayout(context);
// 所以BoxScrollVeiw的子類: 1. ListView 2.GridView 重寫這個(gè)方法來實(shí)現(xiàn)滾動(dòng)視圖
return <Widget>[ sliver ];
}
3.1. Slivers的基本使用
因?yàn)槲覀冃枰押芏嗟腟liver放在一個(gè)CustomScrollView中,所以CustomScrollView有一個(gè)slivers屬性,里面讓我們放對(duì)應(yīng)的一些Sliver:
- SliverList:類似于我們之前使用過的ListView;
- SliverFixedExtentList:類似于SliverList只是可以設(shè)置滾動(dòng)的高度;
- SliverGrid:類似于我們之前使用過的GridView;
- SliverPadding:設(shè)置Sliver的內(nèi)邊距,因?yàn)榭赡芤獑为?dú)給Sliver設(shè)置內(nèi)邊距;
- SliverAppBar:添加一個(gè)AppBar,通常用來作為CustomScrollView的HeaderView;
- SliverSafeArea:設(shè)置內(nèi)容顯示在安全區(qū)域(比如不讓齊劉海擋住我們的內(nèi)容)
使用CustomScrollView演示一下:SliverGrid+SliverPadding+SliverSafeArea的組合
| CustomScrollView的參數(shù)/屬性 | 類型 | 可選? | 作用 | |
|---|---|---|---|---|
| scrollDirection | Axis (enum) | 命名可選 | 滑動(dòng)方向 | |
| controller | ScrollController | 命名可選 | 類似于管理器 | |
| slivers | <Widget>[] | 命名可選 | 被滾動(dòng)視圖顯示的item內(nèi)容 | |
| SliverGrid | ||||
| delegate | SliverChildDelegate | @required | 用來構(gòu)建單元格內(nèi)容的 | |
| gridDelegate | SliverGridDelegate | @required | 用來構(gòu)造滾動(dòng)視圖樣式的 |
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
//滾動(dòng)方向
scrollDirection: Axis.vertical,
//item
slivers: [
//SliverSafeArea 用來設(shè)置 滾動(dòng)視圖在安全區(qū)域內(nèi)展示,
SliverSafeArea(
//SliverPadding ,用來設(shè)置滾動(dòng)視圖中,單元格在橫向和縱向的 paddding
sliver: SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 8,vertical: 8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//每一行顯示多少個(gè)
crossAxisCount: 3,
//item的 寬/高 比
childAspectRatio: 1,
//主軸上,單元格之間的講個(gè)
mainAxisSpacing: 8,
//交叉軸上單元格的間隔
crossAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Color.fromARGB(
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256)),
child: Text('item${index}',style: TextStyle(color: Colors.red,fontSize: 30),),
alignment: Alignment.center,
);
},
childCount: 30,
),
),
),
),
],
);
}
}

image.png
3.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.png
4.滾動(dòng)的監(jiān)聽
在滾動(dòng)視圖中實(shí)現(xiàn)滾動(dòng)監(jiān)聽,有兩種方案
- 方案1: 使用controller進(jìn)行監(jiān)聽
- 方案2: 使用NotificationListener進(jìn)行監(jiān)聽
方案1:demo
class RF_ContentPage extends StatefulWidget {
const RF_ContentPage({
Key key,
}) : super(key: key);
@override
_RF_ContentPageState createState() => _RF_ContentPageState();
}
class _RF_ContentPageState extends State<RF_ContentPage> {
//定義一個(gè)ScrollController 對(duì)象,對(duì)滾動(dòng)視圖進(jìn)行管理的
final ScrollController rfc = ScrollController(initialScrollOffset: 100);
// bool值,用于判斷是否需要顯示floatbutton
bool isShowFb = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('監(jiān)聽滑動(dòng)'),
),
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.person),
title: Text('第 ${index + 1}行'),
);
},
controller: rfc,
),
//floatbutton,根據(jù)isShowFB這個(gè)bool值進(jìn)行判斷顯示
floatingActionButton: isShowFb ? FloatingActionButton(onPressed: (){
rfc.animateTo(0, duration: Duration(seconds: 1), curve: Curves.easeInQuad);
},
child: Icon(Icons.arrow_upward),
) : null,
);
}
// 重寫initState方法,對(duì)滾動(dòng)進(jìn)行監(jiān)聽;
@override
void initState() {
// TODO: implement initState
super.initState();
//ListView的controller通過添加listener添加滾動(dòng)監(jiān)聽
rfc.addListener(() {
print('當(dāng)前偏移量:${rfc.offset}');
setState(() {
isShowFb = rfc.offset >= 100;
});
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
rfc.dispose();
}
}
方案2demo
class RF_ContentPage extends StatefulWidget {
const RF_ContentPage({
Key key,
}) : super(key: key);
@override
_RF_ContentPageState createState() => _RF_ContentPageState();
}
class _RF_ContentPageState extends State<RF_ContentPage> {
final ScrollController rfc = ScrollController(initialScrollOffset: 100);
bool isShowFb = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('監(jiān)聽滑動(dòng)'),
),
body: NotificationListener(
onNotification: (ScrollNotification notified) {
// if(notified is ScrollStartNotification ){
// print('開始滾動(dòng)');
// }else if(notified is ScrollEndNotification){
// print('結(jié)束滾動(dòng)');
// }else if( notified is ScrollUpdateNotification){
// print('正在滾動(dòng)');
// }
//滾動(dòng)偏移量
print('滾動(dòng)偏移量:${notified.metrics.pixels}');
return true;
},
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.person),
title: Text('第 ${index + 1}行'),
);
},
controller: rfc,
),
),
floatingActionButton: isShowFb
? FloatingActionButton(
onPressed: () {
rfc.animateTo(0,
duration: Duration(seconds: 1), curve: Curves.easeInQuad);
},
child: Icon(Icons.arrow_upward),
)
: null,
);
}
@override
void dispose() {
rfc.dispose();
// TODO: implement dispose
super.dispose();
}
}
總結(jié):
controller:可以直接去的滾動(dòng)的控制對(duì)象,對(duì)齊進(jìn)行控制,但是不能監(jiān)聽滾動(dòng)的開始,結(jié)束狀態(tài)
NotificationListener: 可以獲取各種滾動(dòng)狀態(tài),但是不持有滾動(dòng)的管理對(duì)象