QuadrilateralView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. //
  2. // RectangleView.swift
  3. // WeScan
  4. //
  5. // Created by Boris Emorine on 2/8/18.
  6. // Copyright © 2018 WeTransfer. All rights reserved.
  7. //
  8. import UIKit
  9. import AVFoundation
  10. /// Simple enum to keep track of the position of the corners of a quadrilateral.
  11. enum CornerPosition {
  12. case topLeft
  13. case topRight
  14. case bottomRight
  15. case bottomLeft
  16. }
  17. /// The `QuadrilateralView` is a simple `UIView` subclass that can draw a quadrilateral, and optionally edit it.
  18. final class QuadrilateralView: UIView {
  19. private let quadLayer: CAShapeLayer = {
  20. let layer = CAShapeLayer()
  21. layer.strokeColor = UIColor.white.cgColor
  22. layer.lineWidth = 1.0
  23. layer.opacity = 1.0
  24. layer.isHidden = true
  25. return layer
  26. }()
  27. /// We want the corner views to be displayed under the outline of the quadrilateral.
  28. /// Because of that, we need the quadrilateral to be drawn on a UIView above them.
  29. private let quadView: UIView = {
  30. let view = UIView()
  31. view.backgroundColor = UIColor.clear
  32. view.translatesAutoresizingMaskIntoConstraints = false
  33. return view
  34. }()
  35. /// The quadrilateral drawn on the view.
  36. private(set) var quad: Quadrilateral?
  37. public var editable = false {
  38. didSet {
  39. editable == true ? showCornerViews() : hideCornerViews()
  40. quadLayer.fillColor = editable ? UIColor(white: 0.0, alpha: 0.6).cgColor : UIColor(white: 1.0, alpha: 0.5).cgColor
  41. guard let quad = quad else {
  42. return
  43. }
  44. drawQuad(quad, animated: false)
  45. layoutCornerViews(forQuad: quad)
  46. }
  47. }
  48. private var isHighlighted = false {
  49. didSet (oldValue) {
  50. guard oldValue != isHighlighted else {
  51. return
  52. }
  53. quadLayer.fillColor = isHighlighted ? UIColor.clear.cgColor : UIColor(white: 0.0, alpha: 0.6).cgColor
  54. isHighlighted ? bringSubviewToFront(quadView) : sendSubviewToBack(quadView)
  55. }
  56. }
  57. lazy private var topLeftCornerView: EditScanCornerView = {
  58. return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .topLeft)
  59. }()
  60. lazy private var topRightCornerView: EditScanCornerView = {
  61. return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .topRight)
  62. }()
  63. lazy private var bottomRightCornerView: EditScanCornerView = {
  64. return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .bottomRight)
  65. }()
  66. lazy private var bottomLeftCornerView: EditScanCornerView = {
  67. return EditScanCornerView(frame: CGRect(x: 0.0, y: 0.0, width: cornerViewSize, height: cornerViewSize), position: .bottomLeft)
  68. }()
  69. private let highlightedCornerViewSize: CGFloat = 75.0
  70. private let cornerViewSize: CGFloat = 20.0
  71. // MARK: - Life Cycle
  72. override init(frame: CGRect) {
  73. super.init(frame: frame)
  74. commonInit()
  75. }
  76. required public init?(coder aDecoder: NSCoder) {
  77. fatalError("init(coder:) has not been implemented")
  78. }
  79. private func commonInit() {
  80. addSubview(quadView)
  81. setupCornerViews()
  82. setupConstraints()
  83. quadView.layer.addSublayer(quadLayer)
  84. }
  85. private func setupConstraints() {
  86. let quadViewConstraints = [
  87. quadView.topAnchor.constraint(equalTo: topAnchor),
  88. quadView.leadingAnchor.constraint(equalTo: leadingAnchor),
  89. bottomAnchor.constraint(equalTo: quadView.bottomAnchor),
  90. trailingAnchor.constraint(equalTo: quadView.trailingAnchor)
  91. ]
  92. NSLayoutConstraint.activate(quadViewConstraints)
  93. }
  94. private func setupCornerViews() {
  95. addSubview(topLeftCornerView)
  96. addSubview(topRightCornerView)
  97. addSubview(bottomRightCornerView)
  98. addSubview(bottomLeftCornerView)
  99. }
  100. override public func layoutSubviews() {
  101. super.layoutSubviews()
  102. guard quadLayer.frame != bounds else {
  103. return
  104. }
  105. quadLayer.frame = bounds
  106. if let quad = quad {
  107. drawQuadrilateral(quad: quad, animated: false)
  108. }
  109. }
  110. // MARK: - Drawings
  111. /// Draws the passed in quadrilateral.
  112. ///
  113. /// - Parameters:
  114. /// - quad: The quadrilateral to draw on the view. It should be in the coordinates of the current `QuadrilateralView` instance.
  115. func drawQuadrilateral(quad: Quadrilateral, animated: Bool) {
  116. self.quad = quad
  117. drawQuad(quad, animated: animated)
  118. if editable {
  119. showCornerViews()
  120. layoutCornerViews(forQuad: quad)
  121. }
  122. }
  123. private func drawQuad(_ quad: Quadrilateral, animated: Bool) {
  124. var path = quad.path()
  125. if editable {
  126. path = path.reversing()
  127. let rectPath = UIBezierPath(rect: bounds)
  128. path.append(rectPath)
  129. }
  130. if animated == true {
  131. let pathAnimation = CABasicAnimation(keyPath: "path")
  132. pathAnimation.duration = 0.2
  133. quadLayer.add(pathAnimation, forKey: "path")
  134. }
  135. quadLayer.path = path.cgPath
  136. quadLayer.isHidden = false
  137. }
  138. private func layoutCornerViews(forQuad quad: Quadrilateral) {
  139. topLeftCornerView.center = quad.topLeft
  140. topRightCornerView.center = quad.topRight
  141. bottomLeftCornerView.center = quad.bottomLeft
  142. bottomRightCornerView.center = quad.bottomRight
  143. }
  144. func removeQuadrilateral() {
  145. quadLayer.path = nil
  146. quadLayer.isHidden = true
  147. }
  148. // MARK: - Actions
  149. func moveCorner(cornerView: EditScanCornerView, atPoint point: CGPoint) {
  150. guard let quad = quad else {
  151. return
  152. }
  153. let validPoint = self.validPoint(point, forCornerViewOfSize: cornerView.bounds.size, inView: self)
  154. cornerView.center = validPoint
  155. let updatedQuad = update(quad, withPosition: validPoint, forCorner: cornerView.position)
  156. self.quad = updatedQuad
  157. drawQuad(updatedQuad, animated: false)
  158. }
  159. func highlightCornerAtPosition(position: CornerPosition, with image: UIImage) {
  160. guard editable else {
  161. return
  162. }
  163. isHighlighted = true
  164. let cornerView = cornerViewForCornerPosition(position: position)
  165. guard cornerView.isHighlighted == false else {
  166. cornerView.highlightWithImage(image)
  167. return
  168. }
  169. cornerView.frame = CGRect(x: cornerView.frame.origin.x - (highlightedCornerViewSize - cornerViewSize) / 2.0, y: cornerView.frame.origin.y - (highlightedCornerViewSize - cornerViewSize) / 2.0, width: highlightedCornerViewSize, height: highlightedCornerViewSize)
  170. cornerView.highlightWithImage(image)
  171. }
  172. func resetHighlightedCornerViews() {
  173. isHighlighted = false
  174. resetHighlightedCornerViews(cornerViews: [topLeftCornerView, topRightCornerView, bottomLeftCornerView, bottomRightCornerView])
  175. }
  176. private func resetHighlightedCornerViews(cornerViews: [EditScanCornerView]) {
  177. cornerViews.forEach { (cornerView) in
  178. resetHightlightedCornerView(cornerView: cornerView)
  179. }
  180. }
  181. private func resetHightlightedCornerView(cornerView: EditScanCornerView) {
  182. cornerView.reset()
  183. cornerView.frame = CGRect(x: cornerView.frame.origin.x + (cornerView.frame.size.width - cornerViewSize) / 2.0, y: cornerView.frame.origin.y + (cornerView.frame.size.width - cornerViewSize) / 2.0, width: cornerViewSize, height: cornerViewSize)
  184. cornerView.setNeedsDisplay()
  185. }
  186. // MARK: Validation
  187. /// Ensures that the given point is valid - meaning that it is within the bounds of the passed in `UIView`.
  188. ///
  189. /// - Parameters:
  190. /// - point: The point that needs to be validated.
  191. /// - cornerViewSize: The size of the corner view representing the given point.
  192. /// - view: The view which should include the point.
  193. /// - Returns: A new point which is within the passed in view.
  194. private func validPoint(_ point: CGPoint, forCornerViewOfSize cornerViewSize: CGSize, inView view: UIView) -> CGPoint {
  195. var validPoint = point
  196. if point.x > view.bounds.width {
  197. validPoint.x = view.bounds.width
  198. } else if point.x < 0.0 {
  199. validPoint.x = 0.0
  200. }
  201. if point.y > view.bounds.height {
  202. validPoint.y = view.bounds.height
  203. } else if point.y < 0.0 {
  204. validPoint.y = 0.0
  205. }
  206. return validPoint
  207. }
  208. // MARK: - Convenience
  209. private func hideCornerViews() {
  210. topLeftCornerView.isHidden = true
  211. topRightCornerView.isHidden = true
  212. bottomRightCornerView.isHidden = true
  213. bottomLeftCornerView.isHidden = true
  214. }
  215. private func showCornerViews() {
  216. topLeftCornerView.isHidden = false
  217. topRightCornerView.isHidden = false
  218. bottomRightCornerView.isHidden = false
  219. bottomLeftCornerView.isHidden = false
  220. }
  221. private func update(_ quad: Quadrilateral, withPosition position: CGPoint, forCorner corner: CornerPosition) -> Quadrilateral {
  222. var quad = quad
  223. switch corner {
  224. case .topLeft:
  225. quad.topLeft = position
  226. case .topRight:
  227. quad.topRight = position
  228. case .bottomRight:
  229. quad.bottomRight = position
  230. case .bottomLeft:
  231. quad.bottomLeft = position
  232. }
  233. return quad
  234. }
  235. func cornerViewForCornerPosition(position: CornerPosition) -> EditScanCornerView {
  236. switch position {
  237. case .topLeft:
  238. return topLeftCornerView
  239. case .topRight:
  240. return topRightCornerView
  241. case .bottomLeft:
  242. return bottomLeftCornerView
  243. case .bottomRight:
  244. return bottomRightCornerView
  245. }
  246. }
  247. }