快速實現微信圖片裁剪功能

最近和小夥伴 @anotheren 一塊兒在搞事情,打算把微信的圖片選擇器那一套給作出來。因而就有了 AnyImageKit 這個框架,如今已經完成圖片選擇和編輯功能了。在作圖片編輯功能的時候,裁剪這個功能作了好久,想到一個思路去作,作到一半發現不行,推翻重作,反覆經歷了這個過程兩三次以後,最終給作出來了。這個功能的坑仍是挺多的,並且網上關於這一塊的資料很少,因而就想寫一篇文章記錄一下。git

首先咱們要先來解決三個小問題。github

問題一:如何將圖片完整的展現出來

先來考慮橫圖(第二張圖)的狀況,設圖片寬度爲 scrollView.bounds.width,再將圖片的高度進行等比縮放。swift

width = scrollView.bounds.width
height = image.width * scrollView.bounds.height / scrollView.bounds.width
複製代碼

接下來考慮豎圖(第一張圖)的狀況,在上一步的基礎上進行判斷。bash

// 若是圖片高度超過 scrollView.bounds.height 就是豎圖,將圖片高度縮放到 scrollView.bounds.height,再根據比例計算寬度。
if height > scrollView.bounds.height {
    height = scrollView.bounds.height
    width = image.height * scrollView.bounds.width / scrollView.bounds.height
}
複製代碼

最後根據 size 計算一下 imageView.frame 這個問題就解決了。微信

注:灰色的部分是 scrollView框架

問題二:圖片縮放後,如何展現超出 scrollView 的部分

看到這個問題就會很天然的想到,scrollView 多是全屏的,因此才能所有展現出來。 可是全屏的 scrollView 會有一些問題沒法解決,下面的第三個問題會講到,咱們暫時不考慮這個解決方案。ui

第二個方案就相對簡單多了,只須要設置 scrollView.clipsToBounds = false 就解決這個問題了。spa

問題三:無縮放比例時,如何使圖片能夠拉動

效果圖

衆所周知當 scrollView.contentSize < scrollView.bounds.sizescrollView 是沒法滾動的,那麼要怎麼作才能使 scrollView 可滾動呢,答案是 contentInset3d

在平常開發中,contentInset 這個 API 幾乎用不到,可能有一些朋友對這個屬性有點陌生,因此特別說明一下。contentInsetUIEdgeInsets,它的做用是給 scrollView 額外增長一段滾動區域。舉個例子 MJRefresh 中的下拉刷新相信你們都用過,當正在刷新的時候,你會發現 scrollView 的頂部多出了一段可滾動區域,這個就是用 contentInset 這個 API 實現的。code

瞭解了 contentInset 以後,咱們要先更正一下 scrollView 可滾動的條件:

scrollView.contentSize + scrollView.contentInset > scrollView.bounds.size
複製代碼

下面咱們設置 contentInset 的值爲 0.1(肉眼無感知)

scrollView.contentInset = UIEdgeInsets(top: 0.1, left: 0.1, bottom: 0.1, right: 0.1)
複製代碼

這麼設置完以後,圖片能夠左右滾動了,可是沒法上下滾動,由於圖片的寬和 scrollView 是相等的,可是高度不是,因此咱們要針對高度進行一下計算:

let bottomInset = scrollView.bounds.height - cropRect.height + 0.1
複製代碼

對於豎圖來講就是處理寬度的問題,整合一下代碼:

let rightInset = scrollView.bounds.width - cropRect.width + 0.1
let bottomInset = scrollView.bounds.height - cropRect.height + 0.1
scrollView.contentInset = UIEdgeInsets(top: 0.1, left: 0.1, bottom: bottomInset, right: rightInset)
複製代碼

到這裏問題三就解決了,如今咱們反過來看問題二,若是在問題二中採用全屏 scrollView,那要第三個問題是否是就很差解決了呢~

裁剪

關於裁剪的 UI 部分這裏就不展開說了,主要說明一下裁剪框的四個角是用 UIView 畫出來的,他們的層級與 scrollView 相同,他們的位置能夠用一個 CGRect 的變量 cropRect 來描述。

裁剪核心的內容就是當裁剪框移動時,如何將圖片移動到正確的位置上,示例以下。

根據動圖所展現的效果,能夠得出:

  1. scrollView 的縮放有變化
  2. scrollView 的偏移量有變化
  3. 裁剪框的位置移動了

下面咱們一步一步來看怎麼解決這些問題。

ZoomScale

從動圖中咱們能夠看到移動裁剪框以後要對 scrollView 進行縮放,並且有兩種狀況,一種是橫圖,一種是豎圖,因此咱們須要計算兩種狀況的縮放比例,再選擇使用其中的一種。

咱們假設圖片的大小是 ABCD,咱們移動點 D 到點 G 的位置,即裁剪框的位置是 AEFG。當用戶鬆手後,AEFG 要放大到 ABCD 的位置,由此咱們能夠得出縮放比例爲:AB/AE = 375/187.5 = 2.0

可是尚未結束,想象一下,當 AEFG 放大到 ABCD 後,再次將點 D 到點 G 的位置。這個操做至關於,圖片未縮放前從點 G 移動到點 J

