⭐ ️尺寸調整

參考 iOS 照片App 有的尺寸,有原圖、正方形、9:16、4:5、5:7、3:4、3:5、2:3 共 8 個選項,除了正方形外其他比例還有直的跟橫的可以選

這部分是我卡最久的,看著 iOS 照片 App 的畫面,基本上是固定尺寸框,然後自動放大背後的圖片,透過移動或縮放圖片來讓使用者選擇裁切範圍。尺寸框也可以靠 4 個角來縮小跟小範圍移動,過幾秒之後會再自動放大成符合螢幕的大小(如下圖)

然後…沒什麼頭緒😂 目前能力做不到這麼厲害,但我希望至少圖片可以縮放,還有可以自己決定要裁切圖片的哪個部分,後來參考了彼得潘跟一位學姊的文章,終於有一點點想法了!

不過這個部分我還是覺得沒有做得很好,有興趣可以參考看看🥺

放照片的圖層的部分,這裡一樣是用一個最外層的 view,裡頭放著一樣大小的 scroll view,scroll view 裡裝從主頁傳輸過來的照片

1️⃣ 利用 scroll view 實現圖片縮放功能,將所在的 view controller 設為 scroll view 的 delegate,並用 contentInset 將圖片縮小成符合螢幕的大小且置中

