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