使用scroll view的特性(滚动,缩放)实现裁剪图片的交互设计实现
思路:
- scroll view的有效区域作为裁剪区域
- 移动scroll view 边角位置来控制裁剪区域
- 使用scroll view 的缩放和滚动来控制裁剪的内容
实现
- 定义8个view 分别代替scroll view 8个方向
- 8个view是裁剪边框的视觉效果展示
- 8个view 分别添加手势,移动时调整8个边框view和scroll view的frame使保持一致
- 手势代码如下
@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
}
}
移动算法处理
- 在移动时会发现,移动左上时图片会跟着动,没有图片固定移动边框的效果(右下不会出现这样的问题)
- 在移动时还会出现,边框超出了图片的显示区域,这样很不好
- 以上两个问题可以通过scroll view的offset属性和zoom(to rect: CGRect, animated: Bool)方法来解决这个问题
- 根据移动位移量计算出offset 和显示区域
- 代码如下
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的滚动,滚动惯性,边缘弹性等一些特性