請參考上方彼得潘的文章(寫出來這篇可能要看半小時才看得完(?),做完效果如下

2️⃣ 畫裁切框

首先,因為我們讓圖片能依照本來的比例完整顯示在螢幕,且在scroll view範圍的中間,也就是類似 image view 的 Aspect fit 模式,所以我們需要先知道圖片的寬跟高從原圖縮小多少,還有縮小後左上角的座標

例如我們把一個 800 * 800 的正方形圖片放進 375 * 600 的 image view 中,圖片會縮小成 375 * 375,他會以 image view 裝得下的最大邊長來進行縮放

所以我們透過比較「 image view 的寬 / 圖片的寬」跟「 image view 的高 / 圖片的高」的比例,取較小的那一個就是圖片縮小的範圍

比較 375/800、600/800,是 375/800 比較小,所以圖片放進去之後,寬跟高會等比例 * 375/800,得到 375 * 375 的結果

至於座標,image view 的左上角就是他自身座標系統的(0,0),而放入的圖片也是包含在他自身座標系統中,所以只要計算縮小後的圖片的高跟原本 image view 高相差多少,除以 2 就是新的 y 座標

以上圖為例,新的 y 座標就是(600–375)/ 2= 112.5,所以圖片的左上角座標就是(0, 112.5),就是畫裁切外框的起始座標

上面如果可以理解,就開始畫圖啦!

💡 畫外框

func drawRect() -> CAShapeLayer{
let imgW = editedImage.size.width //原圖寬
let imgH = editedImage.size.height //原圖高
let viewW = imageScrollView.bounds.width
let viewH = imageScrollView.bounds.height
let scale = min(viewW / imgW, viewH / imgH) //找出縮小比例
let scaledImgW = imgW * scale //縮小後的圖寬
let scaledImgH = imgH * scale //縮小後的圖高
let offsetX = (viewW - scaledImgW) / 2 //距原點的X距離
let offsetY = (viewH - scaledImgH) / 2 //距原點的Y距離

//畫長方形,寬跟高是縮小後的圖的寬跟高
let path = UIBezierPath(rect: CGRect(x: offsetX, y: offsetY, width: scaledImgW, height: scaledImgH))
let rectangleLayer = CAShapeLayer()
rectangleLayer.path = path.cgPath
rectangleLayer.fillColor = UIColor.clear.cgColor //背景透明
rectangleLayer.strokeColor = UIColor.white.cgColor //外框白色
rectangleLayer.lineWidth = 2 //外框粗度

return rectangleLayer
}

💡 畫內框線

func drawLine() -> CAShapeLayer{
let imgW = editedImage.size.width
let imgH = editedImage.size.height
let viewW = imageScrollView.bounds.width
let viewH = imageScrollView.bounds.height
let scale = min(viewW / imgW, viewH / imgH)
let scaledImgW = imgW * scale
let scaledImgH = imgH * scale
let offsetX = (viewW - scaledImgW) / 2
let offsetY = (viewH - scaledImgH) / 2

let line = UIBezierPath()
//寬跟高等分3份,畫出九宮格
line.move(to: CGPoint(x: offsetX + scaledImgW / 3, y: offsetY))
line.addLine(to: CGPoint(x: offsetX + scaledImgW / 3, y: offsetY + scaledImgH))
line.move(to: CGPoint(x: offsetX + scaledImgW / 3 * 2, y: offsetY))
line.addLine(to: CGPoint(x: offsetX + scaledImgW / 3 * 2, y: offsetY + scaledImgH))
line.move(to: CGPoint(x: offsetX, y: offsetY + scaledImgH / 3))
line.addLine(to: CGPoint(x: offsetX + scaledImgW, y: offsetY + scaledImgH / 3))
line.move(to: CGPoint(x: offsetX, y: offsetY + scaledImgH / 3 * 2))
line.addLine(to: CGPoint(x: offsetX + scaledImgW, y: offsetY + scaledImgH / 3 * 2))
let lineLayer = CAShapeLayer()
lineLayer.path = line.cgPath
lineLayer.strokeColor = UIColor.white.cgColor
lineLayer.lineWidth = 1

return lineLayer
}

💡 畫四個角

func drawCorner() -> CAShapeLayer{
let imgW = editedImage.size.width
let imgH = editedImage.size.height
let viewW = imageScrollView.bounds.width
let viewH = imageScrollView.bounds.height
let scale = min(viewW / imgW, viewH / imgH)
let scaledImgW = imgW * scale
let scaledImgH = imgH * scale
let offsetX = (viewW - scaledImgW) / 2
let offsetY = (viewH - scaledImgH) / 2

let corner = UIBezierPath()
//左上角
corner.move(to: CGPoint(x: offsetX, y: offsetY))
corner.addLine(to: CGPoint(x: offsetX + scaledImgW / 9, y: offsetY))
corner.move(to: CGPoint(x: offsetX, y: offsetY))
corner.addLine(to: CGPoint(x: offsetX, y: offsetY + scaledImgH / 9))
//右上角
corner.move(to: CGPoint(x: offsetX + scaledImgW, y: offsetY))
corner.addLine(to: CGPoint(x: offsetX + scaledImgW / 9 * 8, y: offsetY))
corner.move(to: CGPoint(x: offsetX + scaledImgW, y: offsetY))
corner.addLine(to: CGPoint(x: offsetX + scaledImgW, y: offsetY + scaledImgH / 9))
//右下角
corner.move(to: CGPoint(x: offsetX + scaledImgW, y: offsetY + scaledImgH))
corner.addLine(to: CGPoint(x: offsetX + scaledImgW / 9 * 8, y: offsetY + scaledImgH))
corner.move(to: CGPoint(x: offsetX + scaledImgW, y: offsetY + scaledImgH))
corner.addLine(to: CGPoint(x: offsetX + scaledImgW, y: offsetY + scaledImgH / 9 * 8))
//左下角
corner.move(to: CGPoint(x: offsetX, y: offsetY + scaledImgH))
corner.addLine(to: CGPoint(x: offsetX + scaledImgW / 9, y: offsetY + scaledImgH))
corner.move(to: CGPoint(x: offsetX, y: offsetY + scaledImgH))
corner.addLine(to: CGPoint(x: offsetX, y: offsetY + scaledImgH / 9 * 8))


let cornerLayer = CAShapeLayer()
cornerLayer.path = corner.cgPath
cornerLayer.strokeColor = UIColor.white.cgColor
cornerLayer.lineWidth = 4

return cornerLayer
}

💡 把畫完的東西加到事先宣告好的 cropFrame 中,並放到 containerView 顯示出來,因為 cropFrame 本來也沒有東西,所以需要設定他的寬跟高應該要是多少,才能正常顯示我們要的結果

//cropFrame用來裝裁切框線的layer,以及作為截圖範圍
var cropFrame = UIView()
override func viewDidLoad() {
super.viewDidLoad()
//顯示傳遞過來的圖片
imageView.image = editedImage

//將畫好的外框線、內框線跟四個角放到比例框線view中
cropFrame.layer.addSublayer(drawRect())
cropFrame.layer.addSublayer(drawLine())
cropFrame.layer.addSublayer(drawCorner())

//把比例框線view加到containerView中
containerView.addSubview(cropFrame)
//宣告圖片寬、高的property
let imgW = editedImage.size.width
let imgH = editedImage.size.height
//設定scrollview寬、高的property
let viewW = imageScrollView.bounds.width
let viewH = imageScrollView.bounds.height
//scale是圖片在imageView的縮放比例,取scrollview跟圖片的寬跟高的比中較小的值
let scale = min(viewW / imgW, viewH / imgH)
//圖片縮放後的寬跟高會等於原本的寬跟高乘以比例
let scaledImgW = imgW * scale
let scaledImgH = imgH * scale
//讓比例框線view的寬跟高 = 圖片縮放後的寬跟高
cropFrame.frame.size.width = scaledImgW
cropFrame.frame.size.height = scaledImgH

}

3️⃣ 設定尺寸比例

因為可以選擇顯示直向或橫向的尺寸框,所以宣告一個變數 num 來判斷是選擇直向還是橫向按鈕

//num用來判斷選擇裁切框是直的還是橫的,直的num = 1,橫的num = 0
var num = 0
@IBAction func changeRatio(_ sender: UIButton){
let imgW = editedImage.size.width
let imgH = editedImage.size.height
let viewW = imageScrollView.bounds.width
let viewH = imageScrollView.bounds.height
let scale = min(viewW / imgW, viewH / imgH)
let scaledWidth = imgW * scale
let scaledHeight = imgH * scale
let ratioValue = min(scaledWidth, scaledHeight)

//選擇正方形時,無法選擇直向或橫向
if sender.tag == 1{
portraitButton.isEnabled = false
landscapeButton.isEnabled = false
}else if sender.tag == 0 || sender.tag >= 2{
portraitButton.isEnabled = true
landscapeButton.isEnabled = true
}

//如果選擇橫向的尺寸框,以寬度作為基準,更改高度比例。這裡舉3個當範例,剩下的有興趣去看完整程式碼唷
if num == 0{
switch sender.tag{
case 0:
cropFrame.transform = CGAffineTransform(scaleX: 1, y: 1)
landscapeButton.isSelected = true
case 1:
cropFrame.transform = CGAffineTransform(scaleX: ratioValue / scaledWidth, y: ratioValue / scaledHeight)
portraitButton.isSelected = false
landscapeButton.isSelected = false
case 2:
cropFrame.transform = CGAffineTransform(scaleX: ratioValue / scaledWidth, y: ratioValue / scaledHeight * 9 / 16)
landscapeButton.isSelected = true
case 3:
cropFrame.transform = CGAffineTransform(scaleX: ratioValue / scaledWidth, y: ratioValue / scaledHeight * 4 / 5)
landscapeButton.isSelected = true
......
default:
break
}

//如果選擇直向的尺寸框,以高度作為基準,更改寬度比例
}else if num == 1{
switch sender.tag{
case 0:
cropFrame.transform = CGAffineTransform(scaleX: 1, y: 1)
portraitButton.isSelected = true
case 1:
cropFrame.transform = CGAffineTransform(scaleX: ratioValue / scaledWidth, y: ratioValue / scaledHeight)
portraitButton.isSelected = false
landscapeButton.isSelected = false
case 2:
cropFrame.transform = CGAffineTransform(scaleX: ratioValue / scaledWidth * 9 / 16, y: ratioValue / scaledHeight)
portraitButton.isSelected = true
case 3:
cropFrame.transform = CGAffineTransform(scaleX: ratioValue / scaledWidth * 4 / 5, y: ratioValue / scaledHeight)
portraitButton.isSelected = true
......
default:
break
}
}
}

4️⃣ 設定拖曳手勢跟縮放手勢到 cropFrame

跟第二篇的移動跟縮放貼圖類似,這邊不贅述囉!不過我有試圖要給 cropFrame 限制只能在圖片內移動,但有點 bug😅 後面補充~

傳送門

5️⃣ 照片裁切儲存(UIGraphicsImageRenderer)

要先把 cropFrame 移除再輸出照片回傳,才不會把框也留在上面哦!

還有因為 render 的時候是以 containerView 的座標來裁切,所以也需要另外計算座標QQ 如果照片本身是橫向的,跟 containerView 會有高度差,如果是直向的,則會有寬度差,所以分開寫

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//移除裁切框
cropFrame.removeFromSuperview()

let imgW = editedImage.size.width
let imgH = editedImage.size.height

let offsetX = (containerView.bounds.width - cropFrame.frame.width) / 2
let offsetY = (containerView.bounds.height - cropFrame.frame.height) / 2

if imgW > imgH{
let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: cropFrame.frame.minX, y: cropFrame.frame.minY + offsetY, width: cropFrame.frame.width, height: cropFrame.frame.height))
renderImage = renderer.image(actions: { (context) in
containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
})

}else if imgW < imgH{
let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: cropFrame.frame.minX + offsetX, y: cropFrame.frame.minY, width: cropFrame.frame.width, height: cropFrame.frame.height))
renderImage = renderer.image(actions: { (context) in
containerView.drawHierarchy(in: containerView.bounds, afterScreenUpdates: true)
})
}
}

