diff --git a/Sources/Mantis/CropView/CropView.swift b/Sources/Mantis/CropView/CropView.swift index 2a5930bd..8ca0e109 100644 --- a/Sources/Mantis/CropView/CropView.swift +++ b/Sources/Mantis/CropView/CropView.swift @@ -75,7 +75,7 @@ final class CropView: UIView { indicator.transform = CGAffineTransform(scaleX: 2.0, y: 2.0) activityIndicator = indicator } - + addSubview(activityIndicator) activityIndicator.translatesAutoresizingMaskIntoConstraints = false activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true @@ -108,7 +108,7 @@ final class CropView: UIView { self.cropMaskViewManager = cropMaskViewManager super.init(frame: .zero) - + if let color = cropViewConfig.backgroundColor { self.backgroundColor = color } @@ -183,7 +183,7 @@ final class CropView: UIView { rotationControlView?.isHidden = isHidden } } - + private func imageStatusChanged() -> Bool { if viewModel.getTotalRadians() != 0 { return true } @@ -250,6 +250,17 @@ final class CropView: UIView { addSubview(cropAuxiliaryIndicatorView) } + /** This function is for correct flips. If rotating angle is exact ±45 degrees, + the flip behaviour will be incorrect. So we need to limit the rotating angle. */ + private func clampAngle(_ angle: Angle) -> Angle { + let errorMargin = 1e-10 + let rotationLimit = Constants.rotationDegreeLimit + + return angle.degrees > 0 + ? min(angle, Angle(degrees: rotationLimit - errorMargin)) + : max(angle, Angle(degrees: -rotationLimit + errorMargin)) + } + private func setupRotationDialIfNeeded() { guard let rotationControlView = rotationControlView else { return @@ -257,10 +268,10 @@ final class CropView: UIView { rotationControlView.reset() rotationControlView.isUserInteractionEnabled = true - + rotationControlView.didUpdateRotationValue = { [unowned self] angle in self.viewModel.setTouchRotationBoardStatus() - self.viewModel.setRotatingStatus(by: angle) + self.viewModel.setRotatingStatus(by: clampAngle(angle)) } rotationControlView.didFinishRotation = { [unowned self] in @@ -269,21 +280,21 @@ final class CropView: UIView { } self.viewModel.setBetweenOperationStatus() } - + if rotationControlView.isAttachedToCropView { let boardLength = min(bounds.width, bounds.height) * rotationControlView.getLengthRatio() let dialFrame = CGRect(x: 0, y: 0, width: boardLength, height: cropViewConfig.rotationControlViewHeight) - + rotationControlView.setupUI(withAllowableFrame: dialFrame) } if let rotationDial = rotationControlView as? RotationDialProtocol { rotationDial.setRotationCenter(by: cropAuxiliaryIndicatorView.center, of: self) } - + rotationControlView.updateRotationValue(by: Angle(radians: viewModel.radians)) viewModel.setBetweenOperationStatus() @@ -293,7 +304,7 @@ final class CropView: UIView { private func adaptRotationControlViewToCropBoxIfNeeded() { guard let rotationControlView = rotationControlView, - rotationControlView.isAttachedToCropView else { return } + rotationControlView.isAttachedToCropView else { return } if Orientation.treatAsPortrait { rotationControlView.transform = CGAffineTransform(rotationAngle: 0) @@ -337,7 +348,7 @@ final class CropView: UIView { return confinedPoint } - + func updateCropBoxFrame(withTouchPoint touchPoint: CGPoint) { let imageContainerRect = imageContainer.convert(imageContainer.bounds, to: self) let imageFrame = CGRect(x: cropWorkbenchView.frame.origin.x - cropWorkbenchView.contentOffset.x, @@ -355,7 +366,7 @@ final class CropView: UIView { print("newCropBoxFrame is \(newCropBoxFrame.width) - \(newCropBoxFrame.height)") guard newCropBoxFrame.width >= cropViewMinimumBoxSize - && newCropBoxFrame.height >= cropViewMinimumBoxSize else { + && newCropBoxFrame.height >= cropViewMinimumBoxSize else { return } @@ -529,7 +540,7 @@ extension CropView { let newBoundHeight = abs(sin(radians)) * newCropBounds.size.width + abs(cos(radians)) * newCropBounds.size.height guard newBoundWidth > 0 && newBoundWidth != .infinity - && newBoundHeight > 0 && newBoundHeight != .infinity else { + && newBoundHeight > 0 && newBoundHeight != .infinity else { return } @@ -666,7 +677,7 @@ extension CropView { func addImageMask(to cropOutput: CropOutput) -> CropOutput { let (croppedImage, transformation, cropInfo) = cropOutput - + guard let croppedImage = croppedImage else { assertionFailure("croppedImage should not be nil") return cropOutput @@ -674,13 +685,13 @@ extension CropView { switch cropViewConfig.cropShapeType { case .rect, - .square, - .circle(maskOnly: true), - .roundedRect(_, maskOnly: true), - .path(_, maskOnly: true), - .diamond(maskOnly: true), - .heart(maskOnly: true), - .polygon(_, _, maskOnly: true): + .square, + .circle(maskOnly: true), + .roundedRect(_, maskOnly: true), + .path(_, maskOnly: true), + .diamond(maskOnly: true), + .heart(maskOnly: true), + .polygon(_, _, maskOnly: true): let outputImage: UIImage? if cropViewConfig.cropBorderWidth > 0 { @@ -739,7 +750,7 @@ extension CropView { func getTotalRadians() -> CGFloat { return viewModel.getTotalRadians() } - + func setFixedRatioCropBox(zoom: Bool = true, cropBox: CGRect? = nil) { let refCropBox = cropBox ?? getInitialCropBoxRect() let imageHorizontalToVerticalRatio = ImageHorizontalToVerticalRatio(ratio: getImageHorizontalToVerticalRatio()) @@ -779,7 +790,7 @@ extension CropView { func flip() { flipOddTimes.toggle() - let flipTransform = cropWorkbenchView.transform.scaledBy(x: scaleX, y: scaleY) + let flipTransform = cropWorkbenchView.transform.scaledBy(x: scaleX, y: scaleY) let coff: CGFloat = flipOddTimes ? 2 : -2 cropWorkbenchView.transform = flipTransform.rotated(by: coff*viewModel.radians) @@ -824,7 +835,7 @@ extension CropView: CropViewProtocol { return Double(1 / image.horizontalToVerticalRatio()) } } - + func prepareForViewWillTransition() { viewModel.setDegree90RotatingStatus() saveAnchorPoints() @@ -895,7 +906,7 @@ extension CropView: CropViewProtocol { var rect = cropAuxiliaryIndicatorView.frame rect.size.width = cropAuxiliaryIndicatorView.frame.height rect.size.height = cropAuxiliaryIndicatorView.frame.width - + let newRect = GeometryHelper.getInscribeRect(fromOutsideRect: getContentBounds(), andInsideRect: rect) viewModel.cropBoxFrame = newRect let rotateAngle = newRotateType == .clockwise ? CGFloat.pi / 2 : -CGFloat.pi / 2 @@ -954,7 +965,7 @@ extension CropView: CropViewProtocol { cropWorkbenchView.zoomScale = transformation.scale cropWorkbenchView.contentOffset = transformation.offset viewModel.setBetweenOperationStatus() - + if transformation.maskFrame != .zero { viewModel.cropBoxFrame = transformation.maskFrame } @@ -993,9 +1004,9 @@ extension CropView: CropViewProtocol { width: maskFrameWidth, height: maskFrameHeight) newTransform.cropWorkbenchViewBounds = CGRect(x: transformInfo.cropWorkbenchViewBounds.origin.x * adjustScale, - y: transformInfo.cropWorkbenchViewBounds.origin.y * adjustScale, - width: transformInfo.cropWorkbenchViewBounds.width * adjustScale, - height: transformInfo.cropWorkbenchViewBounds.height * adjustScale) + y: transformInfo.cropWorkbenchViewBounds.origin.y * adjustScale, + width: transformInfo.cropWorkbenchViewBounds.width * adjustScale, + height: transformInfo.cropWorkbenchViewBounds.height * adjustScale) return newTransform } diff --git a/Sources/Mantis/Enum.swift b/Sources/Mantis/Enum.swift index d9187c54..b4a2030a 100644 --- a/Sources/Mantis/Enum.swift +++ b/Sources/Mantis/Enum.swift @@ -162,3 +162,7 @@ enum AutoLayoutPriorityType: Float { case high = 10000 case low = 1 } + +enum Constants { + static let rotationDegreeLimit: CGFloat = 45 +} diff --git a/Sources/Mantis/RotationDial/RotationDial/RotationDial.swift b/Sources/Mantis/RotationDial/RotationDial/RotationDial.swift index 2e5f5b40..c7a5409d 100644 --- a/Sources/Mantis/RotationDial/RotationDial/RotationDial.swift +++ b/Sources/Mantis/RotationDial/RotationDial/RotationDial.swift @@ -38,8 +38,8 @@ final class RotationDial: UIView { private var config: RotationDialConfig - private var angleLimit = Angle(radians: .pi) - private var showRadiansLimit: CGFloat = .pi + private let angleLimit = Angle(degrees: Constants.rotationDegreeLimit) + private let showRadiansLimit: CGFloat = 40 * .pi / 180 private var dialPlate: RotationDialPlate? private var dialPlateHolder: UIView? private var pointer: CAShapeLayer = CAShapeLayer() @@ -92,12 +92,10 @@ extension RotationDial { } private func handleRotation(by angle: Angle) { - if case .limit = config.rotationLimitType { - guard angle <= angleLimit else { - return - } + guard angle <= angleLimit else { + return } - + if updateRotation(bySteppingAngle: angle) { let newAngle = getRotationAngle() didUpdateRotationValue(newAngle) @@ -134,15 +132,7 @@ extension RotationDial { } private func setupDialPlate(in container: UIView) { - var margin = CGFloat(config.margin) - - if case .limit(let degreeAngle) = config.angleShowLimitType { - margin = 0 - showRadiansLimit = Angle(degrees: degreeAngle).radians - } else { - showRadiansLimit = CGFloat.pi - } - + let margin = CGFloat(config.margin) var dialPlateShowHeight = container.frame.height - margin - pointerHeight - spanBetweenDialPlateAndPointer var radius = dialPlateShowHeight / (1 - cos(showRadiansLimit)) @@ -195,12 +185,7 @@ extension RotationDial { extension RotationDial: RotationDialProtocol { func setupUI(withAllowableFrame allowableFrame: CGRect) { - self.frame = allowableFrame - - if case .limit(let degreeAngle) = config.rotationLimitType { - angleLimit = Angle(degrees: degreeAngle) - } - + self.frame = allowableFrame setupUI() setupViewModel() } @@ -209,19 +194,17 @@ extension RotationDial: RotationDialProtocol { guard let dialPlate = dialPlate else { return false } let radians = steppingAngle.radians - if case .limit = config.rotationLimitType { - if (getRotationAngle() * steppingAngle).radians >= 0 && abs(getRotationAngle().radians + radians) > angleLimit.radians { - - if radians > 0 { - rotateDialPlate(to: angleLimit) - } else { - rotateDialPlate(to: -angleLimit) - } - - return false + if (getRotationAngle() * steppingAngle).radians >= 0 && abs(getRotationAngle().radians + radians) > angleLimit.radians { + + if radians > 0 { + rotateDialPlate(to: angleLimit) + } else { + rotateDialPlate(to: -angleLimit) } + + return false } - + dialPlate.transform = dialPlate.transform.rotated(by: radians) setAccessibilityValue() @@ -230,12 +213,10 @@ extension RotationDial: RotationDialProtocol { @discardableResult func updateRotationValue(by angle: Angle) -> Bool { - if case .limit = config.rotationLimitType { - if abs(angle.degrees) > angleLimit.degrees { - return false - } + if abs(angle.degrees) > angleLimit.degrees { + return false } - + rotateDialPlate(to: angle) setAccessibilityValue() @@ -243,16 +224,8 @@ extension RotationDial: RotationDialProtocol { } func rotateDialPlate(to angle: Angle, animated: Bool = false) { - let radians = angle.radians - - if case .limit = config.rotationLimitType { - guard abs(radians) <= angleLimit.radians else { - return - } - } - - func rotate() { - dialPlate?.transform = CGAffineTransform(rotationAngle: radians) + guard abs(angle.radians) <= angleLimit.radians else { + return } if animated { @@ -262,6 +235,10 @@ extension RotationDial: RotationDialProtocol { } else { rotate() } + + func rotate() { + dialPlate?.transform = CGAffineTransform(rotationAngle: angle.radians) + } } func getRotationAngle() -> Angle { diff --git a/Sources/Mantis/RotationDial/RotationDial/RotationDialConfig.swift b/Sources/Mantis/RotationDial/RotationDial/RotationDialConfig.swift index ec4fb837..c0d7bd30 100644 --- a/Sources/Mantis/RotationDial/RotationDial/RotationDialConfig.swift +++ b/Sources/Mantis/RotationDial/RotationDial/RotationDialConfig.swift @@ -11,16 +11,14 @@ import UIKit public struct RotationDialConfig { public init() {} - public var margin: Double = 10 { + public var margin: Double = 0 { didSet { assert(margin >= 0) } } public var lengthRatio: CGFloat = 0.6 - - public var rotationLimitType: RotationLimitType = .limit(degreeAngle: 45) - public var angleShowLimitType: AngleShowLimitType = .limit(degreeAngle: 40) + public var rotationCenterType: RotationCenterType = .useDefault public var numberShowSpan = 1 { @@ -64,16 +62,6 @@ public struct RotationDialConfig { case custom(center: CGPoint) } - public enum AngleShowLimitType { - case noLimit - case limit(degreeAngle: CGFloat) - } - - public enum RotationLimitType { - case noLimit - case limit(degreeAngle: CGFloat) - } - public enum Orientation { case normal case right diff --git a/Sources/Mantis/RotationDial/SlideDial/SlideDialConfig.swift b/Sources/Mantis/RotationDial/SlideDial/SlideDialConfig.swift index 87a68a50..c81da725 100644 --- a/Sources/Mantis/RotationDial/SlideDial/SlideDialConfig.swift +++ b/Sources/Mantis/RotationDial/SlideDial/SlideDialConfig.swift @@ -11,7 +11,7 @@ public struct SlideDialConfig { public init() {} public var lengthRatio: CGFloat = 0.8 - public var limitation: CGFloat = 45 + public var limitation: CGFloat = Constants.rotationDegreeLimit public var scaleBarNumber = 41 public var majorScaleBarNumber = 5 diff --git a/Tests/MantisTests/RotateDialTests.swift b/Tests/MantisTests/RotateDialTests.swift index ec96cba7..a9e7d3ee 100644 --- a/Tests/MantisTests/RotateDialTests.swift +++ b/Tests/MantisTests/RotateDialTests.swift @@ -34,9 +34,8 @@ final class RotateDialTests: XCTestCase { } func testRotateDialPlate() { - // Test valid plus rotation with limitation - var dialConfig = RotationDialConfig() - dialConfig.rotationLimitType = .limit(degreeAngle: 45) + // Test valid plus rotation + let dialConfig = RotationDialConfig() setup(with: dialConfig) var dialPlateTransform = dialPlate.transform @@ -44,8 +43,7 @@ final class RotateDialTests: XCTestCase { XCTAssertTrue(dial.updateRotationValue(by: angle)) XCTAssertEqual(dialPlate.transform, dialPlateTransform.rotated(by: angle.radians)) - // Test invalid plus rotation with limitation - dialConfig.rotationLimitType = .limit(degreeAngle: 45) + // Test invalid plus rotation setup(with: dialConfig) dialPlateTransform = dialPlate.transform @@ -54,8 +52,7 @@ final class RotateDialTests: XCTestCase { XCTAssertNotEqual(dialPlate.transform, dialPlateTransform.rotated(by: angle.radians)) XCTAssertEqual(dialPlate.transform, dialPlateTransform.rotated(by: Angle(degrees: 0).radians)) - // Test valid minus rotation with limitation - dialConfig.rotationLimitType = .limit(degreeAngle: 45) + // Test valid minus rotation setup(with: dialConfig) dialPlateTransform = dialPlate.transform @@ -63,8 +60,7 @@ final class RotateDialTests: XCTestCase { XCTAssertTrue(dial.updateRotationValue(by: angle)) XCTAssertEqual(dialPlate.transform, dialPlateTransform.rotated(by: angle.radians)) - // Test invalid minus rotation with limitation - dialConfig.rotationLimitType = .limit(degreeAngle: 45) + // Test invalid minus rotation setup(with: dialConfig) dialPlateTransform = dialPlate.transform @@ -72,21 +68,10 @@ final class RotateDialTests: XCTestCase { XCTAssertFalse(dial.updateRotationValue(by: angle)) XCTAssertNotEqual(dialPlate.transform, dialPlateTransform.rotated(by: angle.radians)) XCTAssertEqual(dialPlate.transform, dialPlateTransform.rotated(by: Angle(degrees: 0).radians)) - - // Test no limit - dialConfig = RotationDialConfig() - dialConfig.rotationLimitType = .noLimit - setup(with: dialConfig) - - dialPlateTransform = dialPlate.transform - angle = Angle(degrees: 70) - XCTAssertTrue(dial.updateRotationValue(by: angle)) - XCTAssertEqual(dialPlate.transform, dialPlateTransform.rotated(by: angle.radians)) } func testReset() { - var dialConfig = RotationDialConfig() - dialConfig.rotationLimitType = .limit(degreeAngle: 45) + let dialConfig = RotationDialConfig() setup(with: dialConfig) let dialPlateTransform = dialPlate.transform