From ab8c48e2ddf3a336ca8ddc286a82364f69a4021a Mon Sep 17 00:00:00 2001 From: longitachi Date: Thu, 4 Jul 2024 11:12:48 +0800 Subject: [PATCH] Enhance the user experience of the image cropping interface and optimize the animation effects --- .../Example/FontChooserContainerView.swift | 1 - Example/Example/ViewController.swift | 1 + Sources/General/ZLAnimationUtils.swift | 55 ++ Sources/General/ZLBaseStickerView.swift | 6 +- .../General/ZLClipImageViewController.swift | 664 +++++++----------- Sources/General/ZLClipOverlayView.swift | 310 ++++++++ .../General/ZLEditImageViewController.swift | 23 +- .../General/ZLImageEditorConfiguration.swift | 3 + ZLImageEditor.xcodeproj/project.pbxproj | 8 + 9 files changed, 639 insertions(+), 432 deletions(-) create mode 100644 Sources/General/ZLAnimationUtils.swift create mode 100644 Sources/General/ZLClipOverlayView.swift diff --git a/Example/Example/FontChooserContainerView.swift b/Example/Example/FontChooserContainerView.swift index e4f70de..39b16fe 100644 --- a/Example/Example/FontChooserContainerView.swift +++ b/Example/Example/FontChooserContainerView.swift @@ -92,7 +92,6 @@ class FontChooserContainerView: UIView, ZLTextFontChooserDelegate { } } - func setupUI() { importFonts() self.baseView = UIView() diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift index 09274cf..8798135 100644 --- a/Example/Example/ViewController.swift +++ b/Example/Example/ViewController.swift @@ -222,6 +222,7 @@ class ViewController: UIViewController { // Provide a image sticker container view .imageStickerContainerView(ImageStickerContainerView()) .fontChooserContainerView(FontChooserContainerView()) + .clipRatios(ZLImageClipRatio.all) // Custom filter // .filters = [.normal] } diff --git a/Sources/General/ZLAnimationUtils.swift b/Sources/General/ZLAnimationUtils.swift new file mode 100644 index 0000000..b2b54f5 --- /dev/null +++ b/Sources/General/ZLAnimationUtils.swift @@ -0,0 +1,55 @@ +// +// ZLAnimationUtils.swift +// ZLImageEditor +// +// Created by long on 2023/1/13. +// +// 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 + +class ZLAnimationUtils: NSObject { + enum AnimationType: String { + case fade = "opacity" + case scale = "transform.scale" + case rotate = "transform.rotation" + case path + } + + class func animation( + type: ZLAnimationUtils.AnimationType, + fromValue: Any?, + toValue: Any?, + duration: TimeInterval, + fillMode: CAMediaTimingFillMode = .forwards, + isRemovedOnCompletion: Bool = false, + timingFunction: CAMediaTimingFunction? = nil + ) -> CAAnimation { + let animation = CABasicAnimation(keyPath: type.rawValue) + animation.fromValue = fromValue + animation.toValue = toValue + animation.duration = duration + animation.fillMode = fillMode + animation.isRemovedOnCompletion = isRemovedOnCompletion + animation.timingFunction = timingFunction + return animation + } +} diff --git a/Sources/General/ZLBaseStickerView.swift b/Sources/General/ZLBaseStickerView.swift index 21c2cfb..8a9ef9a 100644 --- a/Sources/General/ZLBaseStickerView.swift +++ b/Sources/General/ZLBaseStickerView.swift @@ -52,7 +52,7 @@ protocol ZLStickerViewAdditional: NSObject { func addScale(_ scale: CGFloat) } -public class ZLBaseStickerView: UIView, UIGestureRecognizerDelegate { +class ZLBaseStickerView: UIView, UIGestureRecognizerDelegate { private enum Direction: Int { case up = 0 case right = 90 @@ -171,7 +171,7 @@ public class ZLBaseStickerView: UIView, UIGestureRecognizerDelegate { fatalError("init(coder:) has not been implemented") } - override public func layoutSubviews() { + override func layoutSubviews() { super.layoutSubviews() guard firstLayout else { @@ -348,7 +348,7 @@ public class ZLBaseStickerView: UIView, UIGestureRecognizerDelegate { // MARK: UIGestureRecognizerDelegate - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } } diff --git a/Sources/General/ZLClipImageViewController.swift b/Sources/General/ZLClipImageViewController.swift index 4729909..174d8f6 100644 --- a/Sources/General/ZLClipImageViewController.swift +++ b/Sources/General/ZLClipImageViewController.swift @@ -45,14 +45,6 @@ class ZLClipImageViewController: UIViewController { static let clipRatioItemSize = CGSize(width: 60, height: 70) - var animateDismiss = true - - /// Animation starting frame when first enter - var presentAnimateFrame: CGRect? - - /// Animation image - var presentAnimateImage: UIImage? - /// Animation starting frame when cancel clip var cancelClipAnimateFrame: CGRect = .zero @@ -66,33 +58,125 @@ class ZLClipImageViewController: UIViewController { var editRect: CGRect - var scrollView: UIScrollView! - - var containerView: UIView! - - var imageView: UIImageView! - - var shadowView: ZLClipShadowView! - - var overlayView: ZLClipOverlayView! - - var gridPanGes: UIPanGestureRecognizer! - - var bottomToolView: UIView! - - var bottomShadowLayer: CAGradientLayer! - - var bottomToolLineView: UIView! - - lazy var cancelBtn = ZLEnlargeButton(type: .custom) - - lazy var revertBtn = ZLEnlargeButton(type: .custom) - - lazy var doneBtn = ZLEnlargeButton(type: .custom) - - lazy var rotateBtn = ZLEnlargeButton(type: .custom) - - var clipRatioColView: UICollectionView! + /// 初次进去界面时的动画占位view + private lazy var animateImageView: UIImageView? = { + guard let presentAnimateFrame, let presentAnimateImage else { + return nil + } + + let view = UIImageView(image: presentAnimateImage) + view.frame = presentAnimateFrame + view.contentMode = .scaleAspectFill + view.clipsToBounds = true + return view + }() + + lazy var scrollView: 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 + }() + + lazy var containerView = UIView() + + lazy var imageView: UIImageView = { + let view = UIImageView() + view.image = editImage + view.contentMode = .scaleAspectFit + view.clipsToBounds = true + return view + }() + + lazy var overlayView: ZLClipOverlayView = { + let view = ZLClipOverlayView(frame: view.frame) + view.isUserInteractionEnabled = false + view.isCircle = selectedRatio.isCircle + return view + }() + + lazy var gridPanGes: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer(target: self, action: #selector(gridGesPanAction(_:))) + pan.delegate = self + return pan + }() + + lazy var bottomToolView = UIView() + + 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 + }() + + lazy var bottomToolLineView: UIView = { + let view = UIView() + view.backgroundColor = .zl.rgba(240, 240, 240) + return view + }() + + 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 + }() + + 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 = ZLImageEditorLayout.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 + }() + + 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 + }() + + lazy var clipRatioColView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + 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.alpha = 0 + view.showsHorizontalScrollIndicator = false + ZLImageClipRatioCell.zl.register(view) + return view + }() var shouldLayout = true @@ -104,7 +188,7 @@ class ZLClipImageViewController: UIViewController { var clipOriginFrame: CGRect = .zero - var isRotating = false + var isAnimate = false var angle: CGFloat = 0 @@ -122,6 +206,16 @@ class ZLClipImageViewController: UIViewController { var resetTimer: Timer? + var showRatioColView: Bool { clipRatios.count > 1 } + + var animateDismiss = true + + /// Animation starting frame when first enter + var presentAnimateFrame: CGRect? + + /// Animation image + var presentAnimateImage: UIImage? + var dismissAnimateFromRect: CGRect = .zero var dismissAnimateImage: UIImage? @@ -135,6 +229,9 @@ class ZLClipImageViewController: UIViewController { override var prefersHomeIndicatorAutoHidden: Bool { true} + /// 延缓屏幕上下方通知栏弹出,避免手势冲突 + override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { [.top, .bottom] } + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { deviceIsiPhone() ? .portrait : .all } @@ -195,23 +292,18 @@ class ZLClipImageViewController: UIViewController { return } - if let frame = presentAnimateFrame, let image = presentAnimateImage { - let animateImageView = UIImageView(image: image) - animateImageView.contentMode = .scaleAspectFill - animateImageView.clipsToBounds = true - animateImageView.frame = frame - view.addSubview(animateImageView) - + if let animateImageView { cancelClipAnimateFrame = clipBoxFrame - UIView.animate(withDuration: 0.25, animations: { + UIView.animate(withDuration: 0.25) { animateImageView.frame = self.clipBoxFrame self.bottomToolView.alpha = 1 self.rotateBtn.alpha = 1 - }) { _ in - UIView.animate(withDuration: 0.1, animations: { + self.clipRatioColView.alpha = self.showRatioColView ? 1 : 0 + } completion: { _ in + UIView.animate(withDuration: 0.1) { self.scrollView.alpha = 1 self.overlayView.alpha = 1 - }) { _ in + } completion: { _ in animateImageView.removeFromSuperview() } } @@ -220,6 +312,7 @@ class ZLClipImageViewController: UIViewController { rotateBtn.alpha = 1 scrollView.alpha = 1 overlayView.alpha = 1 + clipRatioColView.alpha = clipRatios.count <= 1 ? 0 : 1 } } @@ -230,9 +323,8 @@ class ZLClipImageViewController: UIViewController { shouldLayout = false scrollView.frame = view.bounds - shadowView.frame = view.bounds - layoutInitialImage() + layoutInitialImage(animate: true) bottomToolView.frame = CGRect(x: 0, y: view.bounds.height - ZLClipImageViewController.bottomToolViewH, width: view.bounds.width, height: ZLClipImageViewController.bottomToolViewH) bottomShadowLayer.frame = bottomToolView.bounds @@ -250,7 +342,7 @@ class ZLClipImageViewController: UIViewController { 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 }) { + if showRatioColView, let index = clipRatios.firstIndex(where: { $0 == self.selectedRatio }) { clipRatioColView.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false) } } @@ -264,92 +356,25 @@ class ZLClipImageViewController: UIViewController { func setupUI() { view.backgroundColor = .black - scrollView = UIScrollView() - scrollView.alwaysBounceVertical = true - scrollView.alwaysBounceHorizontal = true - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - if #available(iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } else { - // Fallback on earlier versions - } - scrollView.delegate = self view.addSubview(scrollView) - - containerView = UIView() scrollView.addSubview(containerView) - - imageView = UIImageView(image: editImage) - imageView.contentMode = .scaleAspectFit - imageView.clipsToBounds = true containerView.addSubview(imageView) - - shadowView = ZLClipShadowView() - shadowView.isUserInteractionEnabled = false - shadowView.backgroundColor = UIColor.black.withAlphaComponent(0.3) - view.addSubview(shadowView) - - overlayView = ZLClipOverlayView() - overlayView.isUserInteractionEnabled = false - overlayView.isCircle = selectedRatio.isCircle view.addSubview(overlayView) - bottomToolView = UIView() view.addSubview(bottomToolView) - - let color1 = UIColor.black.withAlphaComponent(0.15).cgColor - let color2 = UIColor.black.withAlphaComponent(0.35).cgColor - - bottomShadowLayer = CAGradientLayer() - bottomShadowLayer.colors = [color1, color2] - bottomShadowLayer.locations = [0, 1] bottomToolView.layer.addSublayer(bottomShadowLayer) - - bottomToolLineView = UIView() - bottomToolLineView.backgroundColor = .zl.rgba(240, 240, 240) bottomToolView.addSubview(bottomToolLineView) - - cancelBtn.setImage(.zl.getImage("zl_close"), for: .normal) - cancelBtn.adjustsImageWhenHighlighted = false - cancelBtn.enlargeInset = 20 - cancelBtn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside) bottomToolView.addSubview(cancelBtn) - - revertBtn.setTitleColor(.white, for: .normal) - revertBtn.setTitle(localLanguageTextValue(.revert), for: .normal) - revertBtn.enlargeInset = 20 - revertBtn.titleLabel?.font = ZLImageEditorLayout.bottomToolTitleFont - revertBtn.addTarget(self, action: #selector(revertBtnClick), for: .touchUpInside) bottomToolView.addSubview(revertBtn) - - doneBtn.setImage(.zl.getImage("zl_right"), for: .normal) - doneBtn.adjustsImageWhenHighlighted = false - doneBtn.enlargeInset = 20 - doneBtn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside) bottomToolView.addSubview(doneBtn) - rotateBtn.setImage(.zl.getImage("zl_rotateimage"), for: .normal) - rotateBtn.adjustsImageWhenHighlighted = false - rotateBtn.enlargeInset = 20 - rotateBtn.addTarget(self, action: #selector(rotateBtnClick), for: .touchUpInside) view.addSubview(rotateBtn) - - let layout = UICollectionViewFlowLayout() - layout.itemSize = ZLClipImageViewController.clipRatioItemSize - layout.scrollDirection = .horizontal - layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20) - clipRatioColView = UICollectionView(frame: .zero, collectionViewLayout: layout) - clipRatioColView.delegate = self - clipRatioColView.dataSource = self - clipRatioColView.backgroundColor = .clear - clipRatioColView.isHidden = clipRatios.count <= 1 - clipRatioColView.showsHorizontalScrollIndicator = false view.addSubview(clipRatioColView) - ZLImageClipRatioCell.zl.register(clipRatioColView) - gridPanGes = UIPanGestureRecognizer(target: self, action: #selector(gridGesPanAction(_:))) - gridPanGes.delegate = self + if let animateImageView { + view.addSubview(animateImageView) + } + view.addGestureRecognizer(gridPanGes) scrollView.panGestureRecognizer.require(toFail: gridPanGes) @@ -403,7 +428,7 @@ class ZLClipImageViewController: UIViewController { } } - func layoutInitialImage() { + func layoutInitialImage(animate: Bool) { scrollView.minimumZoomScale = 1 scrollView.maximumZoomScale = 1 scrollView.zoomScale = 1 @@ -440,7 +465,7 @@ class ZLClipImageViewController: UIViewController { scrollView.zoomScale = zoomScale scrollView.contentSize = CGSize(width: editImage.size.width * zoomScale, height: editImage.size.height * zoomScale) - changeClipBoxFrame(newFrame: frame) + changeClipBoxFrame(newFrame: frame, animate: animate, updateInset: animate) if (frame.size.width < scaledSize.width - CGFloat.ulpOfOne) || (frame.size.height < scaledSize.height - CGFloat.ulpOfOne) { var offset = CGPoint.zero @@ -455,8 +480,12 @@ class ZLClipImageViewController: UIViewController { scrollView.contentOffset = CGPoint(x: -scrollView.contentInset.left + diffX, y: -scrollView.contentInset.top + diffY) } - func changeClipBoxFrame(newFrame: CGRect) { + func changeClipBoxFrame(newFrame: CGRect, animate: Bool, updateInset: Bool, endEditing: Bool = false) { guard clipBoxFrame != newFrame else { + // 可能是拖拽图片和缩放图片,编辑区域未改变,这里也要调用下endUpdate + if endEditing { + overlayView.endUpdate() + } return } if newFrame.width < CGFloat.ulpOfOne || newFrame.height < CGFloat.ulpOfOne { @@ -486,8 +515,15 @@ class ZLClipImageViewController: UIViewController { // 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) + overlayView.updateLayers(frame, animate: animate, endEditing: endEditing) + + if updateInset { + updateScrollViewContentInsetAndScale() + } + } + + func updateScrollViewContentInsetAndScale() { + let frame = clipBoxFrame scrollView.contentInset = UIEdgeInsets(top: frame.minY, left: frame.minX, bottom: scrollView.frame.maxY - frame.maxY, right: scrollView.frame.maxX - frame.maxX) @@ -510,11 +546,30 @@ class ZLClipImageViewController: UIViewController { } @objc func revertBtnClick() { + guard !isAnimate else { return } + + configFakeAnimateImageView() + let revertAngle: CGFloat + // 如果角度最终效果是顺时针旋转了90度,还原时候就逆时针旋转,否则就顺时针旋转 + if (Int(angle) + 360) % 360 == 90 { + revertAngle = CGFloat(-90).zl.toPi + } else { + revertAngle = -angle.zl.toPi + } + + let transform = CGAffineTransform(rotationAngle: revertAngle) + angle = 0 editImage = originalImage calculateClipRect() imageView.image = editImage - layoutInitialImage() + layoutInitialImage(animate: true) + + let toFrame = view.convert(containerView.frame, from: scrollView) + animateFakeImageView { + self.fakeAnimateImageView.transform = transform + self.fakeAnimateImageView.frame = toFrame + } generateThumbnailImage() clipRatioColView.reloadData() @@ -529,22 +584,14 @@ class ZLClipImageViewController: UIViewController { } @objc func rotateBtnClick() { - guard !isRotating else { - return - } + guard !isAnimate 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: scrollView) - animateImageView.frame = originFrame - view.addSubview(animateImageView) + configFakeAnimateImageView() if selectedRatio.whRatio == 0 || selectedRatio.whRatio == 1 { // 自由比例和1:1比例,进行edit rect转换 @@ -556,34 +603,55 @@ class ZLClipImageViewController: UIViewController { // 将rect进行旋转,转换到相对于旋转后的edit image的rect editRect = CGRect(x: rect.minY, y: editImage.size.height - rect.minX - rect.width, width: rect.height, height: rect.width) } else { - // 其他比例的裁剪框,旋转后都重置edit rect - // 旋转图片 editImage = editImage.zl.rotate(orientation: .left) calculateClipRect() } imageView.image = editImage - layoutInitialImage() + layoutInitialImage(animate: true) let toFrame = view.convert(containerView.frame, from: scrollView) 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 + + animateFakeImageView { + self.fakeAnimateImageView.transform = transform + self.fakeAnimateImageView.frame = toFrame } generateThumbnailImage() clipRatioColView.reloadData() } + /// 图片旋转、还原、切换比例时,用来动画的view + lazy var fakeAnimateImageView: UIImageView = { + let animateImageView = UIImageView() + animateImageView.contentMode = .scaleAspectFit + animateImageView.clipsToBounds = true + return animateImageView + }() + + func configFakeAnimateImageView() { + fakeAnimateImageView.transform = .identity + fakeAnimateImageView.image = editImage + let originFrame = view.convert(containerView.frame, from: scrollView) + fakeAnimateImageView.frame = originFrame + view.insertSubview(fakeAnimateImageView, belowSubview: overlayView) + } + + func animateFakeImageView(animations: @escaping (() -> Void), completion: (() -> Void)? = nil) { + containerView.alpha = 0 + isAnimate = true + UIView.animate(withDuration: 0.25) { + animations() + } completion: { _ in + self.containerView.alpha = 1 + self.isAnimate = false + self.fakeAnimateImageView.removeFromSuperview() + completion?() + } + } + @objc func gridGesPanAction(_ pan: UIPanGestureRecognizer) { let point = pan.location(in: view) if pan.state == .began { @@ -794,13 +862,13 @@ class ZLClipImageViewController: UIViewController { frame.origin.y = originFrame.maxY - minSize.height } - changeClipBoxFrame(newFrame: frame) + changeClipBoxFrame(newFrame: frame, animate: false, updateInset: true) } func startEditing() { cleanTimer() - shadowView.alpha = 0 - overlayView.isEditing = true + + overlayView.beginUpdate() if rotateBtn.alpha != 0 { rotateBtn.layer.removeAllAnimations() clipRatioColView.layer.removeAllAnimations() @@ -812,12 +880,12 @@ class ZLClipImageViewController: UIViewController { } @objc func endEditing() { - overlayView.isEditing = false moveClipContentToCenter() } func startTimer() { cleanTimer() + resetTimer = Timer.scheduledTimer(timeInterval: 0.8, target: ZLWeakProxy(target: self), selector: #selector(endEditing), userInfo: nil, repeats: false) RunLoop.current.add(resetTimer!, forMode: .common) } @@ -852,6 +920,8 @@ class ZLClipImageViewController: UIViewController { 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) + + changeClipBoxFrame(newFrame: clipRect, animate: true, updateInset: false, endEditing: true) UIView.animate(withDuration: 0.3) { if scale < 1 - CGFloat.ulpOfOne || scale > 1 + CGFloat.ulpOfOne { self.scrollView.zoomScale *= scale @@ -863,10 +933,10 @@ class ZLClipImageViewController: UIViewController { offset.y = min(self.scrollView.contentSize.height - clipRect.maxY, offset.y) self.scrollView.contentOffset = offset } + + self.updateScrollViewContentInsetAndScale() self.rotateBtn.alpha = 1 - self.clipRatioColView.alpha = 1 - self.shadowView.alpha = 1 - self.changeClipBoxFrame(newFrame: clipRect) + self.clipRatioColView.alpha = self.showRatioColView ? 1 : 0 } } @@ -905,9 +975,8 @@ extension ZLClipImageViewController: UIGestureRecognizerDelegate { 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) + let innerFrame = clipBoxFrame.insetBy(dx: 22, dy: 22) + let outerFrame = clipBoxFrame.insetBy(dx: -22, dy: -22) if innerFrame.contains(point) || !outerFrame.contains(point) { return false @@ -938,14 +1007,22 @@ extension ZLClipImageViewController: UICollectionViewDataSource, UICollectionVie func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let ratio = clipRatios[indexPath.row] - guard ratio != selectedRatio else { + guard ratio != selectedRatio, !isAnimate else { return } + selectedRatio = ratio clipRatioColView.reloadData() clipRatioColView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true) calculateClipRect() - layoutInitialImage() + + configFakeAnimateImageView() + layoutInitialImage(animate: true) + + let toFrame = view.convert(containerView.frame, from: scrollView) + animateFakeImageView { + self.fakeAnimateImageView.frame = toFrame + } } } @@ -958,6 +1035,15 @@ extension ZLClipImageViewController: UIScrollViewDelegate { startEditing() } + func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { + guard scrollView == self.scrollView else { + return + } + if !scrollView.isDragging { + startTimer() + } + } + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { guard scrollView == self.scrollView else { return @@ -1067,255 +1153,3 @@ class ZLImageClipRatioCell: UICollectionViewCell { 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 - - var cornerBoldLines: [UIView] = [] - - var velLines: [UIView] = [] - - 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) - } -} diff --git a/Sources/General/ZLClipOverlayView.swift b/Sources/General/ZLClipOverlayView.swift new file mode 100644 index 0000000..7ac0c93 --- /dev/null +++ b/Sources/General/ZLClipOverlayView.swift @@ -0,0 +1,310 @@ +// +// ZLClipOverlayView.swift +// ZLImageEditor +// +// Created by long on 2024/7/3. +// +// 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 + +// MARK: 裁剪网格视图 + +class ZLClipOverlayView: UIView { + static let cornerLineWidth: CGFloat = 3 + + private lazy var shadowView: UIView = { + let view = UIView() + view.backgroundColor = .black.withAlphaComponent(0.7) + view.layer.mask = shadowMaskLayer + return view + }() + + private lazy var shadowMaskLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.fillRule = .evenOdd + return layer + }() + + private lazy var cornerLinesView: UIView = { + let view = UIView() + view.layer.addSublayer(cornerLinesLayer) + return view + }() + + private lazy var cornerLinesLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = UIColor.white.cgColor + layer.fillColor = UIColor.clear.cgColor + layer.lineWidth = ZLClipOverlayView.cornerLineWidth + layer.contentsScale = UIScreen.main.scale + return layer + }() + + private lazy var frameBorderView: UIView = { + let view = UIView() + view.layer.addSublayer(frameBorderLayer) + return view + }() + + private lazy var frameBorderLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = UIColor.white.cgColor + layer.fillColor = UIColor.clear.cgColor + layer.lineWidth = 1.2 + layer.contentsScale = UIScreen.main.scale + layer.shadowOffset = CGSize.zero + layer.shadowOpacity = 1 + layer.shadowRadius = 2 + layer.shadowColor = UIColor.black.withAlphaComponent(0.7).cgColor + return layer + }() + + private lazy var gridLinesView: UIView = { + let view = UIView() + view.layer.addSublayer(gridLinesLayer) + view.alpha = 0 + return view + }() + + private lazy var gridLinesLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = UIColor.white.cgColor + layer.fillColor = UIColor.clear.cgColor + layer.lineWidth = 0.5 + layer.contentsScale = UIScreen.main.scale + return layer + }() + + var cropRect: CGRect = .zero + + var isCircle = false { + didSet { + guard oldValue != isCircle else { + return + } + + shadowMaskLayer.path = getShadowMaskLayerPath().cgPath + } + } + + 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() + + updateSubviewsFrame() + } + + private func setupUI() { + addSubview(shadowView) + addSubview(frameBorderView) + addSubview(cornerLinesView) + addSubview(gridLinesView) + + updateSubviewsFrame() + } + + private func updateSubviewsFrame() { + shadowView.frame = bounds + shadowMaskLayer.frame = shadowView.bounds + frameBorderView.frame = bounds + frameBorderLayer.frame = frameBorderView.bounds + cornerLinesView.frame = bounds + cornerLinesLayer.frame = cornerLinesView.bounds + gridLinesView.frame = bounds + gridLinesLayer.frame = gridLinesView.bounds + } + + private func getShadowMaskLayerPath() -> UIBezierPath { + let path = UIBezierPath(rect: shadowView.frame) + let transparentPath: UIBezierPath + if isCircle { + transparentPath = UIBezierPath(roundedRect: cropRect, cornerRadius: cropRect.width / 2) + } else { + transparentPath = UIBezierPath(rect: cropRect) + } + path.append(transparentPath.reversing()) + return path + } + + private func getCornerLinesLayerPath() -> UIBezierPath { + let rect = cropRect.insetBy(dx: -Self.cornerLineWidth / 2, dy: -Self.cornerLineWidth / 2) + let path = UIBezierPath() + let length: CGFloat = 20 + + // 左上 + path.move(to: CGPoint(x: rect.minX + length, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + length)) + + // 右上 + path.move(to: CGPoint(x: rect.maxX - length, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + length)) + + // 左下 + path.move(to: CGPoint(x: rect.minX, y: rect.maxY - length)) + path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.minX + length, y: rect.maxY)) + + // 右下 + path.move(to: CGPoint(x: rect.maxX - length, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - length)) + + return path + } + + private func getGridLinesLayerPath() -> UIBezierPath { + let path = UIBezierPath() + + let r = cropRect.width / 2 + var diff: CGFloat = 0 + if isCircle && ZLImageEditorConfiguration.default().dimClippedAreaDuringAdjustments { + diff = r - sqrt(pow(r, 2) - pow(r / 3, 2)) + } + // 画竖线 + let dw = cropRect.width / 3 + for i in 1...2 { + let x = CGFloat(i) * dw + cropRect.minX + path.move(to: CGPoint(x: x, y: cropRect.minY + diff)) + path.addLine(to: CGPoint(x: x, y: cropRect.maxY - diff)) + } + + // 画横线 + let dh = cropRect.height / 3 + for i in 1...2 { + let y = CGFloat(i) * dh + cropRect.minY + path.move(to: CGPoint(x: cropRect.minX + diff, y: y)) + path.addLine(to: CGPoint(x: cropRect.maxX - diff, y: y)) + } + + return path + } + + func beginUpdate() { + let config = ZLImageEditorConfiguration.default() + shadowView.alpha = config.dimClippedAreaDuringAdjustments ? 1 : 0 + gridLinesView.alpha = 1 + } + + func endUpdate(delay: TimeInterval = 0) { + UIView.animate(withDuration: 0.15, delay: delay) { + if !ZLImageEditorConfiguration.default().dimClippedAreaDuringAdjustments { + self.shadowView.alpha = 1 + } + self.gridLinesView.alpha = 0 + } + } + + func updateLayers(_ rect: CGRect, animate: Bool, endEditing: Bool) { + cropRect = rect + + let shadowMaskPath = getShadowMaskLayerPath() + let frameBorderPath = UIBezierPath(rect: rect) + let cornerLinesPath = getCornerLinesLayerPath() + let gridLinesPath = getGridLinesLayerPath() + + let duration: TimeInterval = 0.25 + func animateShadowMaskLayer() { + shadowMaskLayer.removeAnimation(forKey: "shadowMaskAnimation") + let animation = ZLAnimationUtils.animation( + type: .path, + fromValue: shadowMaskLayer.path, + toValue: shadowMaskPath.cgPath, + duration: duration, + isRemovedOnCompletion: true, + timingFunction: CAMediaTimingFunction(name: .easeInEaseOut) + ) + shadowMaskLayer.add(animation, forKey: "shadowMaskAnimation") + } + + func animateFrameBorderLayer() { + frameBorderLayer.removeAnimation(forKey: "frameBorderAnimation") + let animation = ZLAnimationUtils.animation( + type: .path, + fromValue: frameBorderLayer.path, + toValue: frameBorderPath.cgPath, + duration: duration, + isRemovedOnCompletion: true, + timingFunction: CAMediaTimingFunction(name: .easeInEaseOut) + ) + frameBorderLayer.add(animation, forKey: "frameBorderAnimation") + } + + func animateCornerLinesLayer() { + cornerLinesLayer.removeAnimation(forKey: "cornerLinesAnimation") + let animation = ZLAnimationUtils.animation( + type: .path, + fromValue: cornerLinesLayer.path, + toValue: cornerLinesPath.cgPath, + duration: duration, + isRemovedOnCompletion: true, + timingFunction: CAMediaTimingFunction(name: .easeInEaseOut) + ) + cornerLinesLayer.add(animation, forKey: "cornerLinesAnimation") + } + + func animateGridLinesLayer() { + gridLinesLayer.removeAnimation(forKey: "gridLinesAnimation") + let animation = ZLAnimationUtils.animation( + type: .path, + fromValue: gridLinesLayer.path, + toValue: gridLinesPath.cgPath, + duration: duration, + isRemovedOnCompletion: true, + timingFunction: CAMediaTimingFunction(name: .easeInEaseOut) + ) + gridLinesLayer.add(animation, forKey: "gridLinesAnimation") + } + + if animate { + animateShadowMaskLayer() + animateFrameBorderLayer() + animateCornerLinesLayer() + animateGridLinesLayer() + } + + CATransaction.begin() + CATransaction.setDisableActions(true) + + shadowMaskLayer.path = shadowMaskPath.cgPath + frameBorderLayer.path = frameBorderPath.cgPath + cornerLinesLayer.path = cornerLinesPath.cgPath + gridLinesLayer.path = gridLinesPath.cgPath + + CATransaction.commit() + + if animate, endEditing { + endUpdate(delay: duration) + } + } +} diff --git a/Sources/General/ZLEditImageViewController.swift b/Sources/General/ZLEditImageViewController.swift index 13c8a4d..5b4b699 100644 --- a/Sources/General/ZLEditImageViewController.swift +++ b/Sources/General/ZLEditImageViewController.swift @@ -69,7 +69,7 @@ public class ZLEditImageModel: NSObject { public let clipStatus: ZLClipStatus? - public let adjustStatus: ZLAdjustStatus + public let adjustStatus: ZLAdjustStatus? public let selectFilter: ZLFilter? @@ -81,7 +81,7 @@ public class ZLEditImageModel: NSObject { drawPaths: [ZLDrawPath] = [], mosaicPaths: [ZLMosaicPath] = [], clipStatus: ZLClipStatus? = nil, - adjustStatus: ZLAdjustStatus = ZLAdjustStatus(), + adjustStatus: ZLAdjustStatus? = nil, selectFilter: ZLFilter? = nil, stickers: [ZLBaseStickertState] = [], actions: [ZLEditorAction] = [] @@ -391,6 +391,9 @@ open class ZLEditImageViewController: UIViewController { override open var prefersHomeIndicatorAutoHidden: Bool { true } + /// 延缓屏幕上下方通知栏弹出,避免手势冲突 + override open var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { [.top, .bottom] } + override open var supportedInterfaceOrientations: UIInterfaceOrientationMask { deviceIsiPhone() ? .portrait : .all } @@ -416,13 +419,7 @@ open class ZLEditImageViewController: UIViewController { vc.clipDoneBlock = { angle, editRect, ratio in let m = ZLEditImageModel( - drawPaths: [], - mosaicPaths: [], - clipStatus: ZLClipStatus(editRect: editRect, angle: angle, ratio: ratio), - adjustStatus: ZLAdjustStatus(), - selectFilter: .normal, - stickers: [], - actions: [] + clipStatus: ZLClipStatus(editRect: editRect, angle: angle, ratio: ratio) ) completion?(image.zl.clipImage(angle: angle, editRect: editRect, isCircle: ratio.isCircle) ?? image, m) } @@ -531,13 +528,13 @@ open class ZLEditImageViewController: UIViewController { if #available(iOS 11.0, *) { insets = self.view.safeAreaInsets } + insets.top = max(insets.top, 20) mainScrollView.frame = view.bounds resetContainerViewFrame() topShadowView.frame = CGRect(x: 0, y: 0, width: view.zl.width, height: 150) topShadowLayer.frame = topShadowView.bounds - cancelBtn.frame = CGRect(x: 30, y: insets.top + 10, width: 28, height: 28) bottomShadowView.frame = CGRect(x: 0, y: view.zl.height - 150 - insets.bottom, width: view.zl.width, height: 150 + insets.bottom) bottomShadowLayer.frame = bottomShadowView.bounds @@ -547,9 +544,9 @@ open class ZLEditImageViewController: UIViewController { font: ZLImageEditorLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: 28) ).width - cancelBtn.frame = CGRect(x: 20, y: 60, width: cancelBtnW, height: 30) - redoBtn.frame = CGRect(x: view.zl.width - 15 - 30, y: 60, width: 30, height: 30) - undoBtn.frame = CGRect(x: redoBtn.zl.left - 15 - 30, y: 60, width: 30, height: 30) + cancelBtn.frame = CGRect(x: 20, y: insets.top, width: cancelBtnW, height: 30) + redoBtn.frame = CGRect(x: view.zl.width - 15 - 30, y: insets.top, width: 30, height: 30) + undoBtn.frame = CGRect(x: redoBtn.zl.left - 15 - 30, y: insets.top, width: 30, height: 30) eraserBtn.frame = CGRect(x: 20, y: 30 + (drawColViewH - 36) / 2, width: 36, height: 36) eraserBtnBgBlurView.frame = eraserBtn.frame diff --git a/Sources/General/ZLImageEditorConfiguration.swift b/Sources/General/ZLImageEditorConfiguration.swift index 91d7717..b1867e7 100644 --- a/Sources/General/ZLImageEditorConfiguration.swift +++ b/Sources/General/ZLImageEditorConfiguration.swift @@ -188,6 +188,9 @@ public class ZLImageEditorConfiguration: NSObject { /// If image edit tools only has clip and this property is true. When you click edit, the cropping interface (i.e. ZLClipImageViewController) will be displayed. Defaults to false @objc public var showClipDirectlyIfOnlyHasClipTool = false + + /// Whether to keep clipped area dimmed during adjustments. Defaults to false + @objc public var dimClippedAreaDuringAdjustments = false } public extension ZLImageEditorConfiguration { diff --git a/ZLImageEditor.xcodeproj/project.pbxproj b/ZLImageEditor.xcodeproj/project.pbxproj index eb36cac..d81a9d7 100644 --- a/ZLImageEditor.xcodeproj/project.pbxproj +++ b/ZLImageEditor.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ FD3073352AD80B7700CFB618 /* ZLBaseStickertState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3073342AD80B7700CFB618 /* ZLBaseStickertState.swift */; }; FD3073372AD80D0000CFB618 /* ZLEditorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3073362AD80D0000CFB618 /* ZLEditorManager.swift */; }; FD3073392AD80D4000CFB618 /* ZLPaths.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3073382AD80D4000CFB618 /* ZLPaths.swift */; }; + FD6E25CA2C3528FE00ED5AFD /* ZLClipOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E25C92C3528FE00ED5AFD /* ZLClipOverlayView.swift */; }; + FD6E25CC2C35318400ED5AFD /* ZLAnimationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6E25CB2C35318400ED5AFD /* ZLAnimationUtils.swift */; }; FD7E7508284E134600EF34DD /* ZLProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7E7507284E134600EF34DD /* ZLProgressHUD.swift */; }; FD8EFA522772026D0067ADF1 /* ZLAdjustSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8EFA512772026D0067ADF1 /* ZLAdjustSlider.swift */; }; FD8EFA54277206D30067ADF1 /* ZLEditToolCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8EFA53277206D30067ADF1 /* ZLEditToolCells.swift */; }; @@ -68,6 +70,8 @@ FD3073342AD80B7700CFB618 /* ZLBaseStickertState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLBaseStickertState.swift; sourceTree = ""; }; FD3073362AD80D0000CFB618 /* ZLEditorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLEditorManager.swift; sourceTree = ""; }; FD3073382AD80D4000CFB618 /* ZLPaths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLPaths.swift; sourceTree = ""; }; + FD6E25C92C3528FE00ED5AFD /* ZLClipOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLClipOverlayView.swift; sourceTree = ""; }; + FD6E25CB2C35318400ED5AFD /* ZLAnimationUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZLAnimationUtils.swift; sourceTree = ""; }; FD7E7507284E134600EF34DD /* ZLProgressHUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZLProgressHUD.swift; sourceTree = ""; }; FD8EFA512772026D0067ADF1 /* ZLAdjustSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZLAdjustSlider.swift; sourceTree = ""; }; FD8EFA53277206D30067ADF1 /* ZLEditToolCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZLEditToolCells.swift; sourceTree = ""; }; @@ -135,6 +139,7 @@ E40208E4256B9F9C0077F5DC /* ZLEditImageViewController.swift */, E40208E9256B9F9C0077F5DC /* ZLClipImageViewController.swift */, E40208E8256B9F9C0077F5DC /* ZLInputTextViewController.swift */, + FD6E25C92C3528FE00ED5AFD /* ZLClipOverlayView.swift */, FDC9B2432990D66300963047 /* ZLBaseStickerView.swift */, FD3073342AD80B7700CFB618 /* ZLBaseStickertState.swift */, E40208E5256B9F9C0077F5DC /* ZLImageStickerView.swift */, @@ -148,6 +153,7 @@ FDD5289126C3FFCA00338B06 /* ZLImageEditor.swift */, FDE1A560282E61B500178B0F /* ZLEnlargeButton.swift */, FD7E7507284E134600EF34DD /* ZLProgressHUD.swift */, + FD6E25CB2C35318400ED5AFD /* ZLAnimationUtils.swift */, FDC144952BABE0B4004C2BCD /* ZLWeakProxy.h */, FDC144942BABE0B4004C2BCD /* ZLWeakProxy.m */, FDB3113228752F7300845756 /* ZLWeakProxy.swift */, @@ -272,8 +278,10 @@ E40208F7256B9FCB0077F5DC /* CGFloat+ZLImageEditor.swift in Sources */, FDE1A55D282E595C00178B0F /* UIColor+ZLImageEditor.swift in Sources */, FD8EFA562772CF3B0067ADF1 /* ZLImageEditorConfiguration+Chaining.swift in Sources */, + FD6E25CA2C3528FE00ED5AFD /* ZLClipOverlayView.swift in Sources */, FD8EFA522772026D0067ADF1 /* ZLAdjustSlider.swift in Sources */, E40208EB256B9F9C0077F5DC /* ZLEditImageViewController.swift in Sources */, + FD6E25CC2C35318400ED5AFD /* ZLAnimationUtils.swift in Sources */, E402090E256BA1F40077F5DC /* ZLImageEditorLanguageDefine.swift in Sources */, FDC144962BABE0B4004C2BCD /* ZLWeakProxy.m in Sources */, FDC9B2442990D66300963047 /* ZLBaseStickerView.swift in Sources */,