滑动布局-ScrollView、SingleChildScrollView 、ListView 、GridView
可滑动的Widget-ScrollView
我在用Column进行布局的时候,由于子节点过多,导致出现了OverFlow的提示,这个就是告诉我们内容超出范围了,这个在Android里来做就不会给你提示了,他会直接截取掉,不显示,为了解决这个错误我第一时间想到的就是像Android一样给他套一个ScrollView,Flutter的ScrollView不是一个可以直接使用的滑动widget,他是一个抽象类,这里记录一下ScrollView的一些属性,也是相当于记录了ScrollView子类的一些共性参数
ScrollView({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
bool primary,
ScrollPhysics physics,
this.shrinkWrap = false,
this.center,
this.anchor = 0.0,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
})
scrollDirection: 设置滑动的方向,水平方向或者竖直方向
reverse:是否安装阅读方向相反的方向滑动,默认为false,这个参数就是说我的初始位置是在哪,这个会根据语言环境来改变的,比说国语的阅读方向是从左到右,那么他的起始点就在左边,如果将参数设置为true,表示按阅读方向相反的方向滑动,那么他的起始点就在右边,直接看图可能会好一些
SingleChildScrollView(
reverse: true,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text(
"编号:",
style: TextStyle(color: Colors.red),
),
Text("头0000000000001 "),
Text("0000000000001 00000000000010000000000001000000000000100000000000010000000000001000000000000100000000000010000000000001000000000000100000000000010000000000001000000000000尾")
,
],
),
)
- 默认的情况,不设置或设置为false的情况下
- 设置为true的情况下,我一进来看到的就是尾部,就是Row中的最后端
controller:接收一个ScrollController(默认是用PrimaryScrollController)这个参数是用于控制滚动位置和监听滚动事件,具体用法看后续
primary :是否使用默认的PrimaryScrollController
physics :决定滑动widget如何响应用户的操作,由于滑动列表滑动到边界的时候会出现一些效果,Android与iOS是不一样的,所以我们可以通过这个参数来设置统一效果ClampingScrollPhysics:Android下微光效果。BouncingScrollPhysics:iOS下弹性效果
shrinkWrap:该属性决定是否根据子widget的长度来决定滚动范围,默认情况下为false,默认情况下,滚动范围会尽可能的占据位置,但是如果滚动视图处在于一个无边界的widget中,该参数就必须设置为true
cacheExtent:设置缓存数量,在可见视图的之前和之后
可滑动的widget-SingleChildScrollView
singleChildScrollView他接收一个子类,我们可以把他想成Android中的ScrollViwe,他的参数与ScrollView的一些参数一样,其实ScrollView中的参数我们可以把他当成是滑动widget的一些基本参数
SingleChildScrollView({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.padding,
bool primary,
this.physics,
this.controller,
this.child,
this.dragStartBehavior = DragStartBehavior.start,
})
1234567891011
我们可以看到他的参数基本上在Scrollview中都有,只是多了一个child而已
ListView
ListView是我们用的比较多的,也是比较常见的一个列表控件,他是继承自ScrollView,参数基本一样
ListView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
})
123456789101112131415161718
itemExtent:子widget的高度或者宽度,如果滑动方向是水平的,那么就是子widget的宽度,如果方向是垂直的,那么就是子widget的高度;如果对子widget有特殊的宽高设置要求,我们最好采用itemExtent来设置子widget的宽或者高,因为提前设置好宽高比动态去获取宽高会更高效
addAutomaticKeepAlives:该字段表示是否将列表项包裹在AutomaticKeepAlive Widget中,默认为true,效果就是,列表子widget滑出可见视图时,其状态还会被保持,数据还会保持,并且列表项不会被GC掉
addRepaintBoundaries:表示是否将列表项包裹在RepaintBoundary中,默认为true,如果包裹则列表项不会重绘
- ListView的用法
- 通过构造来创建,通过构造来创建的方式只能用于数据较少的时候,他的children接收一个widget的list,这种方式和singleChildScrollView+线性布局的方式没什么区别,都是先把widget创建好,然后再表现出来,没有达到需要的时候再创建,所以只适合数据量较少的时候
ListView(
children: data == null ? _loging() : _item(),
)
List<Widget> _item() {
return data.map((item) {
return Card(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: _getRow(item),
),
elevation: 3.0,
margin: const EdgeInsets.all(10.0),
);
}).toList();
}
12345678910111213141516
- 通过ListView.build来创建,这种方式适合数据项比较多的或者是无限数据(无限数据就是我们请求后端不停的拉数据,这时候的数据是无法确定的)的时候,他是在列表项真正要显示的时候才会被创建
ListView.builder(
itemCount: 20,
itemBuilder: (BuildContext context,int index){
return ListTile(title:Text("ssss"+"$index"));
})
12345
itemCount:条目的数量,如果是无限数据的时候就不要去设置了
itemBuilder:接收一个IndexedWidgetBuilder,返回一个widget,当滑动到对应的index时,返回对应的widget
- 通过ListView.separated来创建,这个和ListView.build一样,只是多了分割线的设置separatorBuilder
ListView.separated(
itemCount: 20,
separatorBuilder: (BuildContext context,int index){
return Divider(color: Colors.red,);
},
itemBuilder: (BuildContext context,int index){
return ListTile(title:Text("ssss"+"$index"));
})
12345678
separatorBuilder:分割线Builder,返回一个分割线
- ListVIew上拉加载于下拉刷新
class home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return HomeState();
}
}
class myState extends State {
List data;
@override
void initState() {
// TODO: implement initState
super.initState();
_getItemData();
}
void _getItemData() async {
await http
.get("http://www.wanandroid.com/project/list/1/json?cid=1")
.then((http.Response response) {
var convJson = json.decode(response.body);
convJson = convJson["data"]["datas"];
print(convJson);
setState(() {
data = convJson;
});
});
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("网络数据列表"),
),
body: ListView(
children: data == null ? _loging() : _item(),
),
);
}
List<Widget> _item() {
return data.map((item) {
return Card(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: _getRow(item),
),
elevation: 3.0,
margin: const EdgeInsets.all(10.0),
);
}).toList();
}
Widget _getRow(item) {
return Row(
children: <Widget>[
Column(
children: <Widget>[
Text(
"${item["title"]}",
style: TextStyle(color: Colors.blue, fontSize: 20.0),
),
Text(
"${item["desc"]}",
maxLines: 3,
),
],
),
ClipRect(
child: FadeInImage.assetNetwork(
placeholder: "images/lake.jpg",
image: "${item["envelopePic"]}",
width: 50.0,
height: 50.0,
fit: BoxFit.fitWidth,
),
)
],
);
}
List<Widget> _loging() {
return <Widget>[
Container(
height: 300.0,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(
strokeWidth: 1.0,
),
Text("记载中。。。")
],
),
))
];
}
}
class HomeState extends State {
List data;
ScrollController _scrollController = ScrollController();
int page = 1;
bool isLoad = false;
@override
void initState() {
// TODO: implement initState
super.initState();
_pullNet(); //默认第一次进入的时候请求数据
_scrollController.addListener(() {
//滑动监听
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print("滑动到底部");
page++;
_pullNet(); //调用加载更多方法,这里我们可以传入page,此处随意写个方法代替下
}
});
}
void _pullNet() async {
if (!isLoad) {
isLoad = true;
await http
.get(
"http://www.wanandroid.com/project/list/1/json?cid=1")
.then((http.Response response) {
var converDatajson = json.decode(response.body);
converDatajson = converDatajson["data"]["datas"];
print(converDatajson);
setState(() {
List listData = converDatajson; //每次请求到的数据
// data.insertAll(data.length - 1, listData);
if(page==1){
data=listData;
}else {
data.addAll(listData);
}
isLoad = false; //将正在加载中的标示改回来
});
});
}
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("listview"),
),
body: data == null
? _loading()
: RefreshIndicator(
child: ListView.separated(
itemCount: data.length + 1, //加1的目的就是为了能够在底部给他加上一个加载中的widget
separatorBuilder: (BuildContext context, int index) {
//设置一个灰色的分割线
return Divider(
color: Colors.grey,
);
},
itemBuilder: (BuildContext context, int index) {
if (index > data.length-1) {
//当index大于数据长度的时候我们返回一个转圈圈的widget
//当前是最后一个widget
return Container(
//给条目返回一个转圈圈的加载widget
alignment: Alignment.center,
padding: const EdgeInsets.all(6.0),
child: SizedBox(
width: 30.0,
height: 30.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
),
));
}else {
return _item(index); //如果数据不大于,那么直接返回列表项
}
},
controller: _scrollController, //添加一个滑动控制器
),
onRefresh: _RefreshPullNet, //下拉请求数据
));
}
Future<Null> _RefreshPullNet() async{
//下拉刷新的时候将page改为1
page = 1;
_pullNet();
}
Widget _item(index) {
return Card(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: _getRowWidget(data[index]),
),
elevation: 3.0,
margin: const EdgeInsets.all(10.0),
);
}
Widget _getRowWidget(item) {
return Row(
children: <Widget>[
Flexible(
flex: 1,
fit: FlexFit.tight, //这里就是相当于matchParent
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
Text(
"${item["title"]}".trim(),
style: TextStyle(color: Colors.blue, fontSize: 20.0),
textAlign: TextAlign.left,
),
Text(
"${item["desc"]}".trim(),
maxLines: 3,
)
],
)
],
),
),
ClipRect(
child: FadeInImage.assetNetwork(
placeholder: "images/lake.jpg",
image: "${item["envelopePic"]}",
width: 50.0,
height: 50.0,
fit: BoxFit.fitWidth,
),
),
],
);
}
Widget _loading() {
return
Container(
height: 300.0,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(
strokeWidth: 1.0,
),
Text("加载中..."),
],
),
),
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
_scrollController.dispose(); //记得将scrollController dispose掉
}
}
GridView
GridView与ListView一样都是我们用的比较多的滑动列表,他们都是继承自ScrollView,他的效果和Android的GridView是一样的,我们创建一个三列的GridView,子节点有listdata.length个(listdata为请求到的数据),使用如下
第一种:
GridView.count(
crossAxisCount: 3,
children: listData.map((item) {
return Text("${item["SS"]}");
}).toList(),
)
123456
第二种:
GridView.count(
crossAxisCount: 3,
children: List.generate(
listData.length,
(index) {
return Text("${listData[index]}");
},
))
12345678
这两种方法其实都是返回一个widget集合,效果是一样的,GridView是继承自ScrollView的,所以参数基本上我们可以
GridView总共提供了四种调用方式给我们,我们可以根据自己的需要去选择调用
GridView.builder //数据量较多的时候采用此种方式
GridView.custom
GridView.count //内部就是SliverGridDelegateWithFixedCrossAxisCount算法
GridView.extent //内部就是SliverGridDelegateWithMaxCrossAxisExtent算法
1234
GridView与ListView的差距在于设置网格,我们关注一下gridDelegate这个参数,他是用于控制gridView的子节点的展示,flutter给我们提供了两个可选项,SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithFixedCrossAxisCount:实现了一个横轴子节点为固定数量的算法
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
123456
crossAxisCount:横轴元素的数量
mainAxisSpacing :主轴方向的间距
crossAxisSpacing:横轴方向子元素的间距
childAspectRatio:子元素在横轴长度和主轴长度的比例
SliverGridDelegateWithMaxCrossAxisExtent:实现了一个横轴子节点为固定最大长度的算法
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
123456
maxCrossAxisExtent:设置子节点的最大长度,就是通过限制子节点的最大长度来达到每一行能展示的条目