Skip to content

Commit

Permalink
feat: add mirror mode support (#200)
Browse files Browse the repository at this point in the history
* feat: support mirror mode

issues to be solved
UI is broken when rotating device

* fix: restore getting leftTopPoint

rename handleRotate to handleDeviceRotated

chore: remove deprecated comment

* Update Sources/Mantis/CropViewController/CropToolbar.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropToolbar.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropToolbar.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropToolbar.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropToolbarProtocol.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropToolbarProtocol.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropViewController.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* Update Sources/Mantis/CropViewController/CropViewController.swift

Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>

* chore: remove public for ImageRotationType

* chore: use renamed CropToolbarDelegate functions

use didSelectHorizontallyFlip and didSelectVerticallyFlip

Co-authored-by: Yingtao Guo <yingtguo@cisco.com>
Co-authored-by: Niklas Baudy <niklas.baudy@vanniktech.de>
  • Loading branch information
3 people committed Aug 27, 2022
1 parent 42bb6cc commit ea6b1f1
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 29 deletions.
2 changes: 1 addition & 1 deletion Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class ViewController: UIViewController, CropViewControllerDelegate {
}

var config = Mantis.Config()
config.cropToolbarConfig.toolbarButtonOptions = [.clockwiseRotate, .reset, .ratio, .alterCropper90Degree]
config.cropToolbarConfig.toolbarButtonOptions = [.clockwiseRotate, .reset, .ratio, .horizontallyFlip, .verticallyFlip]
config.cropToolbarConfig.backgroundColor = .white
config.cropToolbarConfig.foregroundColor = .gray
config.cropToolbarConfig.ratioCandidatesShowType = .alwaysShowRatioList
Expand Down
106 changes: 93 additions & 13 deletions Sources/Mantis/CropView/CropView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,9 +367,25 @@ class CropView: UIView {
extension CropView {
private func rotateScrollView() {
let totalRadians = forceFixedRatio ? viewModel.radians : viewModel.getTotalRadians()
scrollView.transform = CGAffineTransform(rotationAngle: totalRadians)

self.scrollView.transform = CGAffineTransform(rotationAngle: totalRadians)
self.updatePosition(by: totalRadians)
if viewModel.horizontallyFlip {
if viewModel.rotationType.isRotateByMultiple180 {
scrollView.transform = scrollView.transform.scaledBy(x: -1, y: 1)
} else {
scrollView.transform = scrollView.transform.scaledBy(x: 1, y: -1)
}
}

if viewModel.verticallyFlip {
if viewModel.rotationType.isRotateByMultiple180 {
scrollView.transform = scrollView.transform.scaledBy(x: 1, y: -1)
} else {
scrollView.transform = scrollView.transform.scaledBy(x: -1, y: 1)
}
}

updatePosition(by: totalRadians)
}

private func getInitialCropBoxRect() -> CGRect {
Expand Down Expand Up @@ -540,7 +556,7 @@ extension CropView {
let height = abs(sin(radians)) * gridOverlayView.frame.width + abs(cos(radians)) * gridOverlayView.frame.height

let newSize: CGSize
if viewModel.rotationType == .none || viewModel.rotationType == .counterclockwise180 {
if viewModel.rotationType.isRotateByMultiple180 {
newSize = CGSize(width: width, height: height)
} else {
newSize = CGSize(width: height, height: width)
Expand Down Expand Up @@ -651,14 +667,34 @@ extension CropView {
let zeroPoint = gridOverlayView.center

let translation = CGPoint(x: (point.x - zeroPoint.x), y: (point.y - zeroPoint.y))

var scaleX = scrollView.zoomScale
var scaleY = scrollView.zoomScale

if viewModel.horizontallyFlip {
if viewModel.rotationType.isRotateByMultiple180 {
scaleX = -scaleX
} else {
scaleY = -scaleY
}
}

if viewModel.verticallyFlip {
if viewModel.rotationType.isRotateByMultiple180 {
scaleY = -scaleY
} else {
scaleX = -scaleX
}
}

return CropInfo(
translation: translation,
rotation: getTotalRadians(),
scale: scrollView.zoomScale,
scaleX: scaleX,
scaleY: scaleY,
cropSize: gridOverlayView.frame.size,
imageViewSize: imageContainer.bounds.size
)
)
}

func getTotalRadians() -> CGFloat {
Expand All @@ -669,10 +705,10 @@ extension CropView {
return crop(image)
}

func handleRotate() {
func handleDeviceRotated() {
viewModel.resetCropFrame(by: getInitialCropBoxRect())

scrollView.transform = .identity
scrollView.transform = CGAffineTransform(scaleX: 1, y: 1)
scrollView.resetBy(rect: viewModel.cropBoxFrame)

setupAngleDashboard()
Expand Down Expand Up @@ -727,17 +763,24 @@ extension CropView {

let newRect = GeometryHelper.getInscribeRect(fromOutsideRect: getContentBounds(), andInsideRect: rect)

let radian = rotateAngle
let transfrom = scrollView.transform.rotated(by: radian)
var newRotateAngle = rotateAngle

if viewModel.horizontallyFlip {
newRotateAngle = -newRotateAngle
}

if viewModel.verticallyFlip {
newRotateAngle = -newRotateAngle
}

UIView.animate(withDuration: rorateDuration, animations: {
self.viewModel.cropBoxFrame = newRect
self.scrollView.transform = transfrom
self.updatePositionFor90Rotation(by: radian + self.viewModel.radians)
self.scrollView.transform = self.scrollView.transform.rotated(by: newRotateAngle)
self.updatePositionFor90Rotation(by: newRotateAngle + self.viewModel.radians)
}, completion: {[weak self] _ in
guard let self = self else { return }
self.scrollView.updateMinZoomScale()
self.viewModel.rotateBy90(rotateAngle: rotateAngle)
self.viewModel.rotateBy90(rotateAngle: newRotateAngle)
self.viewModel.setBetweenOperationStatus()
completion()
})
Expand Down Expand Up @@ -793,7 +836,7 @@ extension CropView {
}

func getImageRatioH() -> Double {
if viewModel.rotationType == .none || viewModel.rotationType == .counterclockwise180 {
if viewModel.rotationType.isRotateByMultiple180 {
return Double(image.ratioH())
} else {
return Double(1/image.ratioH())
Expand Down Expand Up @@ -825,4 +868,41 @@ extension CropView {
func getExpectedCropImageSize() -> CGSize {
image.getExpectedCropImageSize(by: getCropInfo())
}

func horizontallyFlip() {
viewModel.horizontallyFlip.toggle()
flip(isHorizontal: true)
}

func verticallyFlip() {
viewModel.verticallyFlip.toggle()
flip(isHorizontal: false)
}

private func flip(isHorizontal: Bool = true, animated: Bool = true) {
var scaleX: CGFloat = 1
var scaleY: CGFloat = 1

if isHorizontal {
if viewModel.rotationType.isRotateByMultiple180 {
scaleX = -scaleX
} else {
scaleY = -scaleY
}
} else {
if viewModel.rotationType.isRotateByMultiple180 {
scaleY = -scaleY
} else {
scaleX = -scaleX
}
}

if animated {
UIView.animate(withDuration: 0.5) {
self.scrollView.transform = self.scrollView.transform.scaledBy(x: scaleX, y: scaleY)
}
} else {
scrollView.transform = scrollView.transform.scaledBy(x: scaleX, y: scaleY)
}
}
}
9 changes: 9 additions & 0 deletions Sources/Mantis/CropView/CropViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ enum ImageRotationType: CGFloat {
self = .counterclockwise270
}
}

var isRotateByMultiple180: Bool {
return self == .none || self == .counterclockwise180
}
}

class CropViewModel: NSObject {
Expand Down Expand Up @@ -70,11 +74,16 @@ class CropViewModel: NSObject {
var aspectRatio: CGFloat = -1
var cropLeftTopOnImage: CGPoint = .zero
var cropRightBottomOnImage: CGPoint = CGPoint(x: 1, y: 1)

var horizontallyFlip = false
var verticallyFlip = false

private let cropViewPadding: CGFloat
private let hotAreaUnit: CGFloat

func reset(forceFixedRatio: Bool = false) {
horizontallyFlip = false
verticallyFlip = false
cropBoxFrame = .zero
degrees = 0
rotationType = .none
Expand Down
32 changes: 31 additions & 1 deletion Sources/Mantis/CropViewController/CropToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ public class CropToolbar: UIView, CropToolbarProtocol {
button.setImage(icon, for: .normal)
return button
}()

private lazy var horizontallyFlipButton: UIButton = {
let button = createOptionButton(withTitle: nil, andAction: #selector(horizontallyFlip))
let icon = iconProvider?.getHorizontallyFlipIcon() ?? ToolBarButtonImageBuilder.horizontallyFlipImage()
button.setImage(icon, for: .normal)
return button
}()

private lazy var verticallyFlipButton: UIButton = {
let button = createOptionButton(withTitle: nil, andAction: #selector(verticallyFlip(_:)))
let icon = iconProvider?.getVerticallyFlipIcon() ?? ToolBarButtonImageBuilder.verticallyFlipImage()
button.setImage(icon, for: .normal)
return button
}()

private lazy var fixedRatioSettingButton: UIButton = {
let button = createOptionButton(withTitle: nil, andAction: #selector(setRatio))
Expand Down Expand Up @@ -94,7 +108,7 @@ public class CropToolbar: UIView, CropToolbarProtocol {
if config.mode == .normal {
addButtonsToContainer(button: cancelButton)
}

if config.toolbarButtonOptions.contains(.counterclockwiseRotate) {
addButtonsToContainer(button: counterClockwiseRotationButton)
}
Expand All @@ -106,6 +120,14 @@ public class CropToolbar: UIView, CropToolbarProtocol {
if config.toolbarButtonOptions.contains(.alterCropper90Degree) {
addButtonsToContainer(button: alterCropper90DegreeButton)
}

if config.toolbarButtonOptions.contains(.horizontallyFlip) {
addButtonsToContainer(button: horizontallyFlipButton)
}

if config.toolbarButtonOptions.contains(.verticallyFlip) {
addButtonsToContainer(button: verticallyFlipButton)
}

if config.toolbarButtonOptions.contains(.reset) {
let icon = iconProvider?.getResetIcon() ?? ToolBarButtonImageBuilder.resetImage()
Expand Down Expand Up @@ -203,6 +225,14 @@ extension CropToolbar {
@objc private func alterCropper90Degree(_ sender: Any) {
cropToolbarDelegate?.didSelectAlterCropper90Degree()
}

@objc private func horizontallyFlip(_ sender: Any) {
cropToolbarDelegate?.didSelectHorizontallyFlip()
}

@objc private func verticallyFlip(_ sender: Any) {
cropToolbarDelegate?.didSelectVerticallyFlip()
}

@objc private func crop(_ sender: Any) {
cropToolbarDelegate?.didSelectCrop()
Expand Down
9 changes: 9 additions & 0 deletions Sources/Mantis/CropViewController/CropToolbarProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import UIKit

/**
Inside Mantis, CropViewController implements all delegate methods
*/
public protocol CropToolbarDelegate: AnyObject {
func didSelectCancel()
func didSelectCrop()
Expand All @@ -16,6 +19,8 @@ public protocol CropToolbarDelegate: AnyObject {
func didSelectSetRatio()
func didSelectRatio(ratio: Double)
func didSelectAlterCropper90Degree()
func didSelectHorizontallyFlip()
func didSelectVerticallyFlip()
}

public protocol CropToolbarIconProvider: AnyObject {
Expand All @@ -26,6 +31,8 @@ public protocol CropToolbarIconProvider: AnyObject {
func getAlterCropper90DegreeIcon() -> UIImage?
func getCancelIcon() -> UIImage?
func getCropIcon() -> UIImage?
func getHorizontallyFlipIcon() -> UIImage?
func getVerticallyFlipIcon() -> UIImage?
}

public extension CropToolbarIconProvider {
Expand All @@ -36,6 +43,8 @@ public extension CropToolbarIconProvider {
func getAlterCropper90DegreeIcon() -> UIImage? { return nil }
func getCancelIcon() -> UIImage? { return nil }
func getCropIcon() -> UIImage? { return nil }
func getHorizontallyFlipIcon() -> UIImage? { return nil }
func getVerticallyFlipIcon() -> UIImage? { return nil }
}

public protocol CropToolbarProtocol: UIView {
Expand Down
24 changes: 20 additions & 4 deletions Sources/Mantis/CropViewController/CropViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ public class CropViewController: UIViewController {
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
cropView.prepareForDeviceRotation()
rotated()
handleDeviceRotated()
}

@objc func rotated() {
@objc func handleDeviceRotated() {
let currentOrientation = Orientation.interfaceOrientation

guard currentOrientation != .unknown else { return }
Expand All @@ -226,7 +226,7 @@ public class CropViewController: UIViewController {
// So delay the execution to make sure handleRotate runs after the final
// viewDidLayoutSubviews
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.cropView.handleRotate()
self?.cropView.handleDeviceRotated()
}
}

Expand Down Expand Up @@ -432,6 +432,14 @@ public class CropViewController: UIViewController {
}
}

private func handleHorizontallyFlip() {
cropView.horizontallyFlip()
}

private func handleVerticallyFlip() {
cropView.verticallyFlip()
}

private func handleCrop() {
let cropResult = cropView.crop()
guard let image = cropResult.croppedImage else {
Expand Down Expand Up @@ -525,6 +533,14 @@ extension CropViewController: CropViewDelegate {
}

extension CropViewController: CropToolbarDelegate {
public func didSelectHorizontallyFlip() {
handleHorizontallyFlip()
}

public func didSelectVerticallyFlip() {
handleVerticallyFlip()
}

public func didSelectCancel() {
handleCancel()
}
Expand Down Expand Up @@ -555,7 +571,7 @@ extension CropViewController: CropToolbarDelegate {

public func didSelectAlterCropper90Degree() {
handleAlterCropper90Degree()
}
}
}

// API
Expand Down
32 changes: 32 additions & 0 deletions Sources/Mantis/CropViewController/ToolBarButtonImageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,36 @@ struct ToolBarButtonImageBuilder {
static func alterCropper90DegreeImage() -> UIImage? {
drawAlterCropper90DegreeImage()
}

static func horizontallyFlipImage() -> UIImage? {
if #available(macCatalyst 13.1, iOS 13.0, *) {
return UIImage(systemName: "flip.horizontal")
}

return nil
}

static func verticallyFlipImage() -> UIImage? {
if #available(macCatalyst 13.1, iOS 13.0, *) {
guard let horizontallyFippedImage = horizontallyFlipImage(),
let cgImage = horizontallyFippedImage.cgImage else {
return nil
}

UIGraphicsBeginImageContextWithOptions(CGSize(width: horizontallyFippedImage.size.height,
height: horizontallyFippedImage.size.width),
false,
horizontallyFippedImage.scale)
let context = UIGraphicsGetCurrentContext()
context?.rotate(by: .pi / 2)
context?.translateBy(x: 0, y: -horizontallyFippedImage.size.height)
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: horizontallyFippedImage.size.height, height: horizontallyFippedImage.size.width))
let image: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}

return nil
}

}

0 comments on commit ea6b1f1

Please sign in to comment.