先上GitHub看一下效果:
前言
網(wǎng)上有不少iOS做圖片縮放平移的教程杏瞻,大部分都使用了一個(gè)UIScrollView內(nèi)嵌一個(gè)UIimageView完成,不容易控制圖片的自由移動(dòng)并以雙中心縮放衙荐,我覺得不是很酷炫捞挥,本篇直接使用了UIImageView,實(shí)現(xiàn)的控件有如下特點(diǎn):
在圖片內(nèi)移動(dòng)的裁剪框忧吟,可以用裁剪框的四邊和四角改變矩形裁剪框的形狀砌函,隨著圖片的旋轉(zhuǎn)按比例縮放并旋轉(zhuǎn)編輯框
任意移動(dòng)圖片,以雙指為錨點(diǎn)實(shí)現(xiàn)縮放溜族,旋轉(zhuǎn)胸嘴。并以圖片大小,編輯框的位置實(shí)現(xiàn)圖片的裁剪
控件沒有大的技術(shù)難點(diǎn)斩祭,但是邏輯比較復(fù)雜劣像,算是一個(gè)寫自定義view練手的列子。
本文貼出關(guān)鍵的代碼摧玫,首先說明了圖片是如何平移耳奕,縮放和旋轉(zhuǎn)的,然后在說明裁剪框的實(shí)現(xiàn)方式诬像。
圖片平移屋群,縮放,旋轉(zhuǎn)關(guān)鍵代碼
1.圖片的平移:使用一個(gè)panGestureRecognizer坏挠,當(dāng)手指移動(dòng)的時(shí)候芍躏,改變imageView.center, 并且根據(jù)圖片縮放的大小,適配手指移動(dòng)的速度
func panImageView(sender: UIPanGestureRecognizer) {
var translation = sender.translation(in:sender.view)
translation.x = translation.x * imageZoomScale
translation.y = translation.y * imageZoomScale
let view = sender.view
if screenHeight - (view!.frame.origin.y + view!.frame.size.height + translation.y) > cropBottomMargin {
translation.y = screenHeight - (view!.frame.origin.y + view!.frame.size.height) - cropBottomMargin
}
if screenWidth - (view!.frame.origin.x + view!.frame.size.width + translation.x) > cropRightMargin {
translation.x = screenWidth - (view!.frame.origin.x + view!.frame.size.width) - cropRightMargin
}
view?.center = CGPoint(x: (view?.center.x)! + translation.x, y: (view?.center.y)! + translation.y)
sender.setTranslation(CGPoint.zero, in: view?.superview)
}
2.圖片的縮放
以雙指開始時(shí)的位置為錨點(diǎn)降狠,通過改變UIImageView的Transform縮放圖片
// 設(shè)置錨點(diǎn)
private func adjustAnchorPointForGesture(sender: UIGestureRecognizer) {
if sender.state == UIGestureRecognizerState.began {
let piceView = imageView
let locationInView = sender.location(in: piceView)
let locationInSuperView = sender.location(in: piceView?.superview)
piceView?.layer.anchorPoint = CGPoint(x: locationInView.x / piceView!.bounds.size.width, y: locationInView.y / piceView!.bounds.size.height)
piceView?.center = locationInSuperView
}
}
// 改變 imageView.transform 并在手勢完成后对竣,判斷最大最小的放大倍數(shù)庇楞,用一個(gè)動(dòng)畫將image view調(diào)整到最大/最小的放大倍數(shù)
@objc fileprivate func handlePinchGesture(sender: UIPinchGestureRecognizer) {
NSLog("pinch")
adjustAnchorPointForGesture(sender: sender)
if sender.state == UIGestureRecognizerState.changed {
imageZoomScale = imageView.frame.size.height / originImageViewFrame.size.height
if imageZoomScale > 0.5 {
imageView.transform = imageView.transform.scaledBy(x: sender.scale, y: sender.scale)
sender.scale = 1
}
if layoutCropView {
updateCropViewLayout()
adjustOverLayView()
}
} else if sender.state == UIGestureRecognizerState.ended
|| sender.state == UIGestureRecognizerState.cancelled {
animationAfterZoom(zoomScale: imageZoomScale)
}
}
3.圖片的旋轉(zhuǎn)
let image = UIImage(cgImage: imageView.image!.cgImage!, scale: 1.0, orientation: .right)
// withOrientation: .right 使得圖片總是向右邊旋轉(zhuǎn)
let newImage = rotateImage(source: image, withOrientation: .right)
func rotateImage(source: UIImage, withOrientation orientation: UIImageOrientation) -> UIImage {
UIGraphicsBeginImageContext(source.size)
let context = UIGraphicsGetCurrentContext()
if orientation == .right {
context?.ctm.rotated(by: CGFloat.pi / 2)
} else if orientation == .left {
context?.ctm.rotated(by: -(CGFloat.pi / 2))
} else if orientation == .down {
// do nothing
} else if orientation == .up {
context?.ctm.rotated(by: CGFloat.pi / 2)
}
source.draw(at: CGPoint.zero)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
裁剪框的實(shí)現(xiàn)
從圖片上可以看到,首先在UIImageView上面加了一個(gè)半透明的浮層否纬,然后在裁剪框的內(nèi)部去掉浮層直接顯示圖片吕晌。
關(guān)于浮層,有些實(shí)現(xiàn)的方法比較復(fù)雜临燃,及上下左右使用了4個(gè)view來做浮層睛驳,并且當(dāng)調(diào)整中間的白色裁剪框時(shí)需要調(diào)整這4個(gè)浮層,示意圖如下:
我的做法是膜廊,整個(gè)浮層使用一個(gè)UIView乏沸,在drawRect方法中使用quartz2d畫圖,首先畫出灰色的浮層爪瓜,然后再畫一個(gè)空白透明的區(qū)域作為cropView屎蜓,這樣就實(shí)現(xiàn)了在一個(gè)view中畫出了灰色浮層和透明的裁剪框。每一次更新cropView的frame就從新繪制這個(gè)view钥勋,代碼如下:
override func draw(_ rect: CGRect) {
UIColor.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.2).set()
UIRectFill(self.frame)
let intersecitonRect = self.frame.intersection(self.cropRect!)
UIColor.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0).set()
UIRectFill(intersecitonRect)
}
關(guān)于cropView炬转,因?yàn)檫@個(gè)cropView需要:
- 可以平移
- 可以通過四邊,四角進(jìn)行大小的調(diào)整
- 圖片旋轉(zhuǎn)之后要保持原來框選的內(nèi)容
要滿足這幾個(gè)要求算灸,用frame會(huì)導(dǎo)致只算復(fù)雜扼劈,所以我選擇了使用AutoLayout來設(shè)置了cropView的四邊到屏幕上下左右的距離。
cropRightMargin = (CGFloat)(originImageViewFrame.size.width / 2) - (CGFloat)(INIT_CROP_VIEW_SIZE / 2)
cropLeftMargin = cropRightMargin
cropTopMargin = (CGFloat)(originImageViewFrame.size.height / 2) - (CGFloat)(INIT_CROP_VIEW_SIZE / 2) + (CGFloat)((screenHeight - originImageViewFrame.size.height) / 2)
cropBottomMargin = cropTopMargin
let views = ["cropView":cropView!, "imageView":imageView!] as [String : UIView]
let Hvfl = String(format: "H:|-%f-[cropView]-%f-|", cropLeftMargin, cropRightMargin);
let Vvfl = String(format: "V:|-%f-[cropView]-%f-|", cropTopMargin, cropBottomMargin)
let cropViewHorizentalConstraints = NSLayoutConstraint.constraints(withVisualFormat: Hvfl, options: [], metrics: nil, views: views)
let cropViewVerticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: Vvfl, options: [], metrics: nil, views: views)
cropViewConstraints += cropViewHorizentalConstraints
cropViewConstraints += cropViewVerticalConstraints
self.addConstraints(cropViewVerticalConstraints)
self.addConstraints(cropViewHorizentalConstraints)
self.layoutIfNeeded()
adjustOverLayView()
并且為CropView添加了子View來模擬四個(gè)角菲驴,四條邊荐吵,并設(shè)置他們的ViewTag
注意到這四個(gè)子View是非常小的,手指很難碰到赊瞬,所以要擴(kuò)大他們的觸摸區(qū)域先煎,這里我通過重寫PointInside方法,根據(jù)viewtag巧涧,擴(kuò)大四邊和四角的觸摸區(qū)域:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var pointInside = false
if self.frame.contains(convert(point, to: self.superview)) {
pointInside = true
hittedViewTag = self.tag
}
for subview in subviews as [UIView] {
if !subview.isHidden && subview.alpha > 0
&& subview.isUserInteractionEnabled {
var extendFrame: CGRect
if subview.tag == LyEditImageView.UP_LINE_TAG || subview.tag == LyEditImageView.DOWN_LINE_TAG {
extendFrame = CGRect(x: subview.frame.origin.x + 25, y: subview.frame.origin.y - 20, width: subview.frame.size.width - 50, height: subview.frame.size.height + 40)
} else if subview.tag == LyEditImageView.LEFT_LINE_TAG || subview.tag == LyEditImageView.RIGHT_LINE_TAG {
extendFrame = CGRect(x: subview.frame.origin.x - 20, y: subview.frame.origin.y + 25, width: subview.frame.size.width + 40, height: subview.frame.size.height - 50)
} else {
extendFrame = CGRect(x: subview.frame.origin.x - 20, y: subview.frame.origin.y - 20, width: subview.frame.size.width + 40, height: subview.frame.size.height + 40)
}
if extendFrame.contains(point) {
hittedViewTag = subview.tag
pointInside = true
}
}
}
return pointInside
}
這樣當(dāng)我需要調(diào)整cropView大小的時(shí)候:
1.平移:同移動(dòng)ImageView薯蝎,根據(jù)UIPanGesture point translate改變cropView的四個(gè)constraints
private func panCropView( translation: CGPoint) {
var translation = translation
let right = cropRightMargin
let left = cropLeftMargin
let top = cropTopMargin
let bottom = cropBottomMargin
cropRightMargin! -= translation.x
cropLeftMargin! += translation.x
cropBottomMargin! -= translation.y
cropTopMargin! += translation.y
updateCropViewLayout()
// redraw overLayView after move cropView
adjustOverLayView()
}
2.通過四邊縮放cropView:改變與某一邊相關(guān)的Margin(constraint)
func handleCropViewPanGesture(sender: UIPanGestureRecognizer) {
let tag:Int = cropView.getCropViewTag()
let view = sender.view
var translation = sender.translation(in: view?.superview)
switch tag {
// 通過左邊改變cropView
case LyEditImageView.LEFT_LINE_TAG:
cropLeftMargin! += translation.x
break
... ...
}
3.通過四個(gè)角縮放cropView:改變與這個(gè)角相關(guān)的兩個(gè)Margin,如通過左上角縮放的話谤绳,需要調(diào)整MarginRight和MarginTop
func handleCropViewPanGesture(sender: UIPanGestureRecognizer) {
let tag:Int = cropView.getCropViewTag()
let view = sender.view
var translation = sender.translation(in: view?.superview)
switch tag {
// 通過左上角改變cropView
case LyEditImageView.LEFT_UP_TAG:
cropTopMargin! += translation.y
cropLeftMargin! += translation.x
break
... ...
}
4.旋轉(zhuǎn)圖片:根據(jù)圖片的zoomScale占锯,cropView距離圖片四邊的值,依次交換
cropLeftMargin = cropBottomToImage * cropViewConstraintsRatio + imageView.frame.origin.x
cropTopMargin = cropLeftToImage * cropViewConstraintsRatio + imageView.frame.origin.y
cropRightMargin = cropTopToImage * cropViewConstraintsRatio + screenWidth - imageView.frame.origin.x - imageView.frame.size.width
cropBottomMargin = cropRightToImage * cropViewConstraintsRatio + screenHeight - imageView.frame.origin.y - imageView.frame.size.height
最后缩筛,點(diǎn)擊四邊的時(shí)候消略,給用戶個(gè)提示,擴(kuò)展一下被點(diǎn)擊邊的視角瞎抛,例如點(diǎn)擊了底邊:
那么在touchsBegin里面:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
print("cropview began")
updateSubView()
delegate?.cropRemoveBlurOverLay?()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
print("cropview end")
resetHightLightView()
delegate?.cropAddBlurOverLay?(cropRect: self.frame)
}
func updateSubView() {
print("updateSubView")
... ...
if hittedViewTag == LyEditImageView.DOWN_LINE_TAG {
downLine.frame = CGRect(x:0, y: self.frame.size.height - LINE_WIDTH, width: self.frame.size.width, height: LINE_WIDTH * 2);
} else {
downLine.frame = CGRect(x:0, y: self.frame.size.height - LINE_WIDTH, width: self.frame.size.width, height: LINE_WIDTH);
}
}
最后
有問題可以評論文章艺演,喜歡的話請按個(gè)贊
Have fun :)