手撸一个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
如果没有传入初始裁剪区域,那么默认缩放尺寸 _scale1.0,并且默认居中裁剪,此时只需要计算出居中的偏移量 _deltaPoint 就行。
如果传入了初始裁剪区域,那么首先需要根据 裁剪框宽高比 和 素材宽高比,来确定 _scale 的数值,如果 裁剪框宽高比 大于 素材宽高比,那么 _scale 相当于将宽边放大到1所需要的倍数,_scale = 1 / resultRect.width,反之 _scale = 1 / resultRect.height
然后再根据中心点、裁剪框 Rect 以及 _scale 计算出初始偏移量,设定摆放位置 _deltaPoint

5、移动手势操作

使用 GestureDetectoronScale 相关事件来进行缩放和移动判断,并且将值传回给 Transform 组件,在视图上体现出来。

6、计算边界值,并且进行修正

因为在移动过程中,我们不能超出素材范围,所以需要进行边界触碰计算和修正,使范围限定在Rect.fromLTRB(0, 0, 1, 1) 范围内。
移动时需要先计算当前位置,然后对比边界值,如果超出了,则移回区域内,并且重新计算位置,再传给 resultRect 值。

7、传出结果

加入回调函数如下

  • cropRectUpdateStart 在裁剪区域开始变化时触发
  • cropRectUpdate初始化以及裁剪区域变化时触发,传出参数为 Rect 类型,表示当前裁剪区域
  • cropRectUpdateEnd 在停止变化时触发,传出参数为 Rect 类型,表示当前裁剪区域

代码地址

git地址

参数设计

参数名 类型 描述 默认值
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

可参考 gitexample,可以直接运行

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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!