CropView.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. //
  2. // CropView.swift
  3. // CropViewController
  4. //
  5. // Created by Guilherme Moura on 2/25/16.
  6. // Copyright © 2016 Reefactor, Inc. All rights reserved.
  7. // Credit https://github.com/sprint84/PhotoCropEditor
  8. import UIKit
  9. import AVFoundation
  10. open class CropView: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate, CropRectViewDelegate {
  11. open var image: UIImage? {
  12. didSet {
  13. if image != nil {
  14. imageSize = image!.size
  15. }
  16. imageView?.removeFromSuperview()
  17. imageView = nil
  18. zoomingView?.removeFromSuperview()
  19. zoomingView = nil
  20. setNeedsLayout()
  21. }
  22. }
  23. open var imageView: UIView? {
  24. didSet {
  25. if let view = imageView , image == nil {
  26. imageSize = view.frame.size
  27. }
  28. usingCustomImageView = true
  29. setNeedsLayout()
  30. }
  31. }
  32. open var croppedImage: UIImage? {
  33. return image?.rotatedImageWithTransform(rotation, croppedToRect: zoomedCropRect())
  34. }
  35. open var keepAspectRatio = false {
  36. didSet {
  37. cropRectView.keepAspectRatio = keepAspectRatio
  38. }
  39. }
  40. open var cropAspectRatio: CGFloat {
  41. set {
  42. setCropAspectRatio(newValue, shouldCenter: true)
  43. }
  44. get {
  45. let rect = scrollView.frame
  46. let width = rect.width
  47. let height = rect.height
  48. return width / height
  49. }
  50. }
  51. open var rotation: CGAffineTransform {
  52. guard let imgView = imageView else {
  53. return CGAffineTransform.identity
  54. }
  55. return imgView.transform
  56. }
  57. open var rotationAngle: CGFloat {
  58. set {
  59. imageView?.transform = CGAffineTransform(rotationAngle: newValue)
  60. }
  61. get {
  62. return atan2(rotation.b, rotation.a)
  63. }
  64. }
  65. open var cropRect: CGRect {
  66. set {
  67. zoomToCropRect(newValue)
  68. }
  69. get {
  70. return scrollView.frame
  71. }
  72. }
  73. open var imageCropRect = CGRect.zero {
  74. didSet {
  75. resetCropRect()
  76. let scale = min(scrollView.frame.width / imageSize.width, scrollView.frame.height / imageSize.height)
  77. let x = imageCropRect.minX * scale + scrollView.frame.minX
  78. let y = imageCropRect.minY * scale + scrollView.frame.minY
  79. let width = imageCropRect.width * scale
  80. let height = imageCropRect.height * scale
  81. let rect = CGRect(x: x, y: y, width: width, height: height)
  82. let intersection = rect.intersection(scrollView.frame)
  83. if !intersection.isNull {
  84. cropRect = intersection
  85. }
  86. }
  87. }
  88. open var resizeEnabled = true {
  89. didSet {
  90. cropRectView.enableResizing(resizeEnabled)
  91. }
  92. }
  93. open var showCroppedArea = true {
  94. didSet {
  95. layoutIfNeeded()
  96. scrollView.clipsToBounds = !showCroppedArea
  97. showOverlayView(showCroppedArea)
  98. }
  99. }
  100. open var rotationGestureRecognizer: UIRotationGestureRecognizer!
  101. fileprivate var imageSize = CGSize(width: 1.0, height: 1.0)
  102. fileprivate var scrollView: UIScrollView!
  103. fileprivate var zoomingView: UIView?
  104. fileprivate let cropRectView = CropRectView()
  105. fileprivate let topOverlayView = UIView()
  106. fileprivate let leftOverlayView = UIView()
  107. fileprivate let rightOverlayView = UIView()
  108. fileprivate let bottomOverlayView = UIView()
  109. fileprivate var insetRect = CGRect.zero
  110. fileprivate var editingRect = CGRect.zero
  111. fileprivate var interfaceOrientation = UIApplication.shared.statusBarOrientation
  112. fileprivate var resizing = false
  113. fileprivate var usingCustomImageView = false
  114. fileprivate let MarginTop: CGFloat = 37.0
  115. fileprivate let MarginLeft: CGFloat = 20.0
  116. public override init(frame: CGRect) {
  117. super.init(frame: frame)
  118. initialize()
  119. }
  120. public required init?(coder aDecoder: NSCoder) {
  121. super.init(coder: aDecoder)
  122. initialize()
  123. }
  124. fileprivate func initialize() {
  125. autoresizingMask = [.flexibleWidth, .flexibleHeight]
  126. backgroundColor = UIColor.clear
  127. scrollView = UIScrollView(frame: bounds)
  128. scrollView.delegate = self
  129. scrollView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleBottomMargin, .flexibleRightMargin]
  130. scrollView.backgroundColor = UIColor.clear
  131. scrollView.maximumZoomScale = 20.0
  132. scrollView.minimumZoomScale = 1.0
  133. scrollView.showsHorizontalScrollIndicator = false
  134. scrollView.showsVerticalScrollIndicator = false
  135. scrollView.bounces = false
  136. scrollView.bouncesZoom = false
  137. scrollView.clipsToBounds = false
  138. addSubview(scrollView)
  139. rotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(CropView.handleRotation(_:)))
  140. rotationGestureRecognizer?.delegate = self
  141. scrollView.addGestureRecognizer(rotationGestureRecognizer)
  142. cropRectView.delegate = self
  143. addSubview(cropRectView)
  144. showOverlayView(showCroppedArea)
  145. addSubview(topOverlayView)
  146. addSubview(leftOverlayView)
  147. addSubview(rightOverlayView)
  148. addSubview(bottomOverlayView)
  149. }
  150. open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
  151. if !isUserInteractionEnabled {
  152. return nil
  153. }
  154. if let hitView = cropRectView.hitTest(convert(point, to: cropRectView), with: event) {
  155. return hitView
  156. }
  157. let locationInImageView = convert(point, to: zoomingView)
  158. let zoomedPoint = CGPoint(x: locationInImageView.x * scrollView.zoomScale, y: locationInImageView.y * scrollView.zoomScale)
  159. if zoomingView!.frame.contains(zoomedPoint) {
  160. return scrollView
  161. }
  162. return super.hitTest(point, with: event)
  163. }
  164. open override func layoutSubviews() {
  165. super.layoutSubviews()
  166. let interfaceOrientation = UIApplication.shared.statusBarOrientation
  167. if image == nil && imageView == nil {
  168. return
  169. }
  170. setupEditingRect()
  171. if imageView == nil {
  172. if interfaceOrientation.isPortrait {
  173. insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginTop)
  174. } else {
  175. insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginLeft)
  176. }
  177. if !showCroppedArea {
  178. insetRect = editingRect
  179. }
  180. setupZoomingView()
  181. setupImageView()
  182. } else if usingCustomImageView {
  183. if interfaceOrientation.isPortrait {
  184. insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginTop)
  185. } else {
  186. insetRect = bounds.insetBy(dx: MarginLeft, dy: MarginLeft)
  187. }
  188. if !showCroppedArea {
  189. insetRect = editingRect
  190. }
  191. setupZoomingView()
  192. imageView?.frame = zoomingView!.bounds
  193. zoomingView?.addSubview(imageView!)
  194. usingCustomImageView = false
  195. }
  196. if !resizing {
  197. layoutCropRectViewWithCropRect(scrollView.frame)
  198. if self.interfaceOrientation != interfaceOrientation {
  199. zoomToCropRect(scrollView.frame)
  200. }
  201. }
  202. self.interfaceOrientation = interfaceOrientation
  203. }
  204. open func setRotationAngle(_ rotationAngle: CGFloat, snap: Bool) {
  205. var rotation = rotationAngle
  206. if snap {
  207. rotation = nearbyint(rotationAngle / CGFloat(Double.pi/2)) * CGFloat(Double.pi/2)
  208. }
  209. self.rotationAngle = rotation
  210. }
  211. open func resetCropRect() {
  212. resetCropRectAnimated(false)
  213. }
  214. open func resetCropRectAnimated(_ animated: Bool) {
  215. if animated {
  216. UIView.beginAnimations(nil, context: nil)
  217. UIView.setAnimationDuration(0.25)
  218. UIView.setAnimationBeginsFromCurrentState(true)
  219. }
  220. imageView?.transform = CGAffineTransform.identity
  221. let contentSize = scrollView.contentSize
  222. let initialRect = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
  223. scrollView.zoom(to: initialRect, animated: false)
  224. layoutCropRectViewWithCropRect(scrollView.bounds)
  225. if animated {
  226. UIView.commitAnimations()
  227. }
  228. }
  229. open func zoomedCropRect() -> CGRect {
  230. let cropRect = convert(scrollView.frame, to: zoomingView)
  231. var ratio: CGFloat = 1.0
  232. let orientation = UIApplication.shared.statusBarOrientation
  233. if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.pad || orientation.isPortrait) {
  234. ratio = AVMakeRect(aspectRatio: imageSize, insideRect: insetRect).width / imageSize.width
  235. } else {
  236. ratio = AVMakeRect(aspectRatio: imageSize, insideRect: insetRect).height / imageSize.height
  237. }
  238. let zoomedCropRect = CGRect(x: cropRect.origin.x / ratio,
  239. y: cropRect.origin.y / ratio,
  240. width: cropRect.size.width / ratio,
  241. height: cropRect.size.height / ratio)
  242. return zoomedCropRect
  243. }
  244. open func croppedImage(_ image: UIImage) -> UIImage {
  245. imageSize = image.size
  246. return image.rotatedImageWithTransform(rotation, croppedToRect: zoomedCropRect())
  247. }
  248. @objc func handleRotation(_ gestureRecognizer: UIRotationGestureRecognizer) {
  249. if let imageView = imageView {
  250. let rotation = gestureRecognizer.rotation
  251. let transform = imageView.transform.rotated(by: rotation)
  252. imageView.transform = transform
  253. gestureRecognizer.rotation = 0.0
  254. }
  255. switch gestureRecognizer.state {
  256. case .began, .changed:
  257. cropRectView.showsGridMinor = true
  258. default:
  259. cropRectView.showsGridMinor = false
  260. }
  261. }
  262. // MARK: - Private methods
  263. fileprivate func showOverlayView(_ show: Bool) {
  264. let color = show ? UIColor(white: 0.0, alpha: 0.4) : UIColor.clear
  265. topOverlayView.backgroundColor = color
  266. leftOverlayView.backgroundColor = color
  267. rightOverlayView.backgroundColor = color
  268. bottomOverlayView.backgroundColor = color
  269. }
  270. fileprivate func setupEditingRect() {
  271. let interfaceOrientation = UIApplication.shared.statusBarOrientation
  272. if interfaceOrientation.isPortrait {
  273. editingRect = bounds.insetBy(dx: MarginLeft, dy: MarginTop)
  274. } else {
  275. editingRect = bounds.insetBy(dx: MarginLeft, dy: MarginLeft)
  276. }
  277. if !showCroppedArea {
  278. editingRect = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height)
  279. }
  280. }
  281. fileprivate func setupZoomingView() {
  282. let cropRect = AVMakeRect(aspectRatio: imageSize, insideRect: insetRect)
  283. scrollView.frame = cropRect
  284. scrollView.contentSize = cropRect.size
  285. zoomingView = UIView(frame: scrollView.bounds)
  286. zoomingView?.backgroundColor = .clear
  287. scrollView.addSubview(zoomingView!)
  288. }
  289. fileprivate func setupImageView() {
  290. let imageView = UIImageView(frame: zoomingView!.bounds)
  291. imageView.backgroundColor = .clear
  292. imageView.contentMode = .scaleAspectFit
  293. imageView.image = image
  294. zoomingView?.addSubview(imageView)
  295. self.imageView = imageView
  296. usingCustomImageView = false
  297. }
  298. fileprivate func layoutCropRectViewWithCropRect(_ cropRect: CGRect) {
  299. cropRectView.frame = cropRect
  300. layoutOverlayViewsWithCropRect(cropRect)
  301. }
  302. fileprivate func layoutOverlayViewsWithCropRect(_ cropRect: CGRect) {
  303. topOverlayView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: cropRect.minY)
  304. leftOverlayView.frame = CGRect(x: 0, y: cropRect.minY, width: cropRect.minX, height: cropRect.height)
  305. rightOverlayView.frame = CGRect(x: cropRect.maxX, y: cropRect.minY, width: bounds.width - cropRect.maxX, height: cropRect.height)
  306. bottomOverlayView.frame = CGRect(x: 0, y: cropRect.maxY, width: bounds.width, height: bounds.height - cropRect.maxY)
  307. }
  308. fileprivate func zoomToCropRect(_ toRect: CGRect) {
  309. zoomToCropRect(toRect, shouldCenter: false, animated: true)
  310. }
  311. fileprivate func zoomToCropRect(_ toRect: CGRect, shouldCenter: Bool, animated: Bool, completion: (() -> Void)? = nil) {
  312. if scrollView.frame.equalTo(toRect) {
  313. return
  314. }
  315. let width = toRect.width
  316. let height = toRect.height
  317. let scale = min(editingRect.width / width, editingRect.height / height)
  318. let scaledWidth = width * scale
  319. let scaledHeight = height * scale
  320. let cropRect = CGRect(x: (bounds.width - scaledWidth) / 2.0, y: (bounds.height - scaledHeight) / 2.0, width: scaledWidth, height: scaledHeight)
  321. var zoomRect = convert(toRect, to: zoomingView)
  322. zoomRect.size.width = cropRect.width / (scrollView.zoomScale * scale)
  323. zoomRect.size.height = cropRect.height / (scrollView.zoomScale * scale)
  324. if let imgView = imageView , shouldCenter {
  325. let imageViewBounds = imgView.bounds
  326. zoomRect.origin.x = (imageViewBounds.width / 2.0) - (zoomRect.width / 2.0)
  327. zoomRect.origin.y = (imageViewBounds.height / 2.0) - (zoomRect.height / 2.0)
  328. }
  329. var duration = 0.0
  330. if animated {
  331. duration = 0.25
  332. }
  333. UIView.animate(withDuration: duration, delay: 0.0, options: .beginFromCurrentState, animations: { [unowned self] in
  334. self.scrollView.bounds = cropRect
  335. self.scrollView.zoom(to: zoomRect, animated: false)
  336. self.layoutCropRectViewWithCropRect(cropRect)
  337. }) { finished in
  338. completion?()
  339. }
  340. }
  341. fileprivate func cappedCropRectInImageRectWithCropRectView(_ cropRectView: CropRectView) -> CGRect {
  342. var cropRect = cropRectView.frame
  343. let rect = convert(cropRect, to: scrollView)
  344. if rect.minX < zoomingView!.frame.minX {
  345. cropRect.origin.x = scrollView.convert(zoomingView!.frame, to: self).minX
  346. let cappedWidth = rect.maxX
  347. let height = !keepAspectRatio ? cropRect.size.height : cropRect.size.height * (cappedWidth / cropRect.size.width)
  348. cropRect.size = CGSize(width: cappedWidth, height: height)
  349. }
  350. if rect.minY < zoomingView!.frame.minY {
  351. cropRect.origin.y = scrollView.convert(zoomingView!.frame, to: self).minY
  352. let cappedHeight = rect.maxY
  353. let width = !keepAspectRatio ? cropRect.size.width : cropRect.size.width * (cappedHeight / cropRect.size.height)
  354. cropRect.size = CGSize(width: width, height: cappedHeight)
  355. }
  356. if rect.maxX > zoomingView!.frame.maxX {
  357. let cappedWidth = scrollView.convert(zoomingView!.frame, to: self).maxX - cropRect.minX
  358. let height = !keepAspectRatio ? cropRect.size.height : cropRect.size.height * (cappedWidth / cropRect.size.width)
  359. cropRect.size = CGSize(width: cappedWidth, height: height)
  360. }
  361. if rect.maxY > zoomingView!.frame.maxY {
  362. let cappedHeight = scrollView.convert(zoomingView!.frame, to: self).maxY - cropRect.minY
  363. let width = !keepAspectRatio ? cropRect.size.width : cropRect.size.width * (cappedHeight / cropRect.size.height)
  364. cropRect.size = CGSize(width: width, height: cappedHeight)
  365. }
  366. return cropRect
  367. }
  368. fileprivate func automaticZoomIfEdgeTouched(_ cropRect: CGRect) {
  369. if cropRect.minX < editingRect.minX - 5.0 ||
  370. cropRect.maxX > editingRect.maxX + 5.0 ||
  371. cropRect.minY < editingRect.minY - 5.0 ||
  372. cropRect.maxY > editingRect.maxY + 5.0 {
  373. UIView.animate(withDuration: 1.0, delay: 0.0, options: .beginFromCurrentState, animations: { [unowned self] in
  374. self.zoomToCropRect(self.cropRectView.frame)
  375. }, completion: nil)
  376. }
  377. }
  378. fileprivate func setCropAspectRatio(_ ratio: CGFloat, shouldCenter: Bool) {
  379. var cropRect = scrollView.frame
  380. var width = cropRect.width
  381. var height = cropRect.height
  382. if ratio <= 1.0 {
  383. width = height * ratio
  384. if width > imageView!.bounds.width {
  385. width = cropRect.width
  386. height = width / ratio
  387. }
  388. } else {
  389. height = width / ratio
  390. if height > imageView!.bounds.height {
  391. height = cropRect.height
  392. width = height * ratio
  393. }
  394. }
  395. cropRect.size = CGSize(width: width, height: height)
  396. zoomToCropRect(cropRect, shouldCenter: shouldCenter, animated: false) {
  397. let scale = self.scrollView.zoomScale
  398. self.scrollView.minimumZoomScale = scale
  399. }
  400. }
  401. // MARK: - CropView delegate methods
  402. func cropRectViewDidBeginEditing(_ view: CropRectView) {
  403. resizing = true
  404. }
  405. func cropRectViewDidChange(_ view: CropRectView) {
  406. let cropRect = cappedCropRectInImageRectWithCropRectView(view)
  407. layoutCropRectViewWithCropRect(cropRect)
  408. automaticZoomIfEdgeTouched(cropRect)
  409. }
  410. func cropRectViewDidEndEditing(_ view: CropRectView) {
  411. resizing = false
  412. zoomToCropRect(cropRectView.frame)
  413. }
  414. // MARK: - ScrollView delegate methods
  415. open func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  416. return zoomingView
  417. }
  418. open func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  419. let contentOffset = scrollView.contentOffset
  420. targetContentOffset.pointee = contentOffset
  421. }
  422. // MARK: - Gesture Recognizer delegate methods
  423. open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
  424. return true
  425. }
  426. }