1352 lines
52 KiB
Swift
1352 lines
52 KiB
Swift
//
|
||
// ZLClipImageViewController.swift
|
||
// ZLPhotoBrowser
|
||
//
|
||
// Created by long on 2020/8/27.
|
||
//
|
||
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
|
||
//
|
||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
// of this software and associated documentation files (the "Software"), to deal
|
||
// in the Software without restriction, including without limitation the rights
|
||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
// copies of the Software, and to permit persons to whom the Software is
|
||
// furnished to do so, subject to the following conditions:
|
||
//
|
||
// The above copyright notice and this permission notice shall be included in
|
||
// all copies or substantial portions of the Software.
|
||
//
|
||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
// THE SOFTWARE.
|
||
|
||
import UIKit
|
||
|
||
extension ZLClipImageViewController {
|
||
enum ClipPanEdge {
|
||
case none
|
||
case top
|
||
case bottom
|
||
case left
|
||
case right
|
||
case topLeft
|
||
case topRight
|
||
case bottomLeft
|
||
case bottomRight
|
||
}
|
||
}
|
||
|
||
class ZLClipImageViewController: UIViewController {
|
||
private static let bottomToolViewH: CGFloat = 90
|
||
|
||
private static let clipRatioItemSize = CGSize(width: 60, height: 70)
|
||
|
||
/// 取消裁剪时动画frame
|
||
private var cancelClipAnimateFrame: CGRect = .zero
|
||
|
||
private var viewDidAppearCount = 0
|
||
|
||
private let originalImage: UIImage
|
||
|
||
private let clipRatios: [ZLImageClipRatio]
|
||
|
||
private var editImage: UIImage
|
||
|
||
/// 初次进入界面时候,裁剪范围
|
||
private var editRect: CGRect
|
||
|
||
private lazy var mainScrollView: UIScrollView = {
|
||
let view = UIScrollView()
|
||
view.alwaysBounceVertical = true
|
||
view.alwaysBounceHorizontal = true
|
||
view.showsVerticalScrollIndicator = false
|
||
view.showsHorizontalScrollIndicator = false
|
||
if #available(iOS 11.0, *) {
|
||
view.contentInsetAdjustmentBehavior = .never
|
||
}
|
||
view.delegate = self
|
||
return view
|
||
}()
|
||
|
||
private lazy var containerView = UIView()
|
||
|
||
private lazy var imageView: UIImageView = {
|
||
let view = UIImageView()
|
||
view.image = editImage
|
||
view.contentMode = .scaleAspectFit
|
||
view.clipsToBounds = true
|
||
return view
|
||
}()
|
||
|
||
private lazy var shadowView: ZLClipShadowView = {
|
||
let view = ZLClipShadowView()
|
||
view.isUserInteractionEnabled = false
|
||
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
|
||
return view
|
||
}()
|
||
|
||
private lazy var overlayView: ZLClipOverlayView = {
|
||
let view = ZLClipOverlayView()
|
||
view.isUserInteractionEnabled = false
|
||
view.isCircle = selectedRatio.isCircle
|
||
return view
|
||
}()
|
||
|
||
private lazy var gridPanGes: UIPanGestureRecognizer = {
|
||
let pan = UIPanGestureRecognizer(target: self, action: #selector(gridGesPanAction(_:)))
|
||
pan.delegate = self
|
||
return pan
|
||
}()
|
||
|
||
private lazy var bottomToolView = UIView()
|
||
|
||
private lazy var bottomShadowLayer: CAGradientLayer = {
|
||
let layer = CAGradientLayer()
|
||
layer.colors = [
|
||
UIColor.black.withAlphaComponent(0.15).cgColor,
|
||
UIColor.black.withAlphaComponent(0.35).cgColor,
|
||
]
|
||
layer.locations = [0, 1]
|
||
return layer
|
||
}()
|
||
|
||
private lazy var bottomToolLineView: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = .zl.rgba(240, 240, 240)
|
||
return view
|
||
}()
|
||
|
||
private lazy var cancelBtn: ZLEnlargeButton = {
|
||
let btn = ZLEnlargeButton(type: .custom)
|
||
btn.setImage(.zl.getImage("zl_close"), for: .normal)
|
||
btn.adjustsImageWhenHighlighted = false
|
||
btn.enlargeInset = 20
|
||
btn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
|
||
return btn
|
||
}()
|
||
|
||
private lazy var revertBtn: ZLEnlargeButton = {
|
||
let btn = ZLEnlargeButton(type: .custom)
|
||
btn.setTitleColor(.white, for: .normal)
|
||
btn.setTitle(localLanguageTextValue(.revert), for: .normal)
|
||
btn.enlargeInset = 20
|
||
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
|
||
btn.addTarget(self, action: #selector(revertBtnClick), for: .touchUpInside)
|
||
return btn
|
||
}()
|
||
|
||
lazy var doneBtn: ZLEnlargeButton = {
|
||
let btn = ZLEnlargeButton(type: .custom)
|
||
btn.setImage(.zl.getImage("zl_right"), for: .normal)
|
||
btn.adjustsImageWhenHighlighted = false
|
||
btn.enlargeInset = 20
|
||
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
|
||
return btn
|
||
}()
|
||
|
||
private lazy var rotateBtn: ZLEnlargeButton = {
|
||
let btn = ZLEnlargeButton(type: .custom)
|
||
btn.setImage(.zl.getImage("zl_rotateimage"), for: .normal)
|
||
btn.adjustsImageWhenHighlighted = false
|
||
btn.enlargeInset = 20
|
||
btn.addTarget(self, action: #selector(rotateBtnClick), for: .touchUpInside)
|
||
return btn
|
||
}()
|
||
|
||
private lazy var clipRatioColView: UICollectionView = {
|
||
let layout = ZLCollectionViewFlowLayout()
|
||
layout.itemSize = ZLClipImageViewController.clipRatioItemSize
|
||
layout.scrollDirection = .horizontal
|
||
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20)
|
||
|
||
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||
view.delegate = self
|
||
view.dataSource = self
|
||
view.backgroundColor = .clear
|
||
view.isHidden = clipRatios.count <= 1
|
||
view.showsHorizontalScrollIndicator = false
|
||
ZLImageClipRatioCell.zl.register(view)
|
||
return view
|
||
}()
|
||
|
||
private var shouldLayout = true
|
||
|
||
private var panEdge: ZLClipImageViewController.ClipPanEdge = .none
|
||
|
||
private var beginPanPoint: CGPoint = .zero
|
||
|
||
private var clipBoxFrame: CGRect = .zero
|
||
|
||
private var clipOriginFrame: CGRect = .zero
|
||
|
||
private var isRotating = false
|
||
|
||
private var angle: CGFloat = 0
|
||
|
||
private var selectedRatio: ZLImageClipRatio {
|
||
didSet {
|
||
overlayView.isCircle = selectedRatio.isCircle
|
||
}
|
||
}
|
||
|
||
private var thumbnailImage: UIImage?
|
||
|
||
private lazy var maxClipFrame = calculateMaxClipFrame()
|
||
|
||
private var minClipSize = CGSize(width: 45, height: 45)
|
||
|
||
private var resetTimer: Timer?
|
||
|
||
var animate = true
|
||
/// 用作进入裁剪界面首次动画frame
|
||
var presentAnimateFrame: CGRect?
|
||
/// 用作进入裁剪界面首次动画和取消裁剪时动画的image
|
||
var presentAnimateImage: UIImage?
|
||
|
||
var dismissAnimateFromRect: CGRect = .zero
|
||
|
||
var dismissAnimateImage: UIImage?
|
||
|
||
/// 传回旋转角度,图片编辑区域的rect
|
||
var clipDoneBlock: ((CGFloat, CGRect, ZLImageClipRatio) -> Void)?
|
||
|
||
var cancelClipBlock: (() -> Void)?
|
||
|
||
override var prefersStatusBarHidden: Bool {
|
||
return true
|
||
}
|
||
|
||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||
return .portrait
|
||
}
|
||
|
||
deinit {
|
||
zl_debugPrint("ZLClipImageViewController deinit")
|
||
cleanTimer()
|
||
}
|
||
|
||
init(image: UIImage, editRect: CGRect?, angle: CGFloat = 0, selectRatio: ZLImageClipRatio?) {
|
||
originalImage = image
|
||
clipRatios = ZLPhotoConfiguration.default().editImageConfiguration.clipRatios
|
||
self.editRect = editRect ?? .zero
|
||
self.angle = angle
|
||
let angle = ((Int(angle) % 360) - 360) % 360
|
||
if angle == -90 {
|
||
editImage = image.zl.rotate(orientation: .left)
|
||
} else if angle == -180 {
|
||
editImage = image.zl.rotate(orientation: .down)
|
||
} else if angle == -270 {
|
||
editImage = image.zl.rotate(orientation: .right)
|
||
} else {
|
||
editImage = image
|
||
}
|
||
var firstEnter = false
|
||
if let selectRatio = selectRatio {
|
||
selectedRatio = selectRatio
|
||
} else {
|
||
firstEnter = true
|
||
selectedRatio = ZLPhotoConfiguration.default().editImageConfiguration.clipRatios.first!
|
||
}
|
||
super.init(nibName: nil, bundle: nil)
|
||
if firstEnter {
|
||
calculateClipRect()
|
||
}
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
|
||
setupUI()
|
||
generateThumbnailImage()
|
||
}
|
||
|
||
override func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
|
||
viewDidAppearCount += 1
|
||
if presentingViewController is ZLEditImageViewController {
|
||
transitioningDelegate = self
|
||
}
|
||
|
||
guard viewDidAppearCount == 1 else {
|
||
return
|
||
}
|
||
|
||
if let presentAnimateFrame = presentAnimateFrame,
|
||
let presentAnimateImage = presentAnimateImage {
|
||
let animateImageView = UIImageView(image: presentAnimateImage)
|
||
animateImageView.contentMode = .scaleAspectFill
|
||
animateImageView.clipsToBounds = true
|
||
animateImageView.frame = presentAnimateFrame
|
||
view.addSubview(animateImageView)
|
||
|
||
cancelClipAnimateFrame = clipBoxFrame
|
||
UIView.animate(withDuration: 0.25, animations: {
|
||
animateImageView.frame = self.clipBoxFrame
|
||
self.bottomToolView.alpha = 1
|
||
self.rotateBtn.alpha = 1
|
||
}) { _ in
|
||
UIView.animate(withDuration: 0.1, animations: {
|
||
self.mainScrollView.alpha = 1
|
||
self.overlayView.alpha = 1
|
||
}) { _ in
|
||
animateImageView.removeFromSuperview()
|
||
}
|
||
}
|
||
} else {
|
||
bottomToolView.alpha = 1
|
||
rotateBtn.alpha = 1
|
||
mainScrollView.alpha = 1
|
||
overlayView.alpha = 1
|
||
}
|
||
}
|
||
|
||
override func viewDidLayoutSubviews() {
|
||
super.viewDidLayoutSubviews()
|
||
|
||
guard shouldLayout else {
|
||
return
|
||
}
|
||
shouldLayout = false
|
||
|
||
mainScrollView.frame = view.bounds
|
||
shadowView.frame = view.bounds
|
||
|
||
layoutInitialImage()
|
||
|
||
bottomToolView.frame = CGRect(x: 0, y: view.bounds.height - ZLClipImageViewController.bottomToolViewH, width: view.bounds.width, height: ZLClipImageViewController.bottomToolViewH)
|
||
bottomShadowLayer.frame = bottomToolView.bounds
|
||
|
||
bottomToolLineView.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: 1 / UIScreen.main.scale)
|
||
let toolBtnH: CGFloat = 25
|
||
let toolBtnY = (ZLClipImageViewController.bottomToolViewH - toolBtnH) / 2 - 10
|
||
cancelBtn.frame = CGRect(x: 30, y: toolBtnY, width: toolBtnH, height: toolBtnH)
|
||
let revertBtnW = localLanguageTextValue(.revert).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: toolBtnH)).width + 20
|
||
revertBtn.frame = CGRect(x: (view.bounds.width - revertBtnW) / 2, y: toolBtnY, width: revertBtnW, height: toolBtnH)
|
||
doneBtn.frame = CGRect(x: view.bounds.width - 30 - toolBtnH, y: toolBtnY, width: toolBtnH, height: toolBtnH)
|
||
|
||
let ratioColViewY = bottomToolView.frame.minY - ZLClipImageViewController.clipRatioItemSize.height - 5
|
||
rotateBtn.frame = CGRect(x: 30, y: ratioColViewY + (ZLClipImageViewController.clipRatioItemSize.height - 25) / 2, width: 25, height: 25)
|
||
let ratioColViewX = rotateBtn.frame.maxX + 15
|
||
clipRatioColView.frame = CGRect(x: ratioColViewX, y: ratioColViewY, width: view.bounds.width - ratioColViewX, height: 70)
|
||
|
||
if clipRatios.count > 1, let index = clipRatios.firstIndex(where: { $0 == self.selectedRatio }) {
|
||
clipRatioColView.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
|
||
}
|
||
}
|
||
|
||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||
super.viewWillTransition(to: size, with: coordinator)
|
||
shouldLayout = true
|
||
maxClipFrame = calculateMaxClipFrame()
|
||
}
|
||
|
||
private func setupUI() {
|
||
view.backgroundColor = .black
|
||
|
||
view.addSubview(mainScrollView)
|
||
mainScrollView.addSubview(containerView)
|
||
containerView.addSubview(imageView)
|
||
view.addSubview(shadowView)
|
||
view.addSubview(overlayView)
|
||
|
||
view.addSubview(bottomToolView)
|
||
bottomToolView.layer.addSublayer(bottomShadowLayer)
|
||
bottomToolView.addSubview(bottomToolLineView)
|
||
bottomToolView.addSubview(cancelBtn)
|
||
bottomToolView.addSubview(revertBtn)
|
||
bottomToolView.addSubview(doneBtn)
|
||
|
||
view.addSubview(rotateBtn)
|
||
view.addSubview(clipRatioColView)
|
||
|
||
view.addGestureRecognizer(gridPanGes)
|
||
mainScrollView.panGestureRecognizer.require(toFail: gridPanGes)
|
||
|
||
mainScrollView.alpha = 0
|
||
overlayView.alpha = 0
|
||
bottomToolView.alpha = 0
|
||
rotateBtn.alpha = 0
|
||
}
|
||
|
||
private func generateThumbnailImage() {
|
||
let size: CGSize
|
||
let ratio = (editImage.size.width / editImage.size.height)
|
||
let fixLength: CGFloat = 100
|
||
if ratio >= 1 {
|
||
size = CGSize(width: fixLength * ratio, height: fixLength)
|
||
} else {
|
||
size = CGSize(width: fixLength, height: fixLength / ratio)
|
||
}
|
||
thumbnailImage = editImage.zl.resize_vI(size)
|
||
}
|
||
|
||
/// 计算最大裁剪范围
|
||
private func calculateMaxClipFrame() -> CGRect {
|
||
var insets = deviceSafeAreaInsets()
|
||
insets.top += 20
|
||
var rect = CGRect.zero
|
||
rect.origin.x = 15
|
||
rect.origin.y = insets.top
|
||
rect.size.width = UIScreen.main.bounds.width - 15 * 2
|
||
rect.size.height = UIScreen.main.bounds.height - insets.top - ZLClipImageViewController.bottomToolViewH - ZLClipImageViewController.clipRatioItemSize.height - 25
|
||
return rect
|
||
}
|
||
|
||
private func calculateClipRect() {
|
||
if selectedRatio.whRatio == 0 {
|
||
editRect = CGRect(origin: .zero, size: editImage.size)
|
||
} else {
|
||
let imageSize = editImage.size
|
||
let imageWHRatio = imageSize.width / imageSize.height
|
||
|
||
var w: CGFloat = 0, h: CGFloat = 0
|
||
if selectedRatio.whRatio >= imageWHRatio {
|
||
w = imageSize.width
|
||
h = w / selectedRatio.whRatio
|
||
} else {
|
||
h = imageSize.height
|
||
w = h * selectedRatio.whRatio
|
||
}
|
||
|
||
editRect = CGRect(x: (imageSize.width - w) / 2, y: (imageSize.height - h) / 2, width: w, height: h)
|
||
}
|
||
}
|
||
|
||
private func layoutInitialImage() {
|
||
mainScrollView.minimumZoomScale = 1
|
||
mainScrollView.maximumZoomScale = 1
|
||
mainScrollView.zoomScale = 1
|
||
|
||
let editSize = editRect.size
|
||
mainScrollView.contentSize = editSize
|
||
let maxClipRect = maxClipFrame
|
||
|
||
containerView.frame = CGRect(origin: .zero, size: editImage.size)
|
||
imageView.frame = containerView.bounds
|
||
|
||
// editRect比例,计算editRect所占frame
|
||
let editScale = min(maxClipRect.width / editSize.width, maxClipRect.height / editSize.height)
|
||
let scaledSize = CGSize(width: floor(editSize.width * editScale), height: floor(editSize.height * editScale))
|
||
|
||
var frame = CGRect.zero
|
||
frame.size = scaledSize
|
||
frame.origin.x = maxClipRect.minX + floor((maxClipRect.width - frame.width) / 2)
|
||
frame.origin.y = maxClipRect.minY + floor((maxClipRect.height - frame.height) / 2)
|
||
|
||
// 按照edit image进行计算最小缩放比例
|
||
let originalScale = min(maxClipRect.width / editImage.size.width, maxClipRect.height / editImage.size.height)
|
||
// 将 edit rect 相对 originalScale 进行缩放,缩放到图片未放大时候的clip rect
|
||
let scaleEditSize = CGSize(width: editRect.width * originalScale, height: editRect.height * originalScale)
|
||
// 计算缩放后的clip rect相对maxClipRect的比例
|
||
let clipRectZoomScale = min(maxClipRect.width / scaleEditSize.width, maxClipRect.height / scaleEditSize.height)
|
||
|
||
mainScrollView.minimumZoomScale = originalScale
|
||
mainScrollView.maximumZoomScale = 10
|
||
// 设置当前zoom scale
|
||
let zoomScale = clipRectZoomScale * originalScale
|
||
mainScrollView.zoomScale = zoomScale
|
||
mainScrollView.contentSize = CGSize(width: editImage.size.width * zoomScale, height: editImage.size.height * zoomScale)
|
||
|
||
changeClipBoxFrame(newFrame: frame)
|
||
|
||
if (frame.size.width < scaledSize.width - CGFloat.ulpOfOne) || (frame.size.height < scaledSize.height - CGFloat.ulpOfOne) {
|
||
var offset = CGPoint.zero
|
||
offset.x = -floor((mainScrollView.frame.width - scaledSize.width) / 2)
|
||
offset.y = -floor((mainScrollView.frame.height - scaledSize.height) / 2)
|
||
mainScrollView.contentOffset = offset
|
||
}
|
||
|
||
// edit rect 相对 image size 的 偏移量
|
||
let diffX = editRect.origin.x / editImage.size.width * mainScrollView.contentSize.width
|
||
let diffY = editRect.origin.y / editImage.size.height * mainScrollView.contentSize.height
|
||
mainScrollView.contentOffset = CGPoint(x: -mainScrollView.contentInset.left + diffX, y: -mainScrollView.contentInset.top + diffY)
|
||
}
|
||
|
||
private func changeClipBoxFrame(newFrame: CGRect) {
|
||
guard clipBoxFrame != newFrame else {
|
||
return
|
||
}
|
||
if newFrame.width < CGFloat.ulpOfOne || newFrame.height < CGFloat.ulpOfOne {
|
||
return
|
||
}
|
||
var frame = newFrame
|
||
let originX = ceil(maxClipFrame.minX)
|
||
let diffX = frame.minX - originX
|
||
frame.origin.x = max(frame.minX, originX)
|
||
// frame.origin.x = floor(max(frame.minX, originX))
|
||
if diffX < -CGFloat.ulpOfOne {
|
||
frame.size.width += diffX
|
||
}
|
||
let originY = ceil(maxClipFrame.minY)
|
||
let diffY = frame.minY - originY
|
||
frame.origin.y = max(frame.minY, originY)
|
||
// frame.origin.y = floor(max(frame.minY, originY))
|
||
if diffY < -CGFloat.ulpOfOne {
|
||
frame.size.height += diffY
|
||
}
|
||
let maxW = maxClipFrame.width + maxClipFrame.minX - frame.minX
|
||
frame.size.width = max(minClipSize.width, min(frame.width, maxW))
|
||
// frame.size.width = floor(max(self.minClipSize.width, min(frame.width, maxW)))
|
||
|
||
let maxH = maxClipFrame.height + maxClipFrame.minY - frame.minY
|
||
frame.size.height = max(minClipSize.height, min(frame.height, maxH))
|
||
// frame.size.height = floor(max(self.minClipSize.height, min(frame.height, maxH)))
|
||
|
||
clipBoxFrame = frame
|
||
shadowView.clearRect = frame
|
||
overlayView.frame = frame.insetBy(dx: -ZLClipOverlayView.cornerLineWidth, dy: -ZLClipOverlayView.cornerLineWidth)
|
||
|
||
mainScrollView.contentInset = UIEdgeInsets(top: frame.minY, left: frame.minX, bottom: mainScrollView.frame.maxY - frame.maxY, right: mainScrollView.frame.maxX - frame.maxX)
|
||
|
||
let scale = max(frame.height / editImage.size.height, frame.width / editImage.size.width)
|
||
mainScrollView.minimumZoomScale = scale
|
||
|
||
// var size = self.mainScrollView.contentSize
|
||
// size.width = floor(size.width)
|
||
// size.height = floor(size.height)
|
||
// self.mainScrollView.contentSize = size
|
||
|
||
mainScrollView.zoomScale = mainScrollView.zoomScale
|
||
}
|
||
|
||
@objc private func cancelBtnClick() {
|
||
dismissAnimateFromRect = cancelClipAnimateFrame
|
||
dismissAnimateImage = presentAnimateImage
|
||
cancelClipBlock?()
|
||
dismiss(animated: animate, completion: nil)
|
||
}
|
||
|
||
@objc private func revertBtnClick() {
|
||
angle = 0
|
||
editImage = originalImage
|
||
calculateClipRect()
|
||
imageView.image = editImage
|
||
layoutInitialImage()
|
||
|
||
generateThumbnailImage()
|
||
clipRatioColView.reloadData()
|
||
}
|
||
|
||
@objc private func doneBtnClick() {
|
||
let image = clipImage()
|
||
dismissAnimateFromRect = clipBoxFrame
|
||
dismissAnimateImage = image.clipImage
|
||
if presentingViewController is ZLCustomCamera {
|
||
dismiss(animated: animate) {
|
||
self.clipDoneBlock?(self.angle, image.editRect, self.selectedRatio)
|
||
}
|
||
} else {
|
||
clipDoneBlock?(angle, image.editRect, selectedRatio)
|
||
dismiss(animated: animate, completion: nil)
|
||
}
|
||
}
|
||
|
||
@objc private func rotateBtnClick() {
|
||
guard !isRotating else {
|
||
return
|
||
}
|
||
angle -= 90
|
||
if angle == -360 {
|
||
angle = 0
|
||
}
|
||
|
||
isRotating = true
|
||
|
||
let animateImageView = UIImageView(image: editImage)
|
||
animateImageView.contentMode = .scaleAspectFit
|
||
animateImageView.clipsToBounds = true
|
||
let originFrame = view.convert(containerView.frame, from: mainScrollView)
|
||
animateImageView.frame = originFrame
|
||
view.addSubview(animateImageView)
|
||
|
||
if selectedRatio.whRatio == 0 || selectedRatio.whRatio == 1 {
|
||
// 自由比例和1:1比例,进行edit rect转换
|
||
|
||
// 将edit rect转换为相对edit image的rect
|
||
let rect = convertClipRectToEditImageRect()
|
||
// 旋转图片
|
||
editImage = editImage.zl.rotate(orientation: .left)
|
||
// 将rect进行旋转,转换到相对于旋转后的edit image的rect
|
||
editRect = CGRect(x: rect.minY, y: editImage.size.height - rect.minX - rect.width, width: rect.height, height: rect.width)
|
||
// 向右旋转可用下面这行代码
|
||
// editRect = CGRect(x: editImage.size.width - rect.maxY, y: rect.minX, width: rect.height, height: rect.width)
|
||
} else {
|
||
// 其他比例的裁剪框,旋转后都重置edit rect
|
||
|
||
// 旋转图片
|
||
editImage = editImage.zl.rotate(orientation: .left)
|
||
calculateClipRect()
|
||
}
|
||
|
||
imageView.image = editImage
|
||
layoutInitialImage()
|
||
|
||
let toFrame = view.convert(containerView.frame, from: mainScrollView)
|
||
let transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
|
||
overlayView.alpha = 0
|
||
containerView.alpha = 0
|
||
UIView.animate(withDuration: 0.3, animations: {
|
||
animateImageView.transform = transform
|
||
animateImageView.frame = toFrame
|
||
}) { _ in
|
||
animateImageView.removeFromSuperview()
|
||
self.overlayView.alpha = 1
|
||
self.containerView.alpha = 1
|
||
self.isRotating = false
|
||
}
|
||
|
||
generateThumbnailImage()
|
||
clipRatioColView.reloadData()
|
||
}
|
||
|
||
@objc private func gridGesPanAction(_ pan: UIPanGestureRecognizer) {
|
||
let point = pan.location(in: view)
|
||
if pan.state == .began {
|
||
startEditing()
|
||
beginPanPoint = point
|
||
clipOriginFrame = clipBoxFrame
|
||
panEdge = calculatePanEdge(at: point)
|
||
} else if pan.state == .changed {
|
||
guard panEdge != .none else {
|
||
return
|
||
}
|
||
updateClipBoxFrame(point: point)
|
||
} else if pan.state == .cancelled || pan.state == .ended {
|
||
panEdge = .none
|
||
startTimer()
|
||
}
|
||
}
|
||
|
||
private func calculatePanEdge(at point: CGPoint) -> ZLClipImageViewController.ClipPanEdge {
|
||
let frame = clipBoxFrame.insetBy(dx: -30, dy: -30)
|
||
|
||
let cornerSize = CGSize(width: 60, height: 60)
|
||
let topLeftRect = CGRect(origin: frame.origin, size: cornerSize)
|
||
if topLeftRect.contains(point) {
|
||
return .topLeft
|
||
}
|
||
|
||
let topRightRect = CGRect(origin: CGPoint(x: frame.maxX - cornerSize.width, y: frame.minY), size: cornerSize)
|
||
if topRightRect.contains(point) {
|
||
return .topRight
|
||
}
|
||
|
||
let bottomLeftRect = CGRect(origin: CGPoint(x: frame.minX, y: frame.maxY - cornerSize.height), size: cornerSize)
|
||
if bottomLeftRect.contains(point) {
|
||
return .bottomLeft
|
||
}
|
||
|
||
let bottomRightRect = CGRect(origin: CGPoint(x: frame.maxX - cornerSize.width, y: frame.maxY - cornerSize.height), size: cornerSize)
|
||
if bottomRightRect.contains(point) {
|
||
return .bottomRight
|
||
}
|
||
|
||
let topRect = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: cornerSize.height))
|
||
if topRect.contains(point) {
|
||
return .top
|
||
}
|
||
|
||
let bottomRect = CGRect(origin: CGPoint(x: frame.minX, y: frame.maxY - cornerSize.height), size: CGSize(width: frame.width, height: cornerSize.height))
|
||
if bottomRect.contains(point) {
|
||
return .bottom
|
||
}
|
||
|
||
let leftRect = CGRect(origin: frame.origin, size: CGSize(width: cornerSize.width, height: frame.height))
|
||
if leftRect.contains(point) {
|
||
return .left
|
||
}
|
||
|
||
let rightRect = CGRect(origin: CGPoint(x: frame.maxX - cornerSize.width, y: frame.minY), size: CGSize(width: cornerSize.width, height: frame.height))
|
||
if rightRect.contains(point) {
|
||
return .right
|
||
}
|
||
|
||
return .none
|
||
}
|
||
|
||
private func updateClipBoxFrame(point: CGPoint) {
|
||
var frame = clipBoxFrame
|
||
let originFrame = clipOriginFrame
|
||
|
||
var newPoint = point
|
||
newPoint.x = max(maxClipFrame.minX, newPoint.x)
|
||
newPoint.y = max(maxClipFrame.minY, newPoint.y)
|
||
|
||
let diffX = ceil(newPoint.x - beginPanPoint.x)
|
||
let diffY = ceil(newPoint.y - beginPanPoint.y)
|
||
let ratio = selectedRatio.whRatio
|
||
|
||
switch panEdge {
|
||
case .left:
|
||
frame.origin.x = originFrame.minX + diffX
|
||
frame.size.width = originFrame.width - diffX
|
||
if ratio != 0 {
|
||
frame.size.height = originFrame.height - diffX / ratio
|
||
}
|
||
case .right:
|
||
frame.size.width = originFrame.width + diffX
|
||
if ratio != 0 {
|
||
frame.size.height = originFrame.height + diffX / ratio
|
||
}
|
||
case .top:
|
||
frame.origin.y = originFrame.minY + diffY
|
||
frame.size.height = originFrame.height - diffY
|
||
if ratio != 0 {
|
||
frame.size.width = originFrame.width - diffY * ratio
|
||
}
|
||
case .bottom:
|
||
frame.size.height = originFrame.height + diffY
|
||
if ratio != 0 {
|
||
frame.size.width = originFrame.width + diffY * ratio
|
||
}
|
||
case .topLeft:
|
||
if ratio != 0 {
|
||
// if abs(diffX / ratio) >= abs(diffY) {
|
||
frame.origin.x = originFrame.minX + diffX
|
||
frame.size.width = originFrame.width - diffX
|
||
frame.origin.y = originFrame.minY + diffX / ratio
|
||
frame.size.height = originFrame.height - diffX / ratio
|
||
// } else {
|
||
// frame.origin.y = originFrame.minY + diffY
|
||
// frame.size.height = originFrame.height - diffY
|
||
// frame.origin.x = originFrame.minX + diffY * ratio
|
||
// frame.size.width = originFrame.width - diffY * ratio
|
||
// }
|
||
} else {
|
||
frame.origin.x = originFrame.minX + diffX
|
||
frame.size.width = originFrame.width - diffX
|
||
frame.origin.y = originFrame.minY + diffY
|
||
frame.size.height = originFrame.height - diffY
|
||
}
|
||
case .topRight:
|
||
if ratio != 0 {
|
||
// if abs(diffX / ratio) >= abs(diffY) {
|
||
frame.size.width = originFrame.width + diffX
|
||
frame.origin.y = originFrame.minY - diffX / ratio
|
||
frame.size.height = originFrame.height + diffX / ratio
|
||
// } else {
|
||
// frame.origin.y = originFrame.minY + diffY
|
||
// frame.size.height = originFrame.height - diffY
|
||
// frame.size.width = originFrame.width - diffY * ratio
|
||
// }
|
||
} else {
|
||
frame.size.width = originFrame.width + diffX
|
||
frame.origin.y = originFrame.minY + diffY
|
||
frame.size.height = originFrame.height - diffY
|
||
}
|
||
case .bottomLeft:
|
||
if ratio != 0 {
|
||
// if abs(diffX / ratio) >= abs(diffY) {
|
||
frame.origin.x = originFrame.minX + diffX
|
||
frame.size.width = originFrame.width - diffX
|
||
frame.size.height = originFrame.height - diffX / ratio
|
||
// } else {
|
||
// frame.origin.x = originFrame.minX - diffY * ratio
|
||
// frame.size.width = originFrame.width + diffY * ratio
|
||
// frame.size.height = originFrame.height + diffY
|
||
// }
|
||
} else {
|
||
frame.origin.x = originFrame.minX + diffX
|
||
frame.size.width = originFrame.width - diffX
|
||
frame.size.height = originFrame.height + diffY
|
||
}
|
||
case .bottomRight:
|
||
if ratio != 0 {
|
||
// if abs(diffX / ratio) >= abs(diffY) {
|
||
frame.size.width = originFrame.width + diffX
|
||
frame.size.height = originFrame.height + diffX / ratio
|
||
// } else {
|
||
// frame.size.width += diffY * ratio
|
||
// frame.size.height += diffY
|
||
// }
|
||
} else {
|
||
frame.size.width = originFrame.width + diffX
|
||
frame.size.height = originFrame.height + diffY
|
||
}
|
||
default:
|
||
break
|
||
}
|
||
|
||
let minSize: CGSize
|
||
let maxSize: CGSize
|
||
let maxClipFrame: CGRect
|
||
if ratio != 0 {
|
||
if ratio >= 1 {
|
||
minSize = CGSize(width: minClipSize.height * ratio, height: minClipSize.height)
|
||
} else {
|
||
minSize = CGSize(width: minClipSize.width, height: minClipSize.width / ratio)
|
||
}
|
||
if ratio > self.maxClipFrame.width / self.maxClipFrame.height {
|
||
maxSize = CGSize(width: self.maxClipFrame.width, height: self.maxClipFrame.width / ratio)
|
||
} else {
|
||
maxSize = CGSize(width: self.maxClipFrame.height * ratio, height: self.maxClipFrame.height)
|
||
}
|
||
maxClipFrame = CGRect(origin: CGPoint(x: self.maxClipFrame.minX + (self.maxClipFrame.width - maxSize.width) / 2, y: self.maxClipFrame.minY + (self.maxClipFrame.height - maxSize.height) / 2), size: maxSize)
|
||
} else {
|
||
minSize = minClipSize
|
||
maxSize = self.maxClipFrame.size
|
||
maxClipFrame = self.maxClipFrame
|
||
}
|
||
|
||
frame.size.width = min(maxSize.width, max(minSize.width, frame.size.width))
|
||
frame.size.height = min(maxSize.height, max(minSize.height, frame.size.height))
|
||
|
||
frame.origin.x = min(maxClipFrame.maxX - minSize.width, max(frame.origin.x, maxClipFrame.minX))
|
||
frame.origin.y = min(maxClipFrame.maxY - minSize.height, max(frame.origin.y, maxClipFrame.minY))
|
||
|
||
if panEdge == .topLeft || panEdge == .bottomLeft || panEdge == .left, frame.size.width <= minSize.width + CGFloat.ulpOfOne {
|
||
frame.origin.x = originFrame.maxX - minSize.width
|
||
}
|
||
if panEdge == .topLeft || panEdge == .topRight || panEdge == .top, frame.size.height <= minSize.height + CGFloat.ulpOfOne {
|
||
frame.origin.y = originFrame.maxY - minSize.height
|
||
}
|
||
|
||
changeClipBoxFrame(newFrame: frame)
|
||
}
|
||
|
||
private func startEditing() {
|
||
cleanTimer()
|
||
shadowView.alpha = 0
|
||
overlayView.isEditing = true
|
||
if rotateBtn.alpha != 0 {
|
||
rotateBtn.layer.removeAllAnimations()
|
||
clipRatioColView.layer.removeAllAnimations()
|
||
UIView.animate(withDuration: 0.2) {
|
||
self.rotateBtn.alpha = 0
|
||
self.clipRatioColView.alpha = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
@objc private func endEditing() {
|
||
overlayView.isEditing = false
|
||
moveClipContentToCenter()
|
||
}
|
||
|
||
private func startTimer() {
|
||
cleanTimer()
|
||
// TODO: 换target写法
|
||
resetTimer = Timer.scheduledTimer(timeInterval: 0.8, target: ZLWeakProxy(target: self), selector: #selector(endEditing), userInfo: nil, repeats: false)
|
||
RunLoop.current.add(resetTimer!, forMode: .common)
|
||
}
|
||
|
||
private func cleanTimer() {
|
||
resetTimer?.invalidate()
|
||
resetTimer = nil
|
||
}
|
||
|
||
private func moveClipContentToCenter() {
|
||
let maxClipRect = maxClipFrame
|
||
var clipRect = clipBoxFrame
|
||
|
||
if clipRect.width < CGFloat.ulpOfOne || clipRect.height < CGFloat.ulpOfOne {
|
||
return
|
||
}
|
||
|
||
let scale = min(maxClipRect.width / clipRect.width, maxClipRect.height / clipRect.height)
|
||
|
||
let focusPoint = CGPoint(x: clipRect.midX, y: clipRect.midY)
|
||
let midPoint = CGPoint(x: maxClipRect.midX, y: maxClipRect.midY)
|
||
|
||
clipRect.size.width = ceil(clipRect.width * scale)
|
||
clipRect.size.height = ceil(clipRect.height * scale)
|
||
clipRect.origin.x = maxClipRect.minX + ceil((maxClipRect.width - clipRect.width) / 2)
|
||
clipRect.origin.y = maxClipRect.minY + ceil((maxClipRect.height - clipRect.height) / 2)
|
||
|
||
var contentTargetPoint = CGPoint.zero
|
||
contentTargetPoint.x = (focusPoint.x + mainScrollView.contentOffset.x) * scale
|
||
contentTargetPoint.y = (focusPoint.y + mainScrollView.contentOffset.y) * scale
|
||
|
||
var offset = CGPoint(x: contentTargetPoint.x - midPoint.x, y: contentTargetPoint.y - midPoint.y)
|
||
offset.x = max(-clipRect.minX, offset.x)
|
||
offset.y = max(-clipRect.minY, offset.y)
|
||
UIView.animate(withDuration: 0.3) {
|
||
if scale < 1 - CGFloat.ulpOfOne || scale > 1 + CGFloat.ulpOfOne {
|
||
self.mainScrollView.zoomScale *= scale
|
||
self.mainScrollView.zoomScale = min(self.mainScrollView.maximumZoomScale, self.mainScrollView.zoomScale)
|
||
}
|
||
|
||
if self.mainScrollView.zoomScale < self.mainScrollView.maximumZoomScale - CGFloat.ulpOfOne {
|
||
offset.x = min(self.mainScrollView.contentSize.width - clipRect.maxX, offset.x)
|
||
offset.y = min(self.mainScrollView.contentSize.height - clipRect.maxY, offset.y)
|
||
self.mainScrollView.contentOffset = offset
|
||
}
|
||
self.rotateBtn.alpha = 1
|
||
self.clipRatioColView.alpha = 1
|
||
self.shadowView.alpha = 1
|
||
self.changeClipBoxFrame(newFrame: clipRect)
|
||
}
|
||
}
|
||
|
||
private func clipImage() -> (clipImage: UIImage, editRect: CGRect) {
|
||
let frame = convertClipRectToEditImageRect()
|
||
let clipImage = editImage.zl.clipImage(angle: 0, editRect: frame, isCircle: selectedRatio.isCircle) ?? editImage
|
||
return (clipImage, frame)
|
||
}
|
||
|
||
private func convertClipRectToEditImageRect() -> CGRect {
|
||
let imageSize = editImage.size
|
||
let contentSize = mainScrollView.contentSize
|
||
let offset = mainScrollView.contentOffset
|
||
let insets = mainScrollView.contentInset
|
||
|
||
var frame = CGRect.zero
|
||
frame.origin.x = floor((offset.x + insets.left) * (imageSize.width / contentSize.width))
|
||
frame.origin.x = max(0, frame.origin.x)
|
||
|
||
frame.origin.y = floor((offset.y + insets.top) * (imageSize.height / contentSize.height))
|
||
frame.origin.y = max(0, frame.origin.y)
|
||
|
||
frame.size.width = ceil(clipBoxFrame.width * (imageSize.width / contentSize.width))
|
||
frame.size.width = min(imageSize.width, frame.width)
|
||
|
||
frame.size.height = ceil(clipBoxFrame.height * (imageSize.height / contentSize.height))
|
||
frame.size.height = min(imageSize.height, frame.height)
|
||
|
||
return frame
|
||
}
|
||
}
|
||
|
||
extension ZLClipImageViewController: UIGestureRecognizerDelegate {
|
||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||
guard gestureRecognizer == gridPanGes else {
|
||
return true
|
||
}
|
||
let point = gestureRecognizer.location(in: view)
|
||
let frame = overlayView.frame
|
||
let innerFrame = frame.insetBy(dx: 22, dy: 22)
|
||
let outerFrame = frame.insetBy(dx: -22, dy: -22)
|
||
|
||
if innerFrame.contains(point) || !outerFrame.contains(point) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
}
|
||
|
||
extension ZLClipImageViewController: UICollectionViewDataSource, UICollectionViewDelegate {
|
||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||
return clipRatios.count
|
||
}
|
||
|
||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLImageClipRatioCell.zl.identifier, for: indexPath) as! ZLImageClipRatioCell
|
||
|
||
let ratio = clipRatios[indexPath.row]
|
||
cell.configureCell(image: thumbnailImage ?? editImage, ratio: ratio)
|
||
|
||
if ratio == selectedRatio {
|
||
cell.titleLabel.textColor = .zl.imageEditorToolTitleTintColor
|
||
} else {
|
||
cell.titleLabel.textColor = .zl.imageEditorToolTitleNormalColor
|
||
}
|
||
|
||
return cell
|
||
}
|
||
|
||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||
let ratio = clipRatios[indexPath.row]
|
||
guard ratio != selectedRatio else {
|
||
return
|
||
}
|
||
selectedRatio = ratio
|
||
clipRatioColView.reloadData()
|
||
clipRatioColView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
||
calculateClipRect()
|
||
layoutInitialImage()
|
||
}
|
||
}
|
||
|
||
extension ZLClipImageViewController: UIScrollViewDelegate {
|
||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||
return containerView
|
||
}
|
||
|
||
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||
startEditing()
|
||
}
|
||
|
||
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||
guard scrollView == mainScrollView else {
|
||
return
|
||
}
|
||
if !scrollView.isDragging {
|
||
startTimer()
|
||
}
|
||
}
|
||
|
||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||
guard scrollView == mainScrollView else {
|
||
return
|
||
}
|
||
startEditing()
|
||
}
|
||
|
||
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||
guard scrollView == mainScrollView else {
|
||
return
|
||
}
|
||
startTimer()
|
||
}
|
||
|
||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||
guard scrollView == mainScrollView else {
|
||
return
|
||
}
|
||
if !decelerate {
|
||
startTimer()
|
||
}
|
||
}
|
||
}
|
||
|
||
extension ZLClipImageViewController: UIViewControllerTransitioningDelegate {
|
||
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||
return ZLClipImageDismissAnimatedTransition()
|
||
}
|
||
}
|
||
|
||
// MARK: 裁剪比例cell
|
||
|
||
class ZLImageClipRatioCell: UICollectionViewCell {
|
||
private lazy var imageView: UIImageView = {
|
||
let view = UIImageView(frame: CGRect(x: 8, y: 5, width: bounds.width - 16, height: bounds.width - 16))
|
||
view.contentMode = .scaleAspectFill
|
||
view.layer.cornerRadius = 3
|
||
view.layer.masksToBounds = true
|
||
view.clipsToBounds = true
|
||
return view
|
||
}()
|
||
|
||
lazy var titleLabel: UILabel = {
|
||
let label = UILabel(frame: CGRect(x: 0, y: bounds.height - 15, width: bounds.width, height: 12))
|
||
label.font = .zl.font(ofSize: 12)
|
||
label.textColor = .white
|
||
label.textAlignment = .center
|
||
label.layer.shadowColor = UIColor.black.withAlphaComponent(0.3).cgColor
|
||
label.layer.shadowOffset = .zero
|
||
label.layer.shadowOpacity = 1
|
||
return label
|
||
}()
|
||
|
||
var image: UIImage?
|
||
|
||
var ratio: ZLImageClipRatio!
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setupUI()
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
guard let ratio = ratio, let image = image else {
|
||
return
|
||
}
|
||
|
||
let center = imageView.center
|
||
var w: CGFloat = 0, h: CGFloat = 0
|
||
|
||
let imageMaxW = bounds.width - 10
|
||
if ratio.whRatio == 0 {
|
||
let maxSide = max(image.size.width, image.size.height)
|
||
w = imageMaxW * image.size.width / maxSide
|
||
h = imageMaxW * image.size.height / maxSide
|
||
} else {
|
||
if ratio.whRatio >= 1 {
|
||
w = imageMaxW
|
||
h = w / ratio.whRatio
|
||
} else {
|
||
h = imageMaxW
|
||
w = h * ratio.whRatio
|
||
}
|
||
}
|
||
if ratio.isCircle {
|
||
imageView.layer.cornerRadius = w / 2
|
||
} else {
|
||
imageView.layer.cornerRadius = 3
|
||
}
|
||
imageView.frame = CGRect(x: center.x - w / 2, y: center.y - h / 2, width: w, height: h)
|
||
}
|
||
|
||
func setupUI() {
|
||
contentView.addSubview(imageView)
|
||
contentView.addSubview(titleLabel)
|
||
}
|
||
|
||
func configureCell(image: UIImage, ratio: ZLImageClipRatio) {
|
||
imageView.image = image
|
||
titleLabel.text = ratio.title
|
||
self.image = image
|
||
self.ratio = ratio
|
||
|
||
setNeedsLayout()
|
||
}
|
||
}
|
||
|
||
class ZLClipShadowView: UIView {
|
||
var clearRect: CGRect = .zero {
|
||
didSet {
|
||
self.setNeedsDisplay()
|
||
}
|
||
}
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
isOpaque = false
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func draw(_ rect: CGRect) {
|
||
UIColor(white: 0, alpha: 0.7).setFill()
|
||
UIRectFill(rect)
|
||
let cr = clearRect.intersection(rect)
|
||
UIColor.clear.setFill()
|
||
UIRectFill(cr)
|
||
}
|
||
}
|
||
|
||
// MARK: 裁剪网格视图
|
||
|
||
class ZLClipOverlayView: UIView {
|
||
static let cornerLineWidth: CGFloat = 3
|
||
|
||
private var cornerBoldLines: [UIView] = []
|
||
|
||
private var velLines: [UIView] = []
|
||
|
||
private var horLines: [UIView] = []
|
||
|
||
var isCircle = false {
|
||
didSet {
|
||
guard oldValue != isCircle else {
|
||
return
|
||
}
|
||
setNeedsDisplay()
|
||
}
|
||
}
|
||
|
||
var isEditing = false {
|
||
didSet {
|
||
guard isCircle else {
|
||
return
|
||
}
|
||
setNeedsDisplay()
|
||
}
|
||
}
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
clipsToBounds = false
|
||
// 两种方法实现裁剪框,drawrect动画效果 更好一点
|
||
// func line(_ isCorner: Bool) -> UIView {
|
||
// let line = UIView()
|
||
// line.backgroundColor = .white
|
||
// line.layer.shadowColor = UIColor.black.cgColor
|
||
// if !isCorner {
|
||
// line.layer.shadowOffset = .zero
|
||
// line.layer.shadowRadius = 1.5
|
||
// line.layer.shadowOpacity = 0.8
|
||
// }
|
||
// self.addSubview(line)
|
||
// return line
|
||
// }
|
||
//
|
||
// (0..<8).forEach { (_) in
|
||
// self.cornerBoldLines.append(line(true))
|
||
// }
|
||
//
|
||
// (0..<4).forEach { (_) in
|
||
// self.velLines.append(line(false))
|
||
// self.horLines.append(line(false))
|
||
// }
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
setNeedsDisplay()
|
||
// let borderLineLength: CGFloat = 20
|
||
// let borderLineWidth: CGFloat = ZLClipOverlayView.cornerLineWidth
|
||
// for (i, line) in self.cornerBoldLines.enumerated() {
|
||
// switch i {
|
||
// case 0:
|
||
// // 左上 hor
|
||
// line.frame = CGRect(x: -borderLineWidth, y: -borderLineWidth, width: borderLineLength, height: borderLineWidth)
|
||
// case 1:
|
||
// // 左上 vel
|
||
// line.frame = CGRect(x: -borderLineWidth, y: -borderLineWidth, width: borderLineWidth, height: borderLineLength)
|
||
// case 2:
|
||
// // 右上 hor
|
||
// line.frame = CGRect(x: self.bounds.width-borderLineLength+borderLineWidth, y: -borderLineWidth, width: borderLineLength, height: borderLineWidth)
|
||
// case 3:
|
||
// // 右上 vel
|
||
// line.frame = CGRect(x: self.bounds.width, y: -borderLineWidth, width: borderLineWidth, height: borderLineLength)
|
||
// case 4:
|
||
// // 左下 hor
|
||
// line.frame = CGRect(x: -borderLineWidth, y: self.bounds.height, width: borderLineLength, height: borderLineWidth)
|
||
// case 5:
|
||
// // 左下 vel
|
||
// line.frame = CGRect(x: -borderLineWidth, y: self.bounds.height-borderLineLength+borderLineWidth, width: borderLineWidth, height: borderLineLength)
|
||
// case 6:
|
||
// // 右下 hor
|
||
// line.frame = CGRect(x: self.bounds.width-borderLineLength+borderLineWidth, y: self.bounds.height, width: borderLineLength, height: borderLineWidth)
|
||
// case 7:
|
||
// line.frame = CGRect(x: self.bounds.width, y: self.bounds.height-borderLineLength+borderLineWidth, width: borderLineWidth, height: borderLineLength)
|
||
//
|
||
// default:
|
||
// break
|
||
// }
|
||
// }
|
||
//
|
||
// let normalLineWidth: CGFloat = 1
|
||
// var x: CGFloat = 0
|
||
// var y: CGFloat = -1
|
||
// // 横线
|
||
// for (index, line) in self.horLines.enumerated() {
|
||
// if index == 0 || index == 3 {
|
||
// x = borderLineLength-borderLineWidth
|
||
// } else {
|
||
// x = 0
|
||
// }
|
||
// line.frame = CGRect(x: x, y: y, width: self.bounds.width - x * 2, height: normalLineWidth)
|
||
// y += (self.bounds.height + 1) / 3
|
||
// }
|
||
//
|
||
// x = -1
|
||
// y = 0
|
||
// // 竖线
|
||
// for (index, line) in self.velLines.enumerated() {
|
||
// if index == 0 || index == 3 {
|
||
// y = borderLineLength-borderLineWidth
|
||
// } else {
|
||
// y = 0
|
||
// }
|
||
// line.frame = CGRect(x: x, y: y, width: normalLineWidth, height: self.bounds.height - y * 2)
|
||
// x += (self.bounds.width + 1) / 3
|
||
// }
|
||
}
|
||
|
||
override func draw(_ rect: CGRect) {
|
||
let context = UIGraphicsGetCurrentContext()
|
||
|
||
context?.setStrokeColor(UIColor.white.cgColor)
|
||
context?.setLineWidth(1)
|
||
context?.beginPath()
|
||
|
||
if isCircle {
|
||
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
|
||
let radius = rect.width / 2 - ZLClipOverlayView.cornerLineWidth
|
||
if !isEditing {
|
||
// top left
|
||
context?.move(to: CGPoint(x: ZLClipOverlayView.cornerLineWidth, y: ZLClipOverlayView.cornerLineWidth))
|
||
context?.addLine(to: CGPoint(x: rect.width / 2, y: rect.origin.y + 3))
|
||
context?.addArc(center: center, radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: true)
|
||
context?.closePath()
|
||
|
||
// top right
|
||
context?.move(to: CGPoint(x: rect.width - ZLClipOverlayView.cornerLineWidth, y: ZLClipOverlayView.cornerLineWidth))
|
||
context?.addLine(to: CGPoint(x: rect.width - ZLClipOverlayView.cornerLineWidth, y: rect.height / 2))
|
||
context?.addArc(center: center, radius: radius, startAngle: 0, endAngle: .pi * 1.5, clockwise: true)
|
||
context?.closePath()
|
||
|
||
// bottom left
|
||
context?.move(to: CGPoint(x: ZLClipOverlayView.cornerLineWidth, y: rect.height - ZLClipOverlayView.cornerLineWidth))
|
||
context?.addLine(to: CGPoint(x: ZLClipOverlayView.cornerLineWidth, y: rect.height / 2))
|
||
context?.addArc(center: center, radius: radius, startAngle: .pi, endAngle: .pi / 2, clockwise: true)
|
||
context?.closePath()
|
||
|
||
// bottom right
|
||
context?.move(to: CGPoint(x: rect.width - ZLClipOverlayView.cornerLineWidth, y: rect.height - ZLClipOverlayView.cornerLineWidth))
|
||
context?.addLine(to: CGPoint(x: rect.width / 2, y: rect.height - ZLClipOverlayView.cornerLineWidth))
|
||
context?.addArc(center: center, radius: radius, startAngle: .pi / 2, endAngle: 0, clockwise: true)
|
||
context?.closePath()
|
||
|
||
context?.setFillColor(UIColor.black.withAlphaComponent(0.7).cgColor)
|
||
context?.fillPath()
|
||
}
|
||
|
||
context?.addArc(center: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: false)
|
||
}
|
||
|
||
let circleDiff: CGFloat = (3 - 2 * sqrt(2)) * (rect.width - 2 * ZLClipOverlayView.cornerLineWidth) / 6
|
||
|
||
var dw: CGFloat = 3
|
||
for i in 0..<4 {
|
||
let isInnerLine = isCircle && 1...2 ~= i
|
||
context?.move(to: CGPoint(x: rect.origin.x + dw, y: ZLClipOverlayView.cornerLineWidth + (isInnerLine ? circleDiff : 0)))
|
||
context?.addLine(to: CGPoint(x: rect.origin.x + dw, y: rect.height - ZLClipOverlayView.cornerLineWidth - (isInnerLine ? circleDiff : 0)))
|
||
dw += (rect.size.width - 6) / 3
|
||
}
|
||
|
||
var dh: CGFloat = 3
|
||
for i in 0..<4 {
|
||
let isInnerLine = isCircle && 1...2 ~= i
|
||
context?.move(to: CGPoint(x: ZLClipOverlayView.cornerLineWidth + (isInnerLine ? circleDiff : 0), y: rect.origin.y + dh))
|
||
context?.addLine(to: CGPoint(x: rect.width - ZLClipOverlayView.cornerLineWidth - (isInnerLine ? circleDiff : 0), y: rect.origin.y + dh))
|
||
dh += (rect.size.height - 6) / 3
|
||
}
|
||
|
||
context?.strokePath()
|
||
|
||
context?.setLineWidth(ZLClipOverlayView.cornerLineWidth)
|
||
|
||
let boldLineLength: CGFloat = 20
|
||
// 左上
|
||
context?.move(to: CGPoint(x: 0, y: 1.5))
|
||
context?.addLine(to: CGPoint(x: boldLineLength, y: 1.5))
|
||
|
||
context?.move(to: CGPoint(x: 1.5, y: 0))
|
||
context?.addLine(to: CGPoint(x: 1.5, y: boldLineLength))
|
||
|
||
// 右上
|
||
context?.move(to: CGPoint(x: rect.width - boldLineLength, y: 1.5))
|
||
context?.addLine(to: CGPoint(x: rect.width, y: 1.5))
|
||
|
||
context?.move(to: CGPoint(x: rect.width - 1.5, y: 0))
|
||
context?.addLine(to: CGPoint(x: rect.width - 1.5, y: boldLineLength))
|
||
|
||
// 左下
|
||
context?.move(to: CGPoint(x: 1.5, y: rect.height - boldLineLength))
|
||
context?.addLine(to: CGPoint(x: 1.5, y: rect.height))
|
||
|
||
context?.move(to: CGPoint(x: 0, y: rect.height - 1.5))
|
||
context?.addLine(to: CGPoint(x: boldLineLength, y: rect.height - 1.5))
|
||
|
||
// 右下
|
||
context?.move(to: CGPoint(x: rect.width - boldLineLength, y: rect.height - 1.5))
|
||
context?.addLine(to: CGPoint(x: rect.width, y: rect.height - 1.5))
|
||
|
||
context?.move(to: CGPoint(x: rect.width - 1.5, y: rect.height - boldLineLength))
|
||
context?.addLine(to: CGPoint(x: rect.width - 1.5, y: rect.height))
|
||
|
||
context?.strokePath()
|
||
|
||
context?.setShadow(offset: CGSize(width: 1, height: 1), blur: 0)
|
||
}
|
||
}
|