iOS - 圖片的平移钧椰,縮放蛛壳,旋轉(zhuǎn)和裁剪

先上GitHub看一下效果:

GitHub:LyEditImageView

preview.png

前言

網(wǎng)上有不少iOS做圖片縮放平移的教程杏瞻,大部分都使用了一個(gè)UIScrollView內(nèi)嵌一個(gè)UIimageView完成,不容易控制圖片的自由移動(dòng)并以雙中心縮放衙荐,我覺得不是很酷炫捞挥,本篇直接使用了UIImageView,實(shí)現(xiàn)的控件有如下特點(diǎn):

  1. 在圖片內(nèi)移動(dòng)的裁剪框忧吟,可以用裁剪框的四邊和四角改變矩形裁剪框的形狀砌函,隨著圖片的旋轉(zhuǎn)按比例縮放并旋轉(zhuǎn)編輯框

  2. 任意移動(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è)浮層,示意圖如下:

4rect.png

我的做法是膜廊,整個(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需要:

  1. 可以平移
  2. 可以通過四邊,四角進(jìn)行大小的調(diào)整
  3. 圖片旋轉(zhuǎn)之后要保持原來框選的內(nèi)容

要滿足這幾個(gè)要求算灸,用frame會(huì)導(dǎo)致只算復(fù)雜扼劈,所以我選擇了使用AutoLayout來設(shè)置了cropView的四邊到屏幕上下左右的距離。

cropview constraints.png
        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

cropview.png

注意到這四個(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)擊了底邊:

屏幕快照 2017-07-03 上午10.36.19.png

那么在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 :)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胎撤,更是在濱河造成了極大的恐慌晓殊,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哩照,死亡現(xiàn)場離奇詭異,居然都是意外死亡懒浮,警方通過查閱死者的電腦和手機(jī)飘弧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砚著,“玉大人次伶,你說我怎么就攤上這事』拢” “怎么了冠王?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舌镶。 經(jīng)常有香客問我柱彻,道長,這世上最難降的妖魔是什么餐胀? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任哟楷,我火速辦了婚禮,結(jié)果婚禮上否灾,老公的妹妹穿的比我還像新娘卖擅。我一直安慰自己,他們只是感情好墨技,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布惩阶。 她就那樣靜靜地躺著,像睡著了一般扣汪。 火紅的嫁衣襯著肌膚如雪断楷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天崭别,我揣著相機(jī)與錄音脐嫂,去河邊找鬼。 笑死紊遵,一個(gè)胖子當(dāng)著我的面吹牛账千,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播暗膜,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼匀奏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了学搜?” 一聲冷哼從身側(cè)響起娃善,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤论衍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后聚磺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坯台,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年瘫寝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜒蕾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,764評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡焕阿,死狀恐怖咪啡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情暮屡,我是刑警寧澤撤摸,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站褒纲,受9級特大地震影響准夷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莺掠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一冕象、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧汁蝶,春花似錦渐扮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至幔亥,卻和暖如春耻讽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背帕棉。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工针肥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人香伴。 一個(gè)月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓慰枕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親即纲。 傳聞我的和親對象是個(gè)殘疾皇子具帮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容