Flutter 三方下拉刷新框架
1. flutter_easyrefresh下拉框架
能够自定义酷炫的Header和Footer,也就是上拉和下拉的效果。
更新及时,不断在完善,录课截至时已经是v1.2.7版本了。
有一个辅导群,虽然文档不太完善,但是有辅导群和详细的案例。
回掉方法简单,这个具体可以看下面的例子。
2.使用方式
1.在 pubspec.yaml 中添加依赖
//pub方式
dependencies:
flutter_easyrefresh: version
//导入方式
dependencies:
flutter_easyrefresh:
path: 项目路径
//git方式
dependencies:
flutter_easyrefresh:
git:
url: git://github.com/xuelongqy/flutter_easyrefresh.git
2.在布局文件中添加 EasyreFresh
import 'package:flutter_easyrefresh/easy_refresh.dart';
....
// 方式一
EasyRefresh(
child: ScrollView(),
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
// 方式二
EasyRefresh.custom(
slivers: <Widget>[],
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
// 方式三
EasyRefresh.builder(
builder: (context, physics, header, footer) {
return CustomScrollView(
physics: physics,
slivers: <Widget>[
...
header,
...
footer,
],
);
}
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
3.触发刷新和加载动作
EasyRefreshController _controller = EasyRefreshController();
....
EasyRefresh(
controller: _controller,
....
);
....
_controller.callRefresh();
_controller.callLoad();
4.控制加载和刷新完成
EasyRefreshController _controller = EasyRefreshController();
....
EasyRefresh(
enableControlFinishRefresh: true,
enableControlFinishLoad: true,
....
);
....
_controller.finishRefresh(success: true);
_controller.finishLoad(success: true, noMore: false);
5使用指定的 Header 和 Footer
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_easyrefresh/material_header.dart';
import 'package:flutter_easyrefresh/material_footer.dart';
....
new EasyRefresh(
header: MaterialHeader(),
footer: MaterialFooter(),
child: ScrollView(),
....
)
6.添加国际化支持
不提供自带国际化支持,请自行设置ClassicalHeader和ClassicalFooter中需要展示的文字。
3细致讲解属性用法
1.ClassicalHeader
属性名称 | 属性描述 | 参数类型 | 默认值 | 要求 |
---|---|---|---|---|
extent | Header的高度 | double | 60.0 | 可选 |
triggerDistance | 触发刷新的距离 | double | 70.0 | 可选 |
float | 是否浮动 | bool | false | 可选 |
completeDuration | 完成延时 | Duration | null | 可选 |
enableInfiniteRefresh | 是否开启无限刷新 | bool | false | 可选 |
enableHapticFeedback | 是否开启震动反馈 | bool | false | 可选 |
headerBuilder | Header构造器 | RefreshControlBuilder | null | 必需 |
/// Key
final Key? key;
/// 方位
final AlignmentGeometry? alignment;
/// 提示刷新文字
final String? refreshText;
/// 准备刷新文字
final String? refreshReadyText;
/// 正在刷新文字
final String? refreshingText;
/// 刷新完成文字
final String? refreshedText;
/// 刷新失败文字
final String? refreshFailedText;
/// 没有更多文字
final String? noMoreText;
/// 显示额外信息(默认为时间)
final bool showInfo;
/// 更多信息
final String? infoText;
/// 背景颜色
final Color bgColor;
/// 字体颜色
final Color textColor;
/// 更多信息文字颜色
final Color infoColor;
2. ClassicalFooter
属性名称 | 属性描述 | 参数类型 | 默认值 | 要求 |
---|---|---|---|---|
extent | Footer的高度 | double | 60.0 | 可选 |
triggerDistance | 触发加载的距离 | double | 70.0 | 可选 |
completeDuration | 完成延时 | Duration | null | 可选 |
enableInfiniteLoad | 是否开启无限加载 | bool | false | 可选 |
enableHapticFeedback | 是否开启震动反馈 | bool | false | 可选 |
footerBuilder | Footer构造器 | LoadControlBuilder | null | 必需 |
3.EasyRefreshController
要求 | ||||
---|---|---|---|---|
callRefresh | 触发刷新 | void Function({Duration duration}) | Duration duration = const Duration(milliseconds: 300) | 可选 |
callLoad | 触发加载 | void Function({Duration duration}) | Duration duration = const Duration(milliseconds: 300) | 可选 |
finishRefresh | 完成刷新 | void Function({{bool success,bool noMore,}}) | success = true, noMore = false | 可选 |
finishLoad | 完成加载 | void Function({Duration duration}) | success = true, noMore = false | 可选 |
resetRefreshState | 重置刷新状态 | void Function() | void | 可选 |
resetLoadState | 重置加载状态 | void Function() | void | 可选 |
4.EasyRefresh
属性名称 | 属性描述 | 参数类型 | 默认值 | 要求 |
---|---|---|---|---|
key | EasyRefresh的键 | GlobalKey | null | 可选 |
controller | EasyRefresh控制器 | EasyRefreshController | null | 可选 |
onRefresh | 刷新回调(为null时关闭刷新) | Future Function() | null | 可选 |
onLoad | 加载回调(为null时关闭加载) | Future Function() | null | 可选 |
enableControlFinishRefresh | 是否开启控制结束刷新 | bool | false | 可选 |
enableControlFinishLoad | 是否开启控制结束加载 | bool | false | 可选 |
taskIndependence | 任务独立(刷新和加载状态独立) | bool | false | 可选 |
defaultHeader | 全局默认Header样式 | static Header | ClassicalHeader | 可选 |
defaultFooter | 全局默认Footer样式 | static Footer | ClassicalFooter | 可选 |
header | Header样式 | Header | _defaultHeader | 可选 |
footer | Footer样式 | Footer | _defaultFooter | 可选 |
builder | 子组件构造器 | EasyRefreshChildBuilder | null | EasyRefresh.builder必需 |
child | 子组件 | Widget | null | EasyRefresh必需 |
slivers | Slivers集合 | List | null | EasyRefresh.custom必需 |
firstRefresh | 首次刷新 | bool | false | 可选 |
firstRefreshWidget | 首次刷新组件(为null时使用Header) | Widget | null | 可选 |
emptyWidget | 空视图(当不为null时,只会显示空视图) | emptyWidget | null | 可选 |
topBouncing | 顶部回弹(onRefresh为null时生效) | bool | true | 可选 |
bottomBouncing | 底部回弹(onLoad为null时生效) | bool | true | 可选 |
scrollController | 滚动控制器 | ScrollController | null | 可选 |
behavior | 滚动行为 | Behavior | EmptyOverScrollScrollBehavior | 可选 |
其他参数 | 与CustomScrollView一致 | 与CustomScrollView参数一致 | null | 可选(EasyRefresh.custom) |
—————— enableControlFinishRefresh
——————enableControlFinishLoad
——————controller EasyRefreshController类型
——————scrollController 滚动控制器 ScrollController null 可选
——————header
——————footer
——————onRefresh
——————onLoad
——————taskIndependence一般不独立,刷新的时候,不能加载
4.使用
2.方式2
// 方式一
EasyRefresh(
child: ScrollView(),
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
EasyRefresh.custom(
///EasyRefresh.custom 构造器 内部继承CustomScrollow 必须配合slivers 使用
controller: _controller,
emptyWidget: _count == 0 ? _emptyViewWidget() : null,
header: BallPulseHeader(),
footer: BallPulseFooter(),
onRefresh: () async {
await Future.delayed(Duration(seconds: 2), () { //模拟网络请求
setState(() {
_count = 10;
onRefreshData();
});
_controller.resetLoadState();
});
},
onLoad: () async {
await Future.delayed(Duration(seconds: 2), () {//模拟网络请求
setState(() {
_count += 10;
onLoadData();
});
_controller.finishLoad(noMore: _count >= 20);
});
},
slivers: <Widget>[
SliverList(delegate: SliverChildBuilderDelegate(
(context, index) {
return listItemBuilder(context, index);
},
childCount: listDeat.length,
),
),
],
),
3方式3
// 方式三
EasyRefresh.builder(
builder: (context, physics, header, footer) {
return CustomScrollView(
physics: physics,
slivers: <Widget>[
...
header,
...
footer,
],
);
}
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
import 'dart:async';
import 'package:example/widget/sample_list_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart';
/// Swiper示例
class SwiperPage extends StatefulWidget {
@override
SwiperPageState createState() {
return SwiperPageState();
}
}
class SwiperPageState extends State<SwiperPage> {
// 条目总数
int _count = 20;
@override
Widget build(BuildContext context) {
return Scaffold(
body: EasyRefresh.builder(
builder: (context, physics, header, footer) {
return CustomScrollView(
physics: physics,
slivers: <Widget>[
SliverAppBar(
expandedHeight: 100.0,
pinned: true,
backgroundColor: Colors.white,
flexibleSpace: FlexibleSpaceBar(
centerTitle: false,
title: Text('Swiper'),
),
),
header!,
SliverList(
delegate: SliverChildListDelegate([
Container(
height: 210.0,
child: ScrollNotificationInterceptor(
child: Swiper(
itemBuilder: (BuildContext context, int index) {
return SampleListItem(direction: Axis.horizontal);
},
itemCount: 5,
viewportFraction: 0.8,
scale: 0.9,
autoplay: true,
),
),
),
]),
),
SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return SampleListItem();
}, childCount: _count),
),
footer!,
],
);
},
onRefresh: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_count = 20;
});
}
});
},
onLoad: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_count += 20;
});
}
});
},
),
);
}
}
5.其他类型
1.首次刷新
firstRefresh | 首次刷新 | bool | false | 可选 |
---|---|---|---|---|
firstRefreshWidget | 首次刷新组件(为null时使用Header) | Widget | null | 可选 |
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'SampleListItem.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// home: CustomScrollViewDemo(),
home: Scaffold(
body: FirstRefreshPage(),
),
);
}
}
/// 首次刷新示例
class FirstRefreshPage extends StatefulWidget {
@override
FirstRefreshPageState createState() {
return FirstRefreshPageState();
}
}
class FirstRefreshPageState extends State<FirstRefreshPage> {
// 总数
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第一次刷新"),
backgroundColor: Colors.white,
),
body: EasyRefresh.custom(
firstRefresh: true,
firstRefreshWidget: Container(
width: double.infinity,
height: double.infinity,
child: Center(
child: SizedBox(
height: 200.0,
width: 300.0,
child: Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
width: 50.0,
height: 50.0,
child: SpinKitFadingCube(
color: Theme.of(context).primaryColor,
size: 25.0,
),
),
Container(
child: Text("加载中"),
)
],
),
),
)),
),
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return SampleListItem();
},
childCount: _count,
),
),
],
onRefresh: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_count = 20;
});
}
});
},
onLoad: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_count += 20;
});
}
});
},
),
);
}
}
2.空视图
emptyWidget: _count == 0
? Container(
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: SizedBox(),
flex: 2,
),
SizedBox(
width: 100.0,
height: 100.0,
child: Image.asset('assets/image/nodata.png'),
),
Text(
S.of(context).noData,
style: TextStyle(fontSize: 16.0, color: Colors.grey[400]),
),
Expanded(
child: SizedBox(),
flex: 3,
),
],
),
)
: null,
3二楼
主要是更具高度计算,滑动屏幕
4.聊天页面 上拉加载,下拉刷新,模拟聊天
原理:首先根据LayoutBuilder确定显示的空间,然后计算每条消息的高度,如果总和消息超过最大高度,则使用ListView,如果没有超过,使用SliverToBoxAdapter循环遍历消息
reverse: true,
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_easyrefresh/easy_refresh.dart';
import 'package:flutter_first/day36easyRefresh/FirstRefreshPageState.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
/// 聊天界面示例
class ChatPage extends StatefulWidget {
@override
ChatPageState createState() {
return ChatPageState();
}
}
class ChatPageState extends State<ChatPage> {
// 信息列表
List<MessageEntity> _msgList;
// 输入框
TextEditingController _textEditingController;
// 滚动控制器
ScrollController _scrollController;
@override
void initState() {
super.initState();
_msgList = [
MessageEntity(true, "It's good!"),
MessageEntity(false, 'EasyRefresh'),
];
_textEditingController = TextEditingController();
_textEditingController.addListener(() {
setState(() {});
});
_scrollController = ScrollController();
}
@override
void dispose() {
super.dispose();
_textEditingController.dispose();
_scrollController.dispose();
}
// 发送消息
void _sendMsg(String msg) {
setState(() {
_msgList.insert(0, MessageEntity(true, msg));
});
_scrollController.animateTo(0.0,
duration: Duration(milliseconds: 300), curve: Curves.linear);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('KnoYo'),
centerTitle: false,
backgroundColor: Colors.grey[200],
elevation: 0.0,
actions: <Widget>[
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) {
return FirstRefreshPage();
},
),
);
},
),
],
),
backgroundColor: Colors.grey[200],
body: Column(
children: <Widget>[
Divider(
height: 0.5,
),
Expanded(
flex: 1,
child: LayoutBuilder(
builder: (context, constraints) {
// 判断列表内容是否大于展示区域
bool overflow = false;
double heightTmp = 0.0;
for (MessageEntity entity in _msgList) {
heightTmp +=
_calculateMsgHeight(context, constraints, entity);
if (heightTmp > constraints.maxHeight) {
overflow = true;
}
}
return EasyRefresh.custom(
scrollController: _scrollController,
reverse: true,
footer: CustomFooter(
enableInfiniteLoad: false,
extent: 40.0,
triggerDistance: 50.0,
footerBuilder: (context,
loadState,
pulledExtent,
loadTriggerPullDistance,
loadIndicatorExtent,
axisDirection,
float,
completeDuration,
enableInfiniteLoad,
success,
noMore) {
return Stack(
children: <Widget>[
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: Container(
width: 30.0,
height: 30.0,
child: SpinKitCircle(
color: Colors.green,
size: 30.0,
),
),
),
],
);
}),
slivers: <Widget>[
if (overflow)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return _buildMsg(_msgList[index]);
},
childCount: _msgList.length,
),
),
if (!overflow)
SliverToBoxAdapter(
child: Container(
height: constraints.maxHeight,
width: double.infinity,
child: Column(
children: <Widget>[
for (MessageEntity entity in _msgList.reversed)
_buildMsg(entity),
],
),
),
),
],
onLoad: () async {
await Future.delayed(Duration(seconds: 2), () {
if (mounted) {
setState(() {
_msgList.addAll([
MessageEntity(true, "It's good!"),
MessageEntity(false, 'EasyRefresh'),
]);
});
}
});
},
);
},
),
),
SafeArea(
child: Container(
color: Colors.grey[100],
padding: EdgeInsets.only(
left: 15.0,
right: 15.0,
top: 10.0,
bottom: 10.0,
),
child: Row(
children: <Widget>[
Expanded(
flex: 1,
child: Container(
padding: EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 10.0,
bottom: 10.0,
),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
child: TextField(
controller: _textEditingController,
decoration: null,
onSubmitted: (value) {
if (_textEditingController.text.isNotEmpty) {
_sendMsg(_textEditingController.text);
_textEditingController.text = '';
}
},
),
),
),
InkWell(
onTap: () {
if (_textEditingController.text.isNotEmpty) {
_sendMsg(_textEditingController.text);
_textEditingController.text = '';
}
},
child: Container(
height: 30.0,
width: 60.0,
alignment: Alignment.center,
margin: EdgeInsets.only(
left: 15.0,
),
decoration: BoxDecoration(
color: _textEditingController.text.isEmpty
? Colors.grey
: Colors.green,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
child: Text(
'发送',
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
),
),
],
),
),
),
],
),
);
}
// 构建消息视图
Widget _buildMsg(MessageEntity entity) {
if (entity.own) {
return Container(
margin: EdgeInsets.all(
10.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
'我',
style: TextStyle(
color: Colors.grey,
fontSize: 13.0,
),
),
Container(
margin: EdgeInsets.only(
top: 5.0,
),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.lightGreen,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
constraints: BoxConstraints(
maxWidth: 200.0,
),
child: Text(
entity.msg,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: 16.0,
),
),
)
],
),
Card(
margin: EdgeInsets.only(
left: 10.0,
),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
elevation: 0.0,
child: Container(
height: 40.0,
width: 40.0,
child: Image.asset('assets/image/head.jpg'),
),
),
],
),
);
} else {
return Container(
margin: EdgeInsets.all(
10.0,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Card(
margin: EdgeInsets.only(
right: 10.0,
),
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
elevation: 0.0,
child: Container(
height: 40.0,
width: 40.0,
child: Image.asset('assets/image/head_knoyo.jpg'),
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'KnoYo',
style: TextStyle(
color: Colors.grey,
fontSize: 13.0,
),
),
Container(
margin: EdgeInsets.only(
top: 5.0,
),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(
4.0,
)),
),
constraints: BoxConstraints(
maxWidth: 200.0,
),
child: Text(
entity.msg,
overflow: TextOverflow.clip,
style: TextStyle(
fontSize: 16.0,
),
),
)
],
),
],
),
);
}
}
// 计算内容的高度
double _calculateMsgHeight(
BuildContext context, BoxConstraints constraints, MessageEntity entity) {
return 45.0 +
_calculateTextHeight(
context,
constraints,
text: '我',
textStyle: TextStyle(
fontSize: 13.0,
),
) +
_calculateTextHeight(
context,
constraints.copyWith(
maxWidth: 200.0,
),
text: entity.msg,
textStyle: TextStyle(
fontSize: 16.0,
),
);
}
/// 计算Text的高度
double _calculateTextHeight(
BuildContext context,
BoxConstraints constraints, {
String text = '',
@required
TextStyle textStyle,
List<InlineSpan> children = const [],
}) {
final span = TextSpan(text: text, style: textStyle, children: children);
final richTextWidget = Text.rich(span).build(context) as RichText;
final renderObject = richTextWidget.createRenderObject(context);
renderObject.layout(constraints);
return renderObject.computeMinIntrinsicHeight(constraints.maxWidth);
}
}
/// 信息实体
class MessageEntity {
bool own;
String msg;
MessageEntity(this.own, this.msg);
}
6样式
————————可以自行通过自定义实现,
1.经典样式
2.Material样式
3.球脉冲样式BallPulse
header: BezierCircleHeader(),
footer: BezierBounceFooter(),
4.贝塞尔圆圈样式
header: BezierHourGlassHeader(
color: Theme.of(context).scaffoldBackgroundColor,
),
footer: BezierBounceFooter(
color: Theme.of(context).scaffoldBackgroundColor,
),
5.快递
header: DeliveryHeader(
backgroundColor: Colors.grey[100],
),