本篇主要讲解最基础的下拉刷新和上拉加载的用法,以后再做如何实现更加酷炫的效果。

现在Flutter Packages里面已经有人写好了的第三方库了,但是我们不应该局限于此,

我们要弄清方法,后面使用开源库才会知其然,知其所以然。

不仅仅只是会使用开源库提供的widget,然后:new SwipeRefreshLayout(xxxx),

这样的程序员 大街上随便拉一个都可以做到。

冒着被媳妇嫌弃的风险,大晚上写文章,我偷偷告诉她写的是“婚后感言”

Flutter提供一个android样式的下拉刷新指示器,在android中下拉刷新控件是SwipeRefreshLayout,在Flutter中使用RefreshIndicator,用它来包裹一个可滚动的widget。RefreshIndicator

使用方法很简单,需要注意的是RefreshIndicator内部参数“ScrollPhysics”,

需要指定为"AlwaysScrollableScrollPhysics",确保滚动视图始终可以滚动。

总觉得不贴个代码很别扭,来个下拉刷新样例代码:

new RefreshIndicator(
child: new ListView.builder(
itemBuilder: (context, index) {
return new Container(
height: 20.0,
alignment: Alignment.topLeft,
key: new PageStorageKey(index),
child:new Text(mDataList[index].content),
);
},
physics: const AlwaysScrollableScrollPhysics(),
itemCount: mDataList.length,
),
//刷新方法 onRefresh: () => _handlerRefresh()
)

下面增加模拟网络加载耗时请求的方法:

Future _handlerRefresh() async {
//模拟耗时5秒 await new Future.delayed(new Duration(seconds: 5));
setState(() {
mDataList=generateData();
});
return null;
}

接下来进入上拉加载的探索,我们需要搞清楚下面两个问题:

问题一:使用什么来获取滚动偏移量?

1.我们可以使用ScrollController 滚动控制器:

添加监听器,用于监听当前偏移量等信息。

滚动控制器内部创建ScrollPosition以管理特定于单个Scrollable小部件的状态。

ScrollController 通常与ListView,GridView,CustomScrollView一起使用。

我们需要定义一个有状态的小部件RefreshState,在这个State中addListener来监听滚动信息,以及removeListener。State class Structure

我们需要重写这两个方法来add和remove我们的Listener

小插曲:

需要注意一点的是IOS和Android的列表在下拉上拉效果是不一样的,IOS自带弹性效果,那么我们可以通过控制ScrollPhysics来让IOS和Android效果一致:ClampingScrollPhysics :防止滚动偏移超出内容范围,Android列表的效果,到达边缘都是拉不动的。

BouncingScrollPhysics:iOS弹跳行为,超出内容边界,还可以继续拉出一段空白空间出来,松开回弹回去。

我们现在使用的是AlwaysScrollableScrollPhysics,使用它表示确保滚动视图始终可以滚动

如果你的列表没有超出屏幕,你不使用这个属性,你的列表是无法滑动的。如果你的列表确定是可以超出屏幕的,你自己选择把,我建议还是使用AlwaysScrollableScrollPhysics这个稳妥。

回到正题:

上面讲到ScrollController添加listener之后我们可以通过scrollController.position.pixels,可以获得滚动的view实际滚动偏移量。

而ScrollPosition是:确定滚动视图中可见的内容部分。

我们可以使用scrollPosition指定scrollView定位到对应的内容位置区域:

1.scrollController.position.jumpTo(double value);

2.scrollController.jumpTo(double value);这个实际上也是调用的是position.jumpTo(double value);

当然有同学发现"scrollController.offset"也是可以获得到滚动的view实际滚动偏移量,大哥,它指向的是scrollposition.pixels。

问题二:如何判断当前滚动到了列表底部?

有了问题一相关内容介绍,我都可以直接贴代码了,废话不多说,自己看源码注释解释

///maxScrollExtent 可以滑动的最大距离bool isBottom = widget.scrollController.position.pixels ==
widget.scrollController.position.maxScrollExtent;

这段代码是判断当前是否滚动到了列表底部。可能IOS设备比较恶心,上下弹的效果可能导致这个值比较的不是很准确,前提是你的第一页数据如果没有超出屏幕的情况下!

建议测试的时候,数据列表弄多一点,尽量超出屏幕,否则你会发现很多不如意的问题。

最后,写一个上拉加载更多,下拉刷新列表:

首先定义一个有状态小部件:

