手撸一个Anything Cut Widget
背景:为啥要开发这个组件呢?因为目前在我们的flutter app中,用到了视频合成技术,这里就涉及到视频或者图片素材的裁剪,目前市面上普遍的组件都是基于图片的,并且基本上都是使用canvas进行渲染和裁剪,不太符合我们的业务需求,所以要自己开发一个裁剪组件。
优势
支持裁剪任何东西,对,是任何东西!代码地址见下方
效果展示
支持拖拽,缩放,以及裁剪框大小设置等等。效果只展示了1:1裁剪框。
需求分析
在移动端,基本都是用手势操作,所以在需求设计之初,就考虑到手势的习惯,以及参考大部分编辑工具,定义出了以下几个需求点:
- 裁剪框固定在屏幕上的一个位置,通过单指拖动,双指缩放的形式调整素材位置和大小,来框定裁剪范围
- 素材的最小边不能小于裁剪框上与其对应的边,即裁剪框只能相对在素材范围内移动
- 支持素材的类型包含图片和视频
方案设计
考虑到需要支持视频和图片类型,所以不方便直接使用canvas进行素材的渲染。
这里计划采用canvas来绘制裁剪框和遮罩层,待裁剪的素材作为组件放入裁剪区域,并进行适配。
裁剪结果只需要给出裁剪区域(即告诉业务方改裁剪哪块区域),具体裁剪由业务方完成,实现解耦,组件不必理解素材类型。
回显的时候需要传入裁剪区域进行裁剪框回显。
实现逻辑
0、基础组件
这里使用 GestureDetector
组件以及 Transform
组件进行手势拖拽和缩放操作,使用 canvas
绘制裁剪框。
ps: 使用
OverflowBox
包裹子组件,不然子组件的尺寸会受到父组件的约束,造成渲染变形
ps2: 使用ClipRect
组件包裹在整个组件外面,不然Transform
会导致移动时超过父组件边界
1、计算裁剪组件区域
这一步主要是计算出裁剪组件占当前视图的大小,以此来框定裁剪框的最大宽高 maxCropSize
。此处需要获取父元素大小,具体逻辑见代码 build
里面。
这里的最大宽高也可以通过参数
maxCropSize
来指定。
2、计算并绘制裁剪框位置
代码见
caculateCropBoxSize()
首先我们需要计算出中心坐标点 _originPos
(组件中心点),以此作为绘制中心点和后续变换中心点。
再根据裁剪框可绘制的最大宽高 maxCropSize
以及裁剪框比例 _cropRatio
计算出裁剪框实际宽高 _cropBoxRealSize
,这里需要考虑到素材宽高比和裁剪框宽高比。
最后根据 _cropBoxRealRect
以及组件宽高来计算出裁剪框绘制位置,位置为相对组件居中。
3、计算素材初始尺寸
代码见
caculateInitClipSize()
我们需要根据传入的素材尺寸 clipSize
计算出素材在组件展示的初始尺寸 _resizeClipSize
,这个尺寸是相对裁剪框的,会做为计算缩放的初始尺寸(_scale = 1.0
),根据裁剪框的宽高比和素材的宽高比,来计算横向还是纵向拉满,拉满方式参考 Boxfit.cover
。
4、计算素材缩放大小和摆放位置
代码见
caculateInitClipPosition()
首先需要判断是否有传入初始裁剪区域 cropRect
,内部使用 resultRect
承载,即 resultRect = widget.cropRect
。
如果没有传入初始裁剪区域,那么默认缩放尺寸 _scale
是 1.0
,并且默认居中裁剪,此时只需要计算出居中的偏移量 _deltaPoint
就行。
如果传入了初始裁剪区域,那么首先需要根据 裁剪框宽高比 和 素材宽高比,来确定 _scale
的数值,如果 裁剪框宽高比 大于 素材宽高比,那么 _scale
相当于将宽边放大到1所需要的倍数,_scale = 1 / resultRect.width
,反之 _scale = 1 / resultRect.height
。
然后再根据中心点、裁剪框 Rect
以及 _scale
计算出初始偏移量,设定摆放位置 _deltaPoint
。
5、移动手势操作
使用 GestureDetector
的 onScale
相关事件来进行缩放和移动判断,并且将值传回给 Transform
组件,在视图上体现出来。
6、计算边界值,并且进行修正
因为在移动过程中,我们不能超出素材范围,所以需要进行边界触碰计算和修正,使范围限定在Rect.fromLTRB(0, 0, 1, 1)
范围内。
移动时需要先计算当前位置,然后对比边界值,如果超出了,则移回区域内,并且重新计算位置,再传给 resultRect
值。
7、传出结果
加入回调函数如下
cropRectUpdateStart
在裁剪区域开始变化时触发cropRectUpdate
在初始化以及裁剪区域变化时触发,传出参数为Rect
类型,表示当前裁剪区域cropRectUpdateEnd
在停止变化时触发,传出参数为Rect
类型,表示当前裁剪区域
代码地址
参数设计
参数名 | 类型 | 描述 | 默认值 |
---|---|---|---|
cropRect | Rect | 初始裁剪区域,如果不填,默认会填充并居中,表现形式类似cover | - |
clipSize | Size | 待裁剪素材的尺寸 | 必填 |
cropRatio | Size | 裁剪框比例,默认16:9 |
Size(16, 9) |
child | Widget | 待裁剪素材 | 必填 |
maxCropSize | Size | 裁剪框当前比例下最大宽高,主要是用于需要主动调整裁剪框大小时使用 如果没有特殊需求,不需要配置 | 根据父组件计算 |
maxScale | Double | 允许放大的最大尺寸 | 10.0 |
borderColor | Color | 裁剪框颜色 | Colors.White |
cropRectUpdateStart | Function | 裁剪区域开始变化时的回调 | - |
cropRectUpdate | Function(Rect rect) | 裁剪区域变化时的回调 | - |
cropRectUpdateEnd | Function(Rect rect) | 返回 | 必填 |
使用Demo
可参考
git
的example
,可以直接运行
git引入
crop_box:
git:
url: https://github.com/godaangel/flutter_crop_box.git
代码
import 'package:crop_box/crop_box.dart';
// ...
CropBox(
// cropRect: Rect.fromLTRB(1 - 0.4083, 0.162, 1, 0.3078), // 2.4倍 随机位置
// cropRect: Rect.fromLTRB(0, 0, 0.4083, 0.1457), //2.4倍,都是0,0
cropRect: Rect.fromLTRB(0, 0, 1, 0.3572), // 1倍
clipSize: Size(200, 315),
cropRatio: Size(16, 9),
cropRectUpdateEnd: (rect) {
print("裁剪区域最终确定 $rect");
},
cropRectUpdate: (rect) {
print("裁剪区域变化 $rect");
},
child: Image.network(
"https://img1.maka.im/materialStore/beijingshejia/tupianbeijinga/9/M_7TNT6NIM/M_7TNT6NIM_v1.jpg",
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent loadingProgress) {
if (loadingProgress == null)
return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes
: null,
),
);
},
),
)
TODO
- 动态变换裁剪框比例
- 优化边界计算代码
- 支持圆角裁剪框绘制
- 支持旋转
本作品采用《CC 协议》,转载必须注明作者和本文链接