在Flutter开发之ListView组件(21) 文章中,我们了解了ListView组件的基本使用。但是数据比较少,没有涉及分页加载。而实际开发中,下拉刷新和分页加载几乎是所有APP的标配。在iOS 开发中我们通过MJRefresh 给UITableView添加mj_header和mj_footer刷新事件来触发下拉刷新和分页加载实现的。那么我们看一下Flutter中的下拉刷新跟上拉加载更多是如何实现的尼?
首先创建一个不带RefreshIndicator
ListView组件。就像之前的一样。
基础代码
import 'package:flutter/material.dart';
import 'package:hello/Models/Todo.dart';
import 'package:hello/Moudles/DetailScreen.dart';
class ListViewRefreshTest extends StatefulWidget {
@override
createState()=> _ListViewRefreshState();
}
Widget _listViewWidget (BuildContext context, List sourceList) {
if(sourceList.length==0) {
return null;
} else {
return ListView.builder(
itemCount: sourceList.length,
//设置physics属性总是可滚动
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context,index) {
return ListTile(
title: Text(sourceList[index].title,style: TextStyle(fontSize: 22),),
subtitle: Text(sourceList[index].description,style: TextStyle(fontSize: 18)),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder:(context)=> DetailScreen(todo: sourceList[index]),
)
);
},
);
}
);
}
}
List _myDataList(){
return List.generate(20,
(i) => new Todo(
'我是表第 $i 项',
'我是内容 $i',
)
);
}
class _ListViewRefreshState extends State {
List entityList = _myDataList();
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: AppBar(
title: Text('刷新和分页加载'),
),
body: _listViewWidget(
context,
entityList,
),
);
}
}
在Flutter中是通过RefreshIndicator
组件来完成下拉刷新的监听动作的。它跟原生Android中的SwipeRefreshLayout
设计思路一样,而且t在外观上几乎也一样,都是为了简化完成下拉刷新的监听动作。Google 发布的Material Design
语言更像是一套界面设计标准。
class RefreshIndicator extends StatefulWidget {
/// Creates a refresh indicator.
///
/// The [onRefresh], [child], and [notificationPredicate] arguments must be
/// non-null. The default
/// [displacement] is 40.0 logical pixels.
///
/// The [semanticsLabel] is used to specify an accessibility label for this widget.
/// If it is null, it will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel].
/// An empty string may be passed to avoid having anything read by screen reading software.
/// The [semanticsValue] may be used to specify progress on the widget.
const RefreshIndicator({
Key key,
@required this.child,
this.displacement = 40.0,
@required this.onRefresh,
this.color,
this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
}) : assert(child != null),
assert(onRefresh != null),
assert(notificationPredicate != null),
super(key: key);
/// The widget below this widget in the tree.
///
/// The refresh indicator will be stacked on top of this child. The indicator
/// will appear when child's Scrollable descendant is over-scrolled.
///
/// Typically a [ListView] or [CustomScrollView].
上面的构造方法中必要参数的注释说明,光说不如直接实战来的直接。我们在 1.中的代码基础上添加 RefreshIndicator
组件来完成下拉刷新的功能。我们只需要3步:
RefreshIndicator
来warpper ListView
onRefresh
并在方法中更新数据源增加代码片段
// 下拉刷新
Future _handleRefresh() async {
print('-------开始刷新------------');
await Future.delayed(Duration(seconds: 2), () { //模拟延时
setState(() {
entityList.clear();
entityList = List.generate(10,
(i) => new Todo(
"刷新我是表第 $i 项",
'刷新我是内容 $i'
)
);
return null;
});
});
}
修改代码片段
body: RefreshIndicator(
child: _listViewWidget(
context,
entityList
),
onRefresh: _handleRefresh
),
效果如图:
刷新完成了,接下来我们实现分页加载。我们借助ScrollController给ListView添加滑动监听事件。也需要两步:
ScrollController
和isLoadData
_scrollController.addListener
并在方法中更新数据源新增代码片段
ScrollController _scrollController = new ScrollController();
bool isLoadData = false;
@override
void initState() {
super.initState();
print('你好!');
_scrollController.addListener((){
if (_scrollController.position.pixels ==
_scrollController.position.maxScrollExtent) {
print("------------加载更多-------------");
_getMoreData();
callTimes = callTimes+1
}
});
}
Future _getMoreData() async {
await Future.delayed(Duration(seconds: 2), () { //模拟延时操作
if (!isLoadData) {
isLoadData = true;
setState(() {
isLoadData = false;
List newList = List.generate(5,
(i) => Todo(
'分页--我是表第 ${i + entityList.length} 项',
'刷新我是内容 ${i + entityList.length} '
)
);
entityList.addAll(newList);
});
}
});
}
完整代码如下:
import 'package:flutter/material.dart';
import 'package:hello/Models/Todo.dart';
import 'package:hello/Moudles/DetailScreen.dart';
class ListViewRefreshTest extends StatefulWidget {
@override
createState()=> _ListViewRefreshState();
}
class LoadMoreView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(child: Padding(
padding: const EdgeInsets.all(18.0),
child: Center(
child: Row(children: [
CircularProgressIndicator(),
Padding(padding: EdgeInsets.all(10)),
Text('加载中...')
],
mainAxisAlignment: MainAxisAlignment.center,
),
),
), color: Colors.white70,);
}
}
Widget _listViewWidget (BuildContext context, List sourceList,ScrollController scrollController,bool isNeedLoad) {
if(sourceList.length==0) {
return null;
} else {
return ListView.builder(
itemCount: isNeedLoad? sourceList.length+1 : sourceList.length,
//设置physics属性总是可滚动
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context,index) {
if (index == sourceList.length) {
return LoadMoreView();
} else {
return ListTile(
title: Text(sourceList[index].title,style: TextStyle(fontSize: 22),),
subtitle: Text(sourceList[index].description,style: TextStyle(fontSize: 18)),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder:(context)=> DetailScreen(todo: sourceList[index]),
)
);
},
);
}
},
controller: scrollController,
);
}
}
List _myDataList(){
return List.generate(10,
(i) => new Todo(
'我是表第 $i 项',
'我是内容 $i',
)
);
}
int callTimes = 0;
class _ListViewRefreshState extends State {
List entityList = _myDataList();
ScrollController _scrollController = new ScrollController();
bool isLoadData = false;
@override
void initState() {
super.initState();
print('你好!');
_scrollController.addListener((){
if (_scrollController.position.maxScrollExtent == _scrollController.position.pixels) {
print("------------加载更多-------------");
_getMoreData();
}
});
}
// 下拉刷新
Future _handleRefresh() async {
print('-------开始刷新------------');
await Future.delayed(Duration(seconds: 2), () { //模拟延时
setState(() {
entityList.clear();
entityList = List.generate(10,
(i) => new Todo(
"刷新我是表第 $i 项",
'刷新我是内容 $i'
)
);
return null;
});
});
}
Future _getMoreData() async {
await Future.delayed(Duration(seconds: 2), () { //模拟延时操作
if (!isLoadData) {
isLoadData = true;
setState(() {
isLoadData = false;
List newList = List.generate(5,
(i) => Todo(
'分页--我是表第 ${i + entityList.length} 项',
'刷新我是内容 ${i + entityList.length} '
)
);
entityList.addAll(newList);
});
}
});
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: AppBar(
title: Text('刷新和分页加载'),
),
body: RefreshIndicator(
onRefresh: _handleRefresh,
child: _listViewWidget(
context,
entityList,
_scrollController,
isLoadData
),
),
);
}
}
整体效果图如下:
解决方法:增加physics: AlwaysScrollableScrollPhysics(),
即可
ListView.builder(
//设置physics属性总是可滚动
physics: AlwaysScrollableScrollPhysics(),
)
body:new NotificationListener(
onNotification: _onNotification,
child: RefreshIndicator(
onRefresh: _handleRefresh,
child: _listViewWidget(
context,
entityList,
_scrollController,
isLoadData
),
),
),
);
添加监听通知的方法
int callTimes = 0;
class _ListViewRefreshState extends State {
List entityList = _myDataList();
ScrollController _scrollController = new ScrollController();
bool isLoadData = false;
double offset = 0;
// 为了解决不满一屏无法上拉加载分页的问题。
bool _onNotification(ScrollNotification notification) {
if (notification is! ScrollNotification) {
// 如果不是滚动事件,直接返回
return false;
}
if (notification is OverscrollNotification) {
offset = notification.overscroll>offset ? notification.overscroll:offset;
}
print('hdhhd---$offset');
if (notification is ScrollEndNotification) {
// 要加上不满一屏幕的条件判断,直执行一次,待到列表数据内容超过屏幕高度,不再这里指执行了。走_scrollController.addListener来分页
if(offset>15&&callTimes==0) {
callTimes = callTimes + 1;
_getMoreData();
offset = 0;
}
}
return true;
}