根據以前的結論咱們能夠得知縮放比例是:AB/AH,在實際代碼中 AB = scrollView.bounds.width,下面要求出 AH 的數值。

  1. AEFG 放大 2.0 倍到 ABCD
  2. 從點 D 到點 G,即 cropRect.width = 187.5
  3. AH = cropRect.width/scrollView.zoomScale = 187.5/2.0 = 93.75

如今咱們得出了橫圖縮放比例的公式,豎圖也是同樣的,代碼以下:

let zoomH = scrollView.bounds.width / (cropRect.width / scrollView.zoomScale)
let zoomV = scrollView.bounds.height / (cropRect.height / scrollView.zoomScale)
複製代碼

接下來咱們要分析該用橫圖的縮放比例仍是豎圖的。咱們將裁剪框的寬,即 cropRect.width,縮放到 scrollView.bounds.width,根據縮放比例可計算出縮放後 cropRect.height,若是 cropRect.height > scrollView.bounds.height,意味着高度太高了,咱們就要用豎圖的縮放公式,反之用橫圖的,代碼以下:

let maxZoom = scrollView.maximumZoomScale
let zoomH = scrollView.bounds.width / (cropRect.width / scrollView.zoomScale)
let zoomV = scrollView.bounds.height / (cropRect.height / scrollView.zoomScale)
let isVertical = cropRect.height * (scrollView.bounds.width / cropRect.width) > scrollView.bounds.height
let zoom: CGFloat
if !isVertical {
    zoom = zoomH > maxZoom ? maxZoom : zoomH
} else {
    zoom = zoomV > maxZoom ? maxZoom : zoomV
}
複製代碼

ContentOffset

如今咱們來計算 contentOffset,設圖片爲 ABCD,將點 A 移動到點 EEFGD 放大 2.0 倍到 ABCD。由此可得:

注:₁ 表示縮放前,₂ 表示縮放一次後;cropStartPanRect 是手勢開始前裁剪框的位置

E(x) = CG₂
     = CG₁ * zoom
     = (cropRect.origin.x - cropStartPanRect.origin.x) * zoom
複製代碼

上述這個公式並非最終的公式,接下來基於當前縮放比例,再次把點 A 移動到點 E,這個操做至關於,圖片未縮放前從點 E 移動到點 H,由此可得:

注:₁ 表示縮放前,₂ 表示縮放一次後,₃ 表示縮放兩次後

// 計算本次縮放的比例
let zoomScale = zoom / scrollView.zoomScale

H(x) = CJ₃
     = CG₃ + GJ₃
     = CG₂ * zoom + GJ₂ * zoomScale
     = scrollView.contentOffset.x * zoomScale + (cropRect.origin.x - cropStartPanRect.origin.x) * zoomScale
複製代碼

最後咱們根據移動的角,計算最終的 contentOffset

let zoomScale = zoom / scrollView.zoomScale
let offsetX = (scrollView.contentOffset.x * zoomScale) + ((cropRect.origin.x - cropStartPanRect.origin.x) * zoomScale)
let offsetY = (scrollView.contentOffset.y * zoomScale) + ((cropRect.origin.y - cropStartPanRect.origin.y) * zoomScale)
let offset: CGPoint
switch position {  // 一個枚舉,標誌角的位置
case .topLeft:     // 移動左上角,contentOffset x 和 y 都要改變
    offset = CGPoint(x: offsetX, y: offsetY)
case .topRight:    // 移動右上角,contentOffset y 要改變
    offset = CGPoint(x: scrollView.contentOffset.x * zoomScale, y: offsetY)
case .bottomLeft:  // 移動左下角,contentOffset x 要改變
    offset = CGPoint(x: offsetX, y: scrollView.contentOffset.y * zoomScale)
case .bottomRight: // 移動右下角,contentOffset 不變
    offset = CGPoint(x: scrollView.contentOffset.x * zoomScale, y: scrollView.contentOffset.y * zoomScale)
}
複製代碼

NewCropRect

最後拖動裁剪框鬆手後,咱們須要把裁剪框放大並居中,這段邏輯和第一個問題計算圖片的縮放比例中使用橫圖豎圖的計算邏輯是同樣的,就再也不贅述了。

let newCropRect: CGRect
if (zoom == maxZoom && !isVertical) || zoom == zoomH {
    let scale = scrollView.bounds.width / cropRect.width
    let height = cropRect.height * scale
    let y = (scrollView.bounds.height - height) / 2 + scrollView.frame.origin.y
    newCropRect = CGRect(x: scrollView.frame.origin.x, y: y, width: scrollView.bounds.width, height: height)
} else {
    let scale = scrollView.bounds.height / cropRect.height
    let width = cropRect.width * scale
    let x = (scrollView.bounds.width - width + scrollView.frame.origin.x) / 2
    newCropRect = CGRect(x: x, y: scrollView.frame.origin.y, width: width, height: scrollView.frame.height)
}
複製代碼

結語

關於裁剪還有一些內容沒講,好比說完成裁剪,裁剪後再次進入裁剪的邏輯等。可是剩下這些裁剪邏輯的難度和上面這些內容差很少,若是你能理解上面的內容,相信剩下的邏輯對你來講也沒有難度了。

最後歡迎你們給咱們的 項目 點 Star,提 Issue 和 PR~

相關文章
相關標籤/搜索