Skip to content

Commit

Permalink
fix: fix flip issue when rotating angle is exact 45 or -45 degrees (#353
Browse files Browse the repository at this point in the history
)

* fix: fix flip issue when rotating angle is exact 45 or -45 degrees

deprecate some enum definitions and properties

* refactor: add function clampAngle

reformat code

* refactor: remove rotationLimit and angleShowLimit from RotationDialConfig

fix some broken unit tests

* refactor: remove AngleShowLimitType and RotationLimitType
  • Loading branch information
guoyingtao authored Nov 13, 2023
1 parent 41680e8 commit 6f73382
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 112 deletions.
67 changes: 39 additions & 28 deletions Sources/Mantis/CropView/CropView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -108,7 +108,7 @@ final class CropView: UIView {
self.cropMaskViewManager = cropMaskViewManager

super.init(frame: .zero)

if let color = cropViewConfig.backgroundColor {
self.backgroundColor = color
}
Expand Down Expand Up @@ -183,7 +183,7 @@ final class CropView: UIView {
rotationControlView?.isHidden = isHidden
}
}

private func imageStatusChanged() -> Bool {
if viewModel.getTotalRadians() != 0 { return true }

Expand Down Expand Up @@ -250,17 +250,28 @@ 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
}

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
Expand All @@ -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()

Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -666,21 +677,21 @@ 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
}

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 {
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -824,7 +835,7 @@ extension CropView: CropViewProtocol {
return Double(1 / image.horizontalToVerticalRatio())
}
}

func prepareForViewWillTransition() {
viewModel.setDegree90RotatingStatus()
saveAnchorPoints()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions Sources/Mantis/Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,7 @@ enum AutoLayoutPriorityType: Float {
case high = 10000
case low = 1
}

enum Constants {
static let rotationDegreeLimit: CGFloat = 45
}
73 changes: 25 additions & 48 deletions Sources/Mantis/RotationDial/RotationDial/RotationDial.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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()
}
Expand All @@ -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()

Expand All @@ -230,29 +213,19 @@ 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()

return true
}

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 {
Expand All @@ -262,6 +235,10 @@ extension RotationDial: RotationDialProtocol {
} else {
rotate()
}

func rotate() {
dialPlate?.transform = CGAffineTransform(rotationAngle: angle.radians)
}
}

func getRotationAngle() -> Angle {
Expand Down
16 changes: 2 additions & 14 deletions Sources/Mantis/RotationDial/RotationDial/RotationDialConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 6f73382

Please sign in to comment.