星的天空的博客

种一颗树,最好的时间是十年前,其次是现在。

0%

Flutter定制上下拉刷新功能

技术选型

项目中需要定制一个基于Gif图片的下拉刷新功能,调研发现
Flutter支持上下拉刷新的框架很多,其中有两个比较有名的有flutter_easyrefreshpull_to_refresh,两个框架功能都很强大,都能满足需求,其中flutter_easyrefresh在github的star更多,lib包大小为644KB,pull_to_refresh在pub.dev的评分更高,lib包大小为172KB,综合考虑后,选择基于pull_to_refresh来实现框架功能。

下拉刷新GIF图片的生成

下拉刷新需要控制Gif图片的播放,所以需要引入组件flutter_gifimage,gifimage支持加载本地和网络的gif图片,但是不支持加载图片列表的方式来执行gif动画,所以我们需要将图片列表生成为gif图片(UI小姐姐只给了图片列表)。
网上有很多网站可以生成gif,但是都有图片数量限制。 下载一个生成gif的软件来生成又显得很麻烦,我们选择使用Python的Pillow库来创建gif图片。Pillow是PIL的python3版本,功能强大,可以很好的完成需求。创建脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from PIL import Image, ImageDraw

def gen_frame(path):
png = Image.open(path).convert('RGBA')
background = Image.new('RGBA', png.size, (255,255,255,0))
alpha_composite = Image.alpha_composite(background, png)
return alpha_composite

image_list = []
im0 = gen_frame('refresh_images/Loading_00@2x.png')
for i in range(1,57):
path = 'refresh_images/Loading_0' + str(i) + "@2x.png"
image_list.append(gen_frame(path))

# 生成透明图片需要加两个个参数:transparency=0, disposal=2
im0.save('GIF.gif', save_all=True, append_images=image_list, loop=0, duration=34, transparency=0, disposal=2)

上述脚本对Gif背景进行了处理,以生成一张透明背景的gif图片

Gif下拉刷新组件头部的实现

pull_to_refresh中提供了抽象类RefreshIndicator与RefreshIndicator,与material提供的重名,所以需要隐藏。 import部分代码如下:

1
2
3
import 'package:flutter/material.dart'
hide RefreshIndicator, RefreshIndicatorState;
import 'package:pull_to_refresh/pull_to_refresh.dart';

最终下拉刷新的Header实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import 'package:flutter/material.dart'
hide RefreshIndicator, RefreshIndicatorState;
import 'package:flutter_gifimage/flutter_gifimage.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

class GifHeader extends RefreshIndicator {
GifHeader() : super(height: 72.0, refreshStyle: RefreshStyle.Follow);
@override
_GifHeaderState createState() => _GifHeaderState();
}

class _GifHeaderState extends RefreshIndicatorState<GifHeader>
with SingleTickerProviderStateMixin {
GifController _gifController;

@override
void initState() {
//value可以理解成Gif图片里面的第几帧
_gifController = GifController(
vsync: this,
value: 0,
);
super.initState();
}

@override
void onModeChange(RefreshStatus mode) {
if (mode == RefreshStatus.refreshing) {
//min和max都可以理解成Gif图片里面的第几帧,这里表示低0帧到第44帧
_gifController.repeat(
min: 0, max: 44, period: Duration(milliseconds: 2000));
}
super.onModeChange(mode);
}

@override
Future<void> endRefresh() {
return _gifController.animateTo(44, duration: Duration(milliseconds: 500));
}

@override
void resetValue() {
// reset not ok , the plugin need to update lowwer
_gifController.value = 0;
super.resetValue();
}

@override
Widget buildContent(BuildContext context, RefreshStatus mode) {
return GifImage(
image: AssetImage("images/pull_refresh.gif"),
controller: _gifController,
height: 72.0,
);
}

@override
void dispose() {
_gifController.dispose();
super.dispose();
}
}

设置Gif的value(帧)时,不能超过Gif的最大帧数,不然超出的帧数是显示一个有颜色的空白页面

集成进入项目

pull_to_refresh提供了全局的统一配置类RefreshConfiguration,用它来包裹MaterialApp则可以全局生效,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Widget build(BuildContext context) {
return RefreshConfiguration(
headerBuilder: () => GifHeader(), // 配置默认头部指示器,假如你每个页面的头部指示器都一样的话,你需要设置这个
footerBuilder: () => ClassicFooter(), // 配置默认底部指示器
headerTriggerDistance: 72.0, // 头部触发刷新的越界距离
// springDescription:SpringDescription(stiffness: 170, damping: 16, mass: 1.9), // 自定义回弹动画,三个属性值意义请查询flutter api
maxOverScrollExtent: 100, //头部最大可以拖动的范围,如果发生冲出视图范围区域,请设置这个属性
maxUnderScrollExtent: 0, // 底部最大可以拖动的范围
enableScrollWhenRefreshCompleted:
true, //这个属性不兼容PageView和TabBarView,如果你特别需要TabBarView左右滑动,你需要把它设置为true
enableLoadingWhenFailed: true, //在加载失败的状态下,用户仍然可以通过手势上拉来触发加载更多
hideFooterWhenNotFull: false, // Viewport不满一屏时,禁用上拉加载更多功能
// 当列表无法充满全屏的时候,加载更多跟在列表后面
shouldFooterFollowWhenNotFull: (status) => true,
enableBallisticLoad: true, // 可以通过惯性滑动触发加载更多
child: MaterialApp(
title: 'Flutter Demo',
home: HomePage(),
),
);
}

全局配置好后,则可以在列表进行集成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:my_flutter/message/notice_list_page/notice_ist_cell.dart';
import 'package:my_flutter/message/notice_list_page/notice_list_model.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';

class NoticeListPage extends StatefulWidget {
@override
_NoticeListPageState createState() => _NoticeListPageState();
}

class _NoticeListPageState extends State<NoticeListPage> {
RefreshController _refreshController =
RefreshController(initialRefresh: true);
List<NoticeListModel> list = [];

void _onRefresh() async {
// monitor network fetch
await Future.delayed(Duration(milliseconds: 2000));
// 这里可以添加逻辑判断,如果无更多数据:_refreshController.loadNoData();
// 如果加载失败: 设置_refreshController.refreshFailed()
_refreshController.refreshCompleted();

setState(() {
list = _getList();
});
}

void _onLoading() async {
// monitor network fetch
await Future.delayed(Duration(milliseconds: 1000));
// if failed,use loadFailed(),if no data return,use LoadNodata()
_refreshController.loadComplete();
if (mounted)
setState(() {
list.addAll(_getList());
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: SmartRefresher(
enablePullDown: true,
enablePullUp: list.length > 0,
controller: _refreshController,
onRefresh: _onRefresh,
onLoading: _onLoading,
child: ListView.builder(
itemBuilder: (c, i) => NoticeListCell(model: list[i]),
itemExtent: 100.0,
itemCount: list.length,
),
),
);
}

List<NoticeListModel> _getList() {
return [NoticeListModel(),NoticeListModel()];
}
}