【Flutter 绘制探索】进度与裁剪 - CustomClipper 的使用


theme: cyanosis

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 1 天,点击查看活动详情


0. 前言

在上传文件时,为了缓解等待的焦虑,一般希望显示上传的 进度,来给用户任务进度的 反馈。在上传图片时,经常见到给出一个透明遮罩,随着进度的增加,遮罩逐渐减少的进度表现形式。本文就来看一下这种表现的实现方式:


1. 实现思路

整体分为三层,底部的图片层、中间的透明遮罩层、上面的文字层。其中透明遮罩会根据进度,以中心为原点,顺时针扫描式地减少。这个效果可以通过 裁剪 完成,如下 35% 时,相当于把右上角裁掉,保留余下的阴影。所以关键点是: 计算余下阴影的路径

如下示意图,根据红色是图片矩形区域的路径;蓝色实线是外接圆上的弧线,弧度值根据进度确定。根据这两个路径进行 xor 的组合,就可以得到阴影路径:

image.png

如下,定义 CustomClipper<Path> 的派生类 ProgressClipper , 在构造时传入进度值。实现 getClip 抽象方法返回 Path 路径对象。裁剪器会根据这个路径进行裁剪,该路径之外的部分会被裁掉。shouldReclip 方法和绘制中的的 shouldRepaint 异曲同工,在 ProgressClipper 对象变化时,控制是否触发 getClip 重新裁剪。

```dart class ProgressClipper extends CustomClipper { 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/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) ], ); })),

```


在实际上传时,可以使用 Diopost 请求,通过 onSendProgress 可以监听到上传的进度,在其中更新进度值即可。

```dart dio.post( url, data: formData, onSendProgress: _sendProgressChange, )

void _sendProgressChange(int count, int total) { uploadProgress.value = count / total; } ```


4. 裁剪方式的拓展

裁剪的表现本质上是路径,所以通过提供不同的路径可以实现不同的效果。如下是随进度增加,阴影区域圆形缩减的效果:

18.gif

该效果通过下面的 CircleProgressClipper 裁剪器实现。逻辑非常简单,进度不断增大,半径逐渐减小,通过 outSide 乘以 1-progress 即可:

```dart class CircleProgressClipper extends CustomClipper { final double progress;

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; } } ```


还可以让遮罩以矩形的方式逐渐缩减,如下图所示:

19.gif

在创建矩形区域时,左下角的纵坐标值取 size.height*(1-progress) 即可。另外,阴影从 左到右右到左上到下 的变化都是类似的,有相关需求的话自己改改即可,当然也可以通过一个枚举类作为参数来控制表现效果。

```dart class RectProgressClipper extends CustomClipper { 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 未允禁转
  • 我的 公众号: 编程之王
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值