快速實現(xiàn)微信圖片裁剪功能

最近和小伙伴 @anotheren 一起在搞事情,打算把微信的圖片選擇器那一套給做出來柠辞。于是就有了 AnyImageKit 這個框架渴邦,現(xiàn)在已經完成圖片選擇和編輯功能了。在做圖片編輯功能的時候澎剥,裁剪這個功能做了很久锡溯,想到一個思路去做,做到一半發(fā)現(xiàn)不行,推翻重做祭饭,反復經歷了這個過程兩三次之后芜茵,最終給做出來了。這個功能的坑還是挺多的倡蝙,而且網上關于這一塊的資料不多九串,于是就想寫一篇文章記錄一下。

首先我們要先來解決三個小問題寺鸥。

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

image

先來考慮橫圖(第二張圖)的情況猪钮,設圖片寬度為 scrollView.bounds.width,再將圖片的高度進行等比縮放胆建。

width = scrollView.bounds.width
height = image.width * scrollView.bounds.height / scrollView.bounds.width

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

// 如果圖片高度超過 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

image

問題二:圖片縮放后檐蚜,如何展示超出 scrollView 的部分

image

看到這個問題就會很自然的想到,scrollView 可能是全屏的沿侈,所以才能全部展示出來闯第。
但是全屏的 scrollView 會有一些問題無法解決,下面的第三個問題會講到缀拭,我們暫時不考慮這個解決方案咳短。

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

問題三:無縮放比例時咙好,如何使圖片可以拉動

效果圖

眾所周知當 scrollView.contentSize < scrollView.bounds.sizescrollView 是無法滾動的褐荷,那么要怎么做才能使 scrollView 可滾動呢勾效,答案是 contentInset

在日常開發(fā)中叛甫,contentInset 這個 API 幾乎用不到层宫,可能有一些朋友對這個屬性有點陌生,所以特別說明一下其监。contentInsetUIEdgeInsets萌腿,它的作用是給 scrollView 額外增加一段滾動區(qū)域。舉個例子 MJRefresh 中的下拉刷新相信大家都用過抖苦,當正在刷新的時候毁菱,你會發(fā)現(xiàn) scrollView 的頂部多出了一段可滾動區(qū)域米死,這個就是用 contentInset 這個 API 實現(xiàn)的。

了解了 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)

到這里問題三就解決了,現(xiàn)在我們反過來看問題二商膊,如果在問題二中采用全屏 scrollView伏伐,那要第三個問題是不是就不好解決了呢~

裁剪

關于裁剪的 UI 部分這里就不展開說了,主要說明一下裁剪框的四個角是用 UIView 畫出來的晕拆,他們的層級與 scrollView 相同藐翎,他們的位置可以用一個 CGRect 的變量 cropRect 來描述。

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

image

根據動圖所展示的效果昆庇,可以得出:

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

下面我們一步一步來看怎么解決這些問題末贾。

ZoomScale

從動圖中我們可以看到移動裁剪框之后要對 scrollView 進行縮放,而且有兩種情況整吆,一種是橫圖拱撵,一種是豎圖,所以我們需要計算兩種情況的縮放比例表蝙,再選擇使用其中的一種拴测。

image

我們假設圖片的大小是 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 的數(shù)值。

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

現(xiàn)在我們得出了橫圖縮放比例的公式咽安,豎圖也是一樣的,代碼如下:

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

image

現(xiàn)在我們來計算 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~

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末轮纫,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子焚鲜,更是在濱河造成了極大的恐慌掌唾,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恃泪,死亡現(xiàn)場離奇詭異郑兴,居然都是意外死亡,警方通過查閱死者的電腦和手機贝乎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叽粹,“玉大人览效,你說我怎么就攤上這事〕婕福” “怎么了锤灿?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辆脸。 經常有香客問我但校,道長,這世上最難降的妖魔是什么啡氢? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任状囱,我火速辦了婚禮,結果婚禮上倘是,老公的妹妹穿的比我還像新娘亭枷。我一直安慰自己,他們只是感情好搀崭,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布叨粘。 她就那樣靜靜地躺著,像睡著了一般瘤睹。 火紅的嫁衣襯著肌膚如雪升敲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天轰传,我揣著相機與錄音驴党,去河邊找鬼。 笑死绸吸,一個胖子當著我的面吹牛鼻弧,可吹牛的內容都是我干的设江。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼攘轩,長吁一口氣:“原來是場噩夢啊……” “哼叉存!你這毒婦竟也來了?” 一聲冷哼從身側響起度帮,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤歼捏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后笨篷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞳秽,經...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年率翅,在試婚紗的時候發(fā)現(xiàn)自己被綠了练俐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡冕臭,死狀恐怖腺晾,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情辜贵,我是刑警寧澤悯蝉,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站托慨,受9級特大地震影響鼻由,放射性物質發(fā)生泄漏。R本人自食惡果不足惜厚棵,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一蕉世、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧窟感,春花似錦讨彼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至躏嚎,卻和暖如春蜜自,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卢佣。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工重荠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虚茶。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓戈鲁,卻偏偏與公主長得像仇参,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子婆殿,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內容