theme: cyanosis
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 1 天,点击查看活动详情
0. 前言
在上传文件时,为了缓解等待的焦虑,一般希望显示上传的 进度
,来给用户任务进度的 反馈
。在上传图片时,经常见到给出一个透明遮罩,随着进度的增加,遮罩逐渐减少的进度表现形式。本文就来看一下这种表现的实现方式:
1. 实现思路
整体分为三层,底部的图片层、中间的透明遮罩层、上面的文字层。其中透明遮罩会根据进度,以中心为原点,顺时针扫描式地减少。这个效果可以通过 裁剪
完成,如下 35%
时,相当于把右上角裁掉,保留余下的阴影。所以关键点是: 计算余下阴影的路径
。
如下示意图,根据红色是图片矩形区域的路径;蓝色实线是外接圆上的弧线,弧度值根据进度确定。根据这两个路径进行 xor
的组合,就可以得到阴影路径:
如下,定义 CustomClipper<Path>
的派生类 ProgressClipper
, 在构造时传入进度值。实现 getClip
抽象方法返回 Path
路径对象。裁剪器会根据这个路径进行裁剪,该路径之外的部分会被裁掉。shouldReclip
方法和绘制中的的 shouldRepaint
异曲同工,在 ProgressClipper
对象变化时,控制是否触发 getClip
重新裁剪。
```dart class ProgressClipper extends CustomClipper
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/2size.width/2 + size.height/2size.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
时的效果:
dart ClipPath( clipper: ProgressClipper(progress: 0.35), child: Container( width: 150, height: 150, color: Colors.black.withOpacity(0.7), ), ),
然后通过 Stack
组件,将 Image
放在遮罩的下层,文字放在上层,效果如下:
```dart 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
进度。代码如下所示:
```dart 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
值即可触发局部构建。这样即可得到如下效果:
```dart ValueNotifier uploadProgress = ValueNotifier (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
可以监听到上传的进度,在其中更新进度值即可。
```dart dio.post( url, data: formData, onSendProgress: _sendProgressChange, )
void _sendProgressChange(int count, int total) { uploadProgress.value = count / total; } ```
4. 裁剪方式的拓展
裁剪的表现本质上是路径,所以通过提供不同的路径可以实现不同的效果。如下是随进度增加,阴影区域圆形缩减的效果:
该效果通过下面的 CircleProgressClipper
裁剪器实现。逻辑非常简单,进度不断增大,半径逐渐减小,通过 outSide
乘以 1-progress
即可:
```dart class CircleProgressClipper extends CustomClipper
CircleProgressClipper({this.progress=0});
@override Path getClip(Size size) { if(progress==0){ return Path(); } double outSide = sqrt(size.widthsize.width+size.heightsize.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)
即可。另外,阴影从 左到右
、右到左
、上到下
的变化都是类似的,有相关需求的话自己改改即可,当然也可以通过一个枚举类作为参数来控制表现效果。
```dart class RectProgressClipper extends CustomClipper
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 未允禁转
我的 公众号: 编程之王