theme: cyanosis
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 1 天,点击查看活动详情
0. 前言
在上传文件时,为了缓解等待的焦虑,一般希望显示上传的 进度
,来给用户任务进度的 反馈
。在上传图片时,经常见到给出一个透明遮罩,随着进度的增加,遮罩逐渐减少的进度表现形式。本文就来看一下这种表现的实现方式:
1. 实现思路
整体分为三层,底部的图片层、中间的透明遮罩层、上面的文字层。其中透明遮罩会根据进度,以中心为原点,顺时针扫描式地减少。这个效果可以通过 裁剪
完成,如下 35%
时,相当于把右上角裁掉,保留余下的阴影。所以关键点是: 计算余下阴影的路径
。
如下示意图,根据红色是图片矩形区域的路径;蓝色实线是外接圆上的弧线,弧度值根据进度确定。根据这两个路径进行 xor
的组合,就可以得到阴影路径:
如下,定义 CustomClipper<Path>
的派生类 ProgressClipper
, 在构造时传入进度值。实现 getClip
抽象方法返回 Path
路径对象。裁剪器会根据这个路径进行裁剪,该路径之外的部分会被裁掉。shouldReclip
方法和绘制中的的 shouldRepaint
异曲同工,在 ProgressClipper
对象变化时,控制是否触发 getClip
重新裁剪。
class ProgressClipper extends CustomClipper<Path> {
final double progress;
ProgressClipper({this.progress=0});
@override
Path getClip(Size size) {
if(progress==0){
return Path();
}
// 红色区域
Path zone = Path()..addRect(Rect.fromLTRB(0, 0, size.width, size.height));
// 蓝色弧线
double outRadius = sqrt(size.width/2*size.width/2 size.height/2*size.height/2);
Path path = Path()
..moveTo(size.width / 2, size.height / 2)
..relativeLineTo(0, -size.height / 2)
..arcTo(
Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: outRadius ,
height: outRadius),
-pi / 2,
2 * pi * progress,
false);
return Path.combine(PathOperation.xor, path, zone);
}
@override
bool shouldReclip(covariant ProgressClipper oldClipper) {
return progress != oldClipper.progress;
}
}
2. 裁剪器的使用
使用 ClipPath
组件,设置 clipper
参数,其类型为 CustomClipper<Path>
,可对 child
组件进行裁剪,如下是使用 ProgressClipper
裁剪器,进度 0.35
时的效果:
ClipPath(
clipper: ProgressClipper(progress: 0.35),
child: Container(
width: 150,
height: 150,
color: Colors.black.withOpacity(0.7),
),
),
然后通过 Stack
组件,将 Image
放在遮罩的下层,文字放在上层,效果如下:
Stack(
alignment: Alignment.center,
children: [
buildImage(),
if (value != 0) buildMask(0.35),
if (value != 0) buildText(0.35)
],
);
Widget buildImage()=> Image.asset(
'assets/bg_5.jpg',
width: 150,
height: 150,
fit: BoxFit.cover,
);
Widget buildMask(double value)=> ClipPath(
clipper: ProgressClipper(progress: value),
child: Container(
width: 150,
height: 150,
color: Colors.black.withOpacity(0.7),
),
);
Widget buildText(double value)=> Text(
"${(uploadProgress.value * 100).toInt()} %",
style: TextStyle(color: Color(0xffEDFBFF), fontSize: 24),
);
3. 进度的变化
然后只要更改进度值,即可完成需求,这里通过 Timer
定时器来模拟进度的变化,每 500 ms
增加 0.05
进度。代码如下所示:
void startTimer() {
if (_timer != null) {
_timer!.cancel();
_timer = null;
}
_timer = Timer.periodic(const Duration(milliseconds: 500), _updateProgress);
}
void _updateProgress(Timer timer){
uploadProgress.value = 0.05;
if (uploadProgress.value >= 1) {
uploadProgress.value = 0;
_timer?.cancel();
_timer = null;
}
}
另外,通过 ValueListenableBuilder
来监听 uploadProgress
进度变化。计时器每次触发回调时,增加 uploadProgress.value
值即可触发局部构建。这样即可得到如下效果:
ValueNotifier<double> uploadProgress = ValueNotifier<double>(0);
---->[build]----
ValueListenableBuilder(
valueListenable: uploadProgress,
builder: (_, double value, child) {
return
Stack(
alignment: Alignment.center,
children: [
buildImage(),
if (value != 0) buildMask(value),
if (value != 0) buildText(value)
],
);
})),
在实际上传时,可以使用 Dio
的 post
请求,通过 onSendProgress
可以监听到上传的进度,在其中更新进度值即可。
dio.post(
url,
data: formData,
onSendProgress: _sendProgressChange,
)
void _sendProgressChange(int count, int total) {
uploadProgress.value = count / total;
}
4. 裁剪方式的拓展
裁剪的表现本质上是路径,所以通过提供不同的路径可以实现不同的效果。如下是随进度增加,阴影区域圆形缩减的效果:
该效果通过下面的 CircleProgressClipper
裁剪器实现。逻辑非常简单,进度不断增大,半径逐渐减小,通过 outSide
乘以 1-progress
即可:
class CircleProgressClipper extends CustomClipper<Path> {
final double progress;
CircleProgressClipper({this.progress=0});
@override
Path getClip(Size size) {
if(progress==0){
return Path();
}
double outSide = sqrt(size.width*size.width size.height*size.height);
Rect rect = Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: outSide*(1-progress) ,
height: outSide*(1-progress) );
Path path = Path()..addOval(rect);
return path;
}
@override
bool shouldReclip(covariant CircleProgressClipper oldClipper) {
return progress != oldClipper.progress;
}
}
还可以让遮罩以矩形的方式逐渐缩减,如下图所示:
在创建矩形区域时,左下角的纵坐标值取 size.height*(1-progress)
即可。另外,阴影从 左到右
、右到左
、上到下
的变化都是类似的,有相关需求的话自己改改即可,当然也可以通过一个枚举类作为参数来控制表现效果。
class RectProgressClipper extends CustomClipper<Path> {
final double progress;
RectProgressClipper({this.progress=0});
@override
Path getClip(Size size) {
if(progress==0){
return Path();
}
Rect rect = Rect.fromPoints(
Offset.zero,
Offset(size.width,size.height*(1-progress)),
);
Path path = Path()..addRect(rect);
return path;
}
@override
bool shouldReclip(covariant RectProgressClipper oldClipper) {
return progress != oldClipper.progress;
}
}
本文主要通过图片上传的进度表现,介绍了 CustomClipper
裁剪器的派生和使用,希望可以为你的图片上传有所帮助。那本文就到这,谢谢观看 ~
@张风捷特烈 2022.09.30 未允禁转