Skip to content

Commit

Permalink
feat: make rotation view customizable (#308)
Browse files Browse the repository at this point in the history
* feat: update API to allow users use their own rotation control view

* refactor: refactor RotationControlViewProtocol and add comments

* refactor: use showRotationControlView instead of showRotationDial in CropViewConfig

* fix: use RotationControlViewProtocol instead of concrete class RotationDial

rename "rotation dial" with "rotation control view"

* feat: add isAttachedToCropView to RotationControlViewProtocol

* refactor: rename showRotationControlView to showAttachedRotationControlView

* fix: fix compile errors in Example project

* feat: add more logic for rotation control view

* feat: move viewModel.setTouchRotationBoardStatus() to rotationControlView.didUpdateRotationValue

to let rotation view controller which is not controlled by CropView to show rotating indicators

* fix: fix logic of setupRotationControlViewIfNeeded

When using rotationControlView passed by users, do not use cropViewConfig.showAttachedRotationControlView

* fix: fix logic in setupRotationControlViewIfNeeded and setupRotationDial

* refactor: rename setupRotationDial to setupRotationDialIfNeeded
  • Loading branch information
guoyingtao committed May 26, 2023
1 parent a54521b commit 07d1b32
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 127 deletions.
4 changes: 2 additions & 2 deletions Example/ViewController.swift
Expand Up @@ -58,7 +58,7 @@ class ViewController: UIViewController, CropViewControllerDelegate {

var config = Mantis.Config()
config.cropMode = .async
config.cropViewConfig.showRotationDial = false
config.cropViewConfig.showAttachedRotationControlView = false
config.showAttachedCropToolbar = false
let cropViewController: CustomViewController = Mantis.cropViewController(image: image, config: config)
cropViewController.delegate = self
Expand Down Expand Up @@ -101,7 +101,7 @@ class ViewController: UIViewController, CropViewControllerDelegate {

var config = Mantis.Config()
config.showAttachedCropToolbar = false
config.cropViewConfig.showRotationDial = false
config.cropViewConfig.showAttachedRotationControlView = false
config.cropViewConfig.minimumZoomScale = 2.0
config.cropViewConfig.maximumZoomScale = 10.0

Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -29,7 +29,7 @@
## Breaking Changes in 2.x.x
* Add CropViewConfig
* move some properties from Config to CropViewConfig
* make dialConfig as a property of CropViewConfig
* make roationControlViewConfig as a property of CropViewConfig
* Refactor CropToolbarConfigProtocol
* rename some properties

Expand Down Expand Up @@ -145,7 +145,7 @@ let cropViewController = Mantis.cropViewController(image: <Your Image>, config:

When choose alwaysUsingOnePresetFixedRatio, fixed-ratio setting button does not show.

* If you want to hide rotation dial, set Mantis.Config..cropViewConfig.dialConfig = nil
* If you want to hide rotation control view, set Mantis.Config.cropViewConfig.showAttachedRotationControlView = false
* If you want to use ratio list instead of presenter, set Mantis.CropToolbarConfig.ratioCandidatesShowType = .alwaysShowRatioList

```swift
Expand Down
9 changes: 4 additions & 5 deletions Sources/Mantis/CropView/CropView+Touches.swift
Expand Up @@ -12,8 +12,8 @@ extension CropView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let newPoint = convert(point, to: self)

if let rotationDial = rotationDial, rotationDial.frame.contains(newPoint) {
return rotationDial
if let rotationControlView = rotationControlView, rotationControlView.frame.contains(newPoint) {
return rotationControlView
}

if !cropViewConfig.disableCropBoxDeformation && isHitGridOverlayView(by: newPoint) {
Expand Down Expand Up @@ -44,8 +44,7 @@ extension CropView {
// A resize event has begun by grabbing the crop UI, so notify delegate
delegate?.cropViewDidBeginResize(self)

if touch.view is RotationDial {
viewModel.setTouchRotationBoardStatus()
if touch.view is RotationControlViewProtocol {
return
}

Expand All @@ -60,7 +59,7 @@ extension CropView {
return
}

if touch.view is RotationDial {
if touch.view is RotationControlViewProtocol {
return
}

Expand Down
130 changes: 72 additions & 58 deletions Sources/Mantis/CropView/CropView.swift
Expand Up @@ -32,8 +32,6 @@ protocol CropViewDelegate: AnyObject {
}

class CropView: UIView {
private let angleDashboardHeight: CGFloat = 60

var image: UIImage

let viewModel: CropViewModelProtocol
Expand All @@ -52,9 +50,11 @@ class CropView: UIView {
let cropWorkbenchView: CropWorkbenchViewProtocol
let cropMaskViewManager: CropMaskViewManagerProtocol

var rotationDial: RotationDialProtocol? {
var rotationControlView: RotationControlViewProtocol? {
didSet {
addSubview(rotationDial!)
if rotationControlView?.isAttachedToCropView == true {
addSubview(rotationControlView!)
}
}
}

Expand Down Expand Up @@ -154,27 +154,33 @@ class CropView: UIView {
case .degree90Rotating:
cropMaskViewManager.showVisualEffectBackground(animated: true)
cropAuxiliaryIndicatorView.isHidden = true
rotationDial?.isHidden = true
toggleRotationControlViewIsNeeded(isHidden: true)
case .touchImage:
cropMaskViewManager.showDimmingBackground(animated: true)
cropAuxiliaryIndicatorView.gridLineNumberType = .crop
cropAuxiliaryIndicatorView.gridHidden = false
case .touchCropboxHandle(let tappedEdge):
cropAuxiliaryIndicatorView.handleIndicatorHandleTouched(with: tappedEdge)
rotationDial?.isHidden = true
toggleRotationControlViewIsNeeded(isHidden: true)
cropMaskViewManager.showDimmingBackground(animated: true)
case .touchRotationBoard:
cropAuxiliaryIndicatorView.gridLineNumberType = .rotate
cropAuxiliaryIndicatorView.gridHidden = false
cropMaskViewManager.showDimmingBackground(animated: true)
case .betweenOperation:
cropAuxiliaryIndicatorView.handleEdgeUntouched()
rotationDial?.isHidden = false
adaptAngleDashboardToCropBox()
toggleRotationControlViewIsNeeded(isHidden: false)
adaptRotationControlViewToCropBoxIfNeeded()
cropMaskViewManager.showVisualEffectBackground(animated: true)
checkImageStatusChanged()
}
}

private func toggleRotationControlViewIsNeeded(isHidden: Bool) {
if rotationControlView?.isAttachedToCropView == true {
rotationControlView?.isHidden = isHidden
}
}

private func imageStatusChanged() -> Bool {
if viewModel.getTotalRadians() != 0 { return true }
Expand Down Expand Up @@ -212,7 +218,7 @@ class CropView: UIView {
cropWorkbenchView.resetImageContent(by: viewModel.cropBoxFrame)
cropAuxiliaryIndicatorView.bringSelfToFront()

setupRotationDial()
setupRotationDialIfNeeded()

if aspectRatioLockEnabled {
setFixedRatioCropBox()
Expand Down Expand Up @@ -242,55 +248,63 @@ class CropView: UIView {
addSubview(cropAuxiliaryIndicatorView)
}

private func setupRotationDial() {
guard cropViewConfig.showRotationDial, let rotationDial = rotationDial else {
private func setupRotationDialIfNeeded() {
guard let rotationControlView = rotationControlView else {
return
}

rotationDial.reset()

let boardLength = min(bounds.width, bounds.height) * 0.6
let dialFrame = CGRect(x: 0,
y: 0,
width: boardLength,
height: angleDashboardHeight)
rotationDial.setup(with: dialFrame)
rotationDial.isUserInteractionEnabled = true

rotationDial.setRotationCenter(by: cropAuxiliaryIndicatorView.center, of: self)

rotationDial.didRotate = { [unowned self] angle in
rotationControlView.reset()
rotationControlView.isUserInteractionEnabled = true

rotationControlView.didUpdateRotationValue = { [unowned self] angle in
self.viewModel.setTouchRotationBoardStatus()
self.viewModel.setRotatingStatus(by: angle)
}

rotationDial.didFinishedRotate = { [unowned self] in
rotationControlView.didFinishRotation = { [unowned self] in
self.viewModel.setBetweenOperationStatus()
}

if rotationControlView.isAttachedToCropView {
let boardLength = min(bounds.width, bounds.height) * 0.6
let dialFrame = CGRect(x: 0,
y: 0,
width: boardLength,
height: cropViewConfig.rotationControlViewHeight)

rotationControlView.setupUI(withAllowableFrame: dialFrame)
}

rotationDial.rotateDialPlate(by: Angle(radians: viewModel.radians))
rotationDial.bringSelfToFront()
if let rotationDial = rotationControlView as? RotationDialProtocol {
rotationDial.setRotationCenter(by: cropAuxiliaryIndicatorView.center, of: self)
}

adaptAngleDashboardToCropBox()
rotationControlView.updateRotationValue(by: Angle(radians: viewModel.radians))
viewModel.setBetweenOperationStatus()

adaptRotationControlViewToCropBoxIfNeeded()
rotationControlView.bringSelfToFront()
}

private func adaptAngleDashboardToCropBox() {
guard let rotationDial = rotationDial else { return }
private func adaptRotationControlViewToCropBoxIfNeeded() {
guard let rotationControlView = rotationControlView,
rotationControlView.isAttachedToCropView else { return }

if Orientation.treatAsPortrait {
rotationDial.transform = CGAffineTransform(rotationAngle: 0)
rotationDial.frame.origin.x = cropAuxiliaryIndicatorView.frame.origin.x +
(cropAuxiliaryIndicatorView.frame.width - rotationDial.frame.width) / 2
rotationDial.frame.origin.y = cropAuxiliaryIndicatorView.frame.maxY
rotationControlView.transform = CGAffineTransform(rotationAngle: 0)
rotationControlView.frame.origin.x = cropAuxiliaryIndicatorView.frame.origin.x +
(cropAuxiliaryIndicatorView.frame.width - rotationControlView.frame.width) / 2
rotationControlView.frame.origin.y = cropAuxiliaryIndicatorView.frame.maxY
} else if Orientation.isLandscapeLeft {
rotationDial.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
rotationDial.frame.origin.x = cropAuxiliaryIndicatorView.frame.maxX
rotationDial.frame.origin.y = cropAuxiliaryIndicatorView.frame.origin.y +
(cropAuxiliaryIndicatorView.frame.height - rotationDial.frame.height) / 2
rotationControlView.transform = CGAffineTransform(rotationAngle: -CGFloat.pi / 2)
rotationControlView.frame.origin.x = cropAuxiliaryIndicatorView.frame.maxX
rotationControlView.frame.origin.y = cropAuxiliaryIndicatorView.frame.origin.y +
(cropAuxiliaryIndicatorView.frame.height - rotationControlView.frame.height) / 2
} else if Orientation.isLandscapeRight {
rotationDial.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
rotationDial.frame.origin.x = cropAuxiliaryIndicatorView.frame.minX - rotationDial.frame.width
rotationDial.frame.origin.y = cropAuxiliaryIndicatorView.frame.origin.y +
(cropAuxiliaryIndicatorView.frame.height - rotationDial.frame.height) / 2
rotationControlView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2)
rotationControlView.frame.origin.x = cropAuxiliaryIndicatorView.frame.minX - rotationControlView.frame.width
rotationControlView.frame.origin.y = cropAuxiliaryIndicatorView.frame.origin.y +
(cropAuxiliaryIndicatorView.frame.height - rotationControlView.frame.height) / 2
}
}

Expand Down Expand Up @@ -411,21 +425,27 @@ extension CropView {
let rect = self.bounds
var contentRect = CGRect.zero

var rotationControlViewHeight: CGFloat = 0

if cropViewConfig.showAttachedRotationControlView && rotationControlView?.isAttachedToCropView == true {
rotationControlViewHeight = cropViewConfig.rotationControlViewHeight
}

if Orientation.treatAsPortrait {
contentRect.origin.x = rect.origin.x + cropViewPadding
contentRect.origin.y = rect.origin.y + cropViewPadding

contentRect.size.width = rect.width - 2 * cropViewPadding
contentRect.size.height = rect.height - 2 * cropViewPadding - angleDashboardHeight
contentRect.size.height = rect.height - 2 * cropViewPadding - rotationControlViewHeight
} else if Orientation.isLandscape {
contentRect.size.width = rect.width - 2 * cropViewPadding - angleDashboardHeight
contentRect.size.width = rect.width - 2 * cropViewPadding - rotationControlViewHeight
contentRect.size.height = rect.height - 2 * cropViewPadding

contentRect.origin.y = rect.origin.y + cropViewPadding
if Orientation.isLandscapeLeft {
contentRect.origin.x = rect.origin.x + cropViewPadding
} else {
contentRect.origin.x = rect.origin.x + cropViewPadding + angleDashboardHeight
contentRect.origin.x = rect.origin.x + cropViewPadding + rotationControlViewHeight
}
}

Expand Down Expand Up @@ -690,13 +710,7 @@ extension CropView {
func getTotalRadians() -> CGFloat {
return viewModel.getTotalRadians()
}

private func setRotation(byRadians radians: CGFloat) {
cropWorkbenchView.transform = CGAffineTransform(rotationAngle: radians)
updatePosition(by: radians)
rotationDial?.rotateDialPlate(to: Angle(radians: radians), animated: false)
}


func setFixedRatioCropBox(zoom: Bool = true, cropBox: CGRect? = nil) {
let refCropBox = cropBox ?? getInitialCropBoxRect()
let imageHorizontalToVerticalRatio = ImageHorizontalToVerticalRatio(ratio: getImageHorizontalToVerticalRatio())
Expand All @@ -711,7 +725,7 @@ extension CropView {
self.viewModel.setBetweenOperationStatus()
}

adaptAngleDashboardToCropBox()
adaptRotationControlViewToCropBoxIfNeeded()
cropWorkbenchView.updateMinZoomScale()
}

Expand Down Expand Up @@ -884,7 +898,7 @@ extension CropView: CropViewProtocol {
}
}

func transform(byTransformInfo transformation: Transformation, rotateDial: Bool = true) {
func transform(byTransformInfo transformation: Transformation, isUpdateRotationControlView: Bool = true) {
viewModel.setRotatingStatus(by: Angle(radians: transformation.rotation))

if transformation.cropWorkbenchViewBounds != .zero {
Expand All @@ -900,9 +914,9 @@ extension CropView: CropViewProtocol {
viewModel.cropBoxFrame = transformation.maskFrame
}

if rotateDial {
rotationDial?.rotateDialPlate(by: Angle(radians: viewModel.radians))
adaptAngleDashboardToCropBox()
if isUpdateRotationControlView {
rotationControlView?.updateRotationValue(by: Angle(radians: viewModel.radians))
adaptRotationControlViewToCropBoxIfNeeded()
}
}

Expand Down Expand Up @@ -979,7 +993,7 @@ extension CropView: CropViewProtocol {
var newTransform = getTransformInfo(byTransformInfo: transformInfo)

// The first transform is just for retrieving the final cropBoxFrame
transform(byTransformInfo: newTransform, rotateDial: false)
transform(byTransformInfo: newTransform, isUpdateRotationControlView: false)

// The second transform is for adjusting the scale of transformInfo
let adjustScale = (viewModel.cropBoxFrame.width / viewModel.cropBoxOriginFrame.width)
Expand Down
17 changes: 13 additions & 4 deletions Sources/Mantis/CropViewConfig.swift
Expand Up @@ -42,12 +42,19 @@ public struct CropViewConfig {
public var maximumZoomScale: CGFloat = 15

/**
Rotation Dial currently is tightly coupled with other parts of CropView, we see rotation dial as a part of CropView,
so we put dialConfig inside CropViewConfig
Rotation control view currently is tightly coupled with other parts of CropView, we see rotation control view as a part of CropView,
so we put RotationControlViewConfig inside CropViewConfig
*/
public var dialConfig = DialConfig()
public var rotationControlViewConfig = RotationControlViewConfig()

public var showRotationDial = true
@available(*, deprecated, message: "Use showAttachedRotationControlView instead")
public var showRotationDial = true {
didSet {
showAttachedRotationControlView = showRotationDial
}
}

public var showAttachedRotationControlView = true

public var padding: CGFloat = 14 {
didSet {
Expand All @@ -59,6 +66,8 @@ public struct CropViewConfig {

public var cropActivityIndicatorSize = CGSize(width: 100, height: 100)

public var rotationControlViewHeight: CGFloat = 60

var minimumCropBoxSize: CGFloat = 42

public var disableCropBoxDeformation = false
Expand Down

0 comments on commit 07d1b32

Please sign in to comment.