class PullToRefreshWidget extends StatefulWidget {
@override
State createState() => PullToRefreshState();
}

紧接着定义一个状态类,在状态类中定义一个ScrollController:

class PullToRefreshState extends State {
ScrollController _scrollController = new ScrollController();
@override
Widget build(BuildContext context) {
return new RefreshWidget(
scrollController: _scrollController,
);
}
}

为了做的比较让别人难懂,装逼下,在定义一个有状态小部件:

class RefreshWidget extends StatefulWidget {
RefreshWidget({this.scrollController});
final ScrollController scrollController;
@override
State createState() => RefreshState();
}

最后定义RefreshState:

class RefreshState extends State

在问题一中我们已经告诉大家如何addListener和removeListener:

@override
void initState() {
super.initState();
widget.scrollController.addListener(_updateScrollPosition);
mDataList = getDatas();
}
@override
void dispose() {
widget.scrollController.removeListener(_updateScrollPosition);
super.dispose();
}

加载更多如何显示呢?其实就是一个占位,我们使用SizedBox和Opacity组合使用:

new Container(
alignment: Alignment.center,
padding: EdgeInsets.all(5.0),
child: new SizedBox(//约束下 height: 40.0,
width: 40.0,
child: new Opacity(
opacity: isLoadingMore?1.0:0.0,//透明度,显示隐藏 child: new CircularProgressIndicator(),
),
),
)

上拉加载和下拉刷新,关键就是两个状态之间切换的时候,标志要判断对,就不会出现混乱,

目前还有一个问题是,无法禁用下拉加载,知道的老铁欢迎留言回复。

来个最终效果视频,最下方贴上RefreshState中的全部代码:https://www.zhihu.com/video/1005582083640553472

class RefreshState extends State {
bool isRefreshing = false;
bool isLoadingMore = false;
var mDataList;
final GlobalKey _refreshIndicatorKey =
new GlobalKey();
void bindItemData(List itemDatas) {
setState(() {
mDataList.addAll(itemDatas);
isLoadingMore = false;
isRefreshing = false;
});
}
void _updateScrollPosition() {
bool isBottom = widget.scrollController.position.pixels ==
widget.scrollController.position.maxScrollExtent;
/*print(">>>>isLoadingMore:$isLoadingMore""+isRefreshing:$isRefreshing""+isBottom:$isBottom");*/
if (!isLoadingMore && isBottom && !isRefreshing) {
setState(() {
isRefreshing = false;
isLoadingMore = true;
_loadMore();
});
}
}
Future _loadMore() async {
//模拟耗时3秒 await new Future.delayed(new Duration(seconds: 5));
bindItemData(generateData());
return null;
}
@override
void initState() {
super.initState();
widget.scrollController.addListener(_updateScrollPosition);
mDataList = getDatas();
}
@override
void dispose() {
widget.scrollController.removeListener(_updateScrollPosition);
super.dispose();
}
@override
Widget build(BuildContext context) {
return new RefreshIndicator(
child: _buildListView(), onRefresh: _handlerRefresh);
}
Widget _buildListView() {
return new ListView.builder(
key: _refreshIndicatorKey,
controller: widget.scrollController,
itemBuilder: (context, index) {
if (index == mDataList.length) {
return new Container(
alignment: Alignment.center,
padding: EdgeInsets.all(5.0),
child: new SizedBox(
height: 40.0,
width: 40.0,
child: new Opacity(
opacity: isLoadingMore?1.0:0.0,
child: new CircularProgressIndicator(),
),
),
);
}
return new Column(
children: [
new Container(
width: double.infinity,
height: 38.0,
alignment: Alignment.centerLeft,
key: new PageStorageKey(index),
child: new Text(mDataList[index].content,),
),
new Divider(
height: 2.0,
)
],
);
},
physics: const AlwaysScrollableScrollPhysics(),
itemCount: mDataList.length + 1,
);
}
Future _handlerRefresh() async {
if(!isLoadingMore){
setState(() {
isRefreshing = true;
isLoadingMore = false;
});
//模拟耗时3秒 await new Future.delayed(new Duration(seconds: 5));
setState(() {
mDataList = generateData();
isRefreshing = false;
isLoadingMore = false;
});
widget.scrollController.jumpTo(0.0);
}
//widget.scrollController.animateTo(0.0, duration: new Duration(milliseconds:100), curve: Curves.linear); return null;
}
}

晚安,各位老铁。