⭐️ 分享照片(UIActivityViewController)

@IBAction func saveImage(_ sender: Any) {    //輸出最終照片
let renderer = UIGraphicsImageRenderer(bounds: CGRect(x: imageView.frame.minX + offsetX, y: imageView.frame.minY + offsetY, width: newWidth, height: newHeight))
renderImage = renderer.image(actions: { (context) in
containerView.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
})

//分享照片
let activityViewController = UIActivityViewController(activityItems: [renderImage], applicationActivities: nil)
present(activityViewController, animated: true, completion: nil)

}

一樣要記得到 info 設定隱私權限跟請求權限的文字,才不會閃退唷!

附上作業 Github 連結

參考資料

— — — — — —

好!!!終於打完了!!!

這麼長大家看完是不是很累,我也是XDDD

最後說說做這個作業的感想,這是目前最讓我燒腦還有迷茫的作業!!

其實我遇到問題很少打擾彼得潘,因為幾乎參考官方文件或是網路上其他人分享的文章都可以找得到解答,結果這個作業讓我跟彼得潘求救了😂 (感謝情歌王子)然後我中間還砍掉重練過…

雖然大部分的功能大概做出來了,不過還是有一些 bug 待解決

  • 尺寸框縮放時會回到原本的比例縮放,而不是根據點選的尺寸
  • 尺寸框縮小後不知道是不是因為座標軸又跟著位移了,所以移動範圍跟裁切會有誤差,只有寬或高符合圖片的時候裁切會是正常
  • 圖片從橫向轉成直向之後,輸出不會是完整的圖片,我猜也是跟 frame、bounds 有關
  • CIImage 轉回 UIImage 轉向問題,加入程式碼之後 filter 就沒有作用@@

我覺得我花太多時間在這個作業上了,先去做後面的其他作業,有空再回來研究😭

--

--