图片裁剪交互设计

使用scroll view的特性(滚动,缩放)实现裁剪图片的交互设计实现
思路:

  1. scroll view的有效区域作为裁剪区域
  2. 移动scroll view 边角位置来控制裁剪区域
  3. 使用scroll view 的缩放和滚动来控制裁剪的内容

实现

  1. 定义8个view 分别代替scroll view 8个方向
  2. 8个view是裁剪边框的视觉效果展示
  3. 8个view 分别添加手势,移动时调整8个边框view和scroll view的frame使保持一致
  4. 手势代码如下
    @objc private func clipTagViewMove(pan: UIPanGestureRecognizer) {
        
        let location = pan.location(in: view)
        
        switch pan.state {
        case .began:
            currentMovingTag = pan.view?.tag ?? 0
            lastTopLocation = topLeftView.center.y
            lastLeftLocation = topLeftView.center.x
            
        case .changed:
            switch currentMovingTag {
            case CLIP_TOP_LEFT_TAG:
                updateShowRect(top: location.y, left: location.x, right: bottomRightView.frame.midX, bottom: bottomRightView.frame.midY)
                
            case CLIP_TOP_RIGHT_TAG:
                updateShowRect(top: location.y, left: topLeftView.frame.midX, right: location.x, bottom: bottomRightView.frame.midY)
                
            case CLIP_BOTTOM_LEFT_TAG:
                updateShowRect(top: topLeftView.frame.midY, left: location.x, right: bottomRightView.frame.midX, bottom: location.y)
                
            case CLIP_BOTTOM_RIGHT_TAG:
                updateShowRect(top: topLeftView.frame.midY, left: topLeftView.frame.midX, right: location.x, bottom: location.y)
                
            case CLIP_TOP_TAG:
                updateShowRect(top: location.y, left: topLeftView.frame.midX, right: bottomRightView.frame.midX, bottom: bottomRightView.frame.midY)
                
            case CLIP_LEFT_TAG:
                updateShowRect(top: topLeftView.frame.midY, left: location.x, right: bottomRightView.frame.midX, bottom: bottomRightView.frame.midY)
                
            case CLIP_RIGHT_TAG:
                updateShowRect(top: topLeftView.frame.midY, left: topLeftView.frame.midX, right: location.x, bottom: bottomRightView.frame.midY)
                
            case CLIP_BOTTOM_TAG:
                updateShowRect(top: topLeftView.frame.midY, left: topLeftView.frame.midX, right: bottomRightView.frame.midX, bottom: location.y)
                
            default:
                Log.Debug(message: "unknow clip border")
            }
        case .ended:
            
            let size = scrollView.frame.size
            let scale = 1 / scrollView.zoomScale
            clippedOffset = scrollView.contentOffset
            resetShowBorderRect(with: size, scale: scale)
            
        default:
            currentMovingTag = 0
        }
        
    }

移动算法处理

  1. 在移动时会发现,移动左上时图片会跟着动,没有图片固定移动边框的效果(右下不会出现这样的问题)
  2. 在移动时还会出现,边框超出了图片的显示区域,这样很不好
  3. 以上两个问题可以通过scroll view的offset属性和zoom(to rect: CGRect, animated: Bool)方法来解决这个问题
  4. 根据移动位移量计算出offset 和显示区域
  5. 代码如下
    func updateShowRect(top: CGFloat, left: CGFloat, right: CGFloat, bottom: CGFloat) {
        
        if top >= bottom || left >= right {
            return
        }
        
        let contentSize = scrollView.contentSize
        let scale = scrollView.zoomScale
        let contentOffsetX = min(contentSize.width - right + left, max(0, scrollView.contentOffset.x + left - lastLeftLocation))
        let contentOffsetY = min(contentSize.height - bottom + top, max(0, scrollView.contentOffset.y + top - lastTopLocation))
        let scrollFrame = CGRect(x: left, y: top, width: right - left, height: bottom - top)
        
        var visibleX = contentOffsetX / scale
        var visibleY = contentOffsetY / scale
        var visibleWidth = scrollFrame.width / scale
        var visibleHeight = scrollFrame.height / scale
        
        if visibleHeight > contentSize.height / scale || visibleWidth > contentSize.width / scale {
            if contentSize.width / scrollFrame.width > contentSize.height / scrollFrame.height {
                visibleHeight = contentSize.height / scale
                let width = visibleHeight / scrollFrame.height * scrollFrame.width
                visibleX -= (width - visibleWidth) / 2
                visibleWidth = width
                
            } else {
                visibleWidth = contentSize.width / scale
                let height = visibleWidth / scrollFrame.width * scrollFrame.height
                visibleY -= (height - visibleHeight) / 2
                visibleHeight = height
            }
        }
        let visibleRect = CGRect(x: visibleX, y: visibleY, width: visibleWidth, height: visibleHeight)
        
        scrollView.frame = scrollFrame
        scrollView.contentOffset = CGPoint(x: contentOffsetX, y: contentOffsetY)
        scrollView.zoom(to: visibleRect, animated: false)
        lastTopLocation = top
        lastLeftLocation = left
        
        updateBorderLocation(top: top, left: left, right: right, bottom: bottom)
    }

