手擼一個Anything Cut Widget

godaangel發表於2021-01-13

背景:為啥要開發這個元件呢?因為目前在我們的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 協議》,轉載必須註明作者和本文連結

相關文章