移动完成后重置显示内容,裁剪区域最大化展示(屏幕的80%)

    private func resetShowBorderRect(with size: CGSize, scale: CGFloat) {
        
        let (clippedTop, clippedLeft, clippedRight, clippedBottom) = calculateMargin(with: size)

        updateBorderLocation(top: clippedTop, left: clippedLeft, right: clippedRight, bottom: clippedBottom)
        
        let x = clippedOffset.x * scale
        let y = clippedOffset.y * scale
        let w = size.width * scale
        let h = size.height * scale
        
        scrollView.frame = CGRect(x: clippedLeft, y: clippedTop, width: clippedRight - clippedLeft, height: clippedBottom - clippedTop)
        let minScale = max(scrollView.frame.size.width / scrollView.contentSize.width * scrollView.zoomScale,
                        scrollView.frame.size.height / scrollView.contentSize.height * scrollView.zoomScale)
        scrollView.minimumZoomScale = minScale
        scrollView.zoom(to: CGRect(x: x, y: y, width: w, height: h), animated: false)
    }

    func calculateMargin(with size: CGSize, scale: CGFloat = 0.8) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
        
        var width = view.bounds.width * scale
        var height = view.bounds.height * scale
        if size.width / size.height > width / height {
            height = width / size.width * size.height
        } else {
            width = height / size.height * size.width
        }
        let left = view.bounds.midX - width / 2
        let right = view.bounds.midX + width / 2
        let top = view.bounds.midY - height / 2
        let bottom = view.bounds.midY + height / 2
        
        return (top, left, right, bottom)
    }

最后裁剪图片

    @objc func finishClipAction() {
        
        let scale = editImage!.size.width / scrollView.contentSize.width
        
        let x = scrollView.contentOffset.x * scale
        let y = scrollView.contentOffset.y * scale
        let w = scrollView.frame.width * scale
        let h = scrollView.frame.height * scale
        
        let rect = CGRect(x: floor(x), y: floor(y), width: CGFloat(round(Double(w))), height: CGFloat(round(Double(h))))

        if let cgImage = editImage?.cgImage?.cropping(to: rect) {
            
            clippedOffset = CGPoint(x: x, y: y)
            clippedImage = UIImage(cgImage: cgImage)
            imgView.image = clippedImage
            
        } else {
            Log.Debug(message: "clip failed")
        }
    }

最后一个重要的问题就是在scrollview的区域外面不可以响应手势去滚动
解决办法时劫持手势的响应,使手势作用的scrollview上
手势响应机制是冲最上层的视图开始一层一层的判断,
系统默认是通过触发点是否在view的有效区域内,不在就返回空不响应
所以子类化实现一个scrollview 并重写一下方式返回自身

class ClipScrollView: UIScrollView {
    override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if let view = super.hitTest(point, with: event) {
            return view
        } else {
            return self
        }
    }
}

使用的这个方法的目的在于,可以使用scroll view的滚动,滚动惯性,边缘弹性等一些特性

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。