NCZoomableView.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. //
  2. // SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
  3. // SPDX-License-Identifier: GPL-3.0-or-later
  4. //
  5. import Foundation
  6. @objc protocol NCZoomableViewDelegate {
  7. @objc func contentViewZoomDidChange(_ view: NCZoomableView, _ scale: Double)
  8. }
  9. @objcMembers class NCZoomableView: UIView, UIGestureRecognizerDelegate {
  10. public weak var delegate: NCZoomableViewDelegate?
  11. public var disablePanningOnInitialZoom = false
  12. var pinchGestureRecognizer: UIPinchGestureRecognizer?
  13. var panGestureRecognizer: UIPanGestureRecognizer?
  14. var doubleTapGestureRecoginzer: UITapGestureRecognizer?
  15. private(set) var contentView = UIView()
  16. var contentViewSize = CGSize()
  17. public var isZoomed: Bool {
  18. let scaleFactor = self.contentView.transform.a
  19. return scaleFactor != 1
  20. }
  21. override init(frame: CGRect) {
  22. super.init(frame: frame)
  23. self.addSubview(self.contentView)
  24. self.initGestureRecognizers()
  25. }
  26. required init?(coder aDecoder: NSCoder) {
  27. super.init(coder: aDecoder)
  28. self.addSubview(self.contentView)
  29. self.initGestureRecognizers()
  30. }
  31. func initGestureRecognizers() {
  32. self.pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
  33. self.pinchGestureRecognizer?.delegate = self
  34. self.addGestureRecognizer(self.pinchGestureRecognizer!)
  35. self.panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
  36. self.panGestureRecognizer?.delegate = self
  37. self.addGestureRecognizer(self.panGestureRecognizer!)
  38. self.doubleTapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(_:)))
  39. self.doubleTapGestureRecoginzer?.delegate = self
  40. self.doubleTapGestureRecoginzer?.numberOfTapsRequired = 2
  41. self.contentView.addGestureRecognizer(self.doubleTapGestureRecoginzer!)
  42. }
  43. public func replaceContentView(_ newView: UIView) {
  44. if let pinchGestureRecognizer = self.pinchGestureRecognizer {
  45. self.removeGestureRecognizer(pinchGestureRecognizer)
  46. }
  47. if let panGestureRecognizer = self.panGestureRecognizer {
  48. self.removeGestureRecognizer(panGestureRecognizer)
  49. }
  50. if let doubleTapGestureRecoginzer = self.doubleTapGestureRecoginzer {
  51. self.contentView.removeGestureRecognizer(doubleTapGestureRecoginzer)
  52. }
  53. self.contentView.removeFromSuperview()
  54. self.contentView = newView
  55. self.contentViewSize = newView.frame.size
  56. self.addSubview(self.contentView)
  57. self.initGestureRecognizers()
  58. self.resizeContentView()
  59. }
  60. func handlePinch(_ recognizer: UIPinchGestureRecognizer) {
  61. self.zoomView(view: self.contentView, toPoint: recognizer.location(in: self.contentView), usingScale: recognizer.scale)
  62. recognizer.scale = 1
  63. if recognizer.state == .ended {
  64. let bounds = self.contentView.bounds
  65. let zoomedSize = self.contentView.frame.size
  66. let aspectRatioContentViewSize = AVMakeRect(aspectRatio: self.contentViewSize, insideRect: bounds).size
  67. // Don't zoom smaller than the original size
  68. if zoomedSize.width < aspectRatioContentViewSize.width || zoomedSize.height < aspectRatioContentViewSize.height {
  69. UIView.animate(withDuration: 0.3) {
  70. self.resizeContentView()
  71. }
  72. } else {
  73. self.adjustViewPosition()
  74. }
  75. }
  76. }
  77. func handlePan(_ recognizer: UIPanGestureRecognizer) {
  78. if self.disablePanningOnInitialZoom, !self.isZoomed {
  79. return
  80. }
  81. let point = recognizer.translation(in: self.contentView)
  82. // We need to take the current scaling into account when panning
  83. // As we have the same scale factor for X and Y, we can take only one here
  84. let scaleFactor = self.contentView.transform.a
  85. self.contentView.center = CGPoint(x: self.contentView.center.x + point.x * scaleFactor, y: self.contentView.center.y + point.y * scaleFactor)
  86. recognizer.setTranslation(.zero, in: self.contentView)
  87. if recognizer.state == .ended {
  88. self.adjustViewPosition()
  89. }
  90. }
  91. func handleDoubleTap(_ recognizer: UITapGestureRecognizer) {
  92. if recognizer.state == .recognized {
  93. // We need to take the current scaling into account when panning
  94. // As we have the same scale factor for X and Y, we can take only one here
  95. let scaleFactor = self.contentView.transform.a
  96. UIView.animate(withDuration: 0.3) {
  97. if scaleFactor > 1 {
  98. // Set screenView's original size
  99. self.resizeContentView()
  100. } else {
  101. // Zoom 3x screenView into the tap point
  102. self.zoomView(view: recognizer.view!, toPoint: recognizer.location(in: recognizer.view!), usingScale: 3)
  103. }
  104. }
  105. self.adjustViewPosition()
  106. }
  107. }
  108. func zoomView(view: UIView, toPoint point: CGPoint, usingScale scale: CGFloat) {
  109. let bounds = view.bounds
  110. var resultPoint = point
  111. resultPoint.x -= bounds.midX
  112. resultPoint.y -= bounds.midY
  113. var transform = view.transform
  114. transform = CGAffineTransformTranslate(transform, resultPoint.x, resultPoint.y)
  115. transform = CGAffineTransformScale(transform, scale, scale)
  116. transform = CGAffineTransformTranslate(transform, -resultPoint.x, -resultPoint.y)
  117. view.transform = transform
  118. self.delegate?.contentViewZoomDidChange(self, transform.a)
  119. }
  120. func adjustViewPosition() {
  121. let parentSize = self.frame.size
  122. let size = self.contentView.frame.size
  123. var position = self.contentView.frame.origin
  124. let viewLeft = position.x
  125. let viewRight = position.x + size.width
  126. let viewTop = position.y
  127. let viewBottom = position.y + size.height
  128. // Left align screenView if it has been moved to the center (and it is wide enough)
  129. if viewLeft > 0, size.width >= parentSize.width {
  130. position = CGPoint(x: 0, y: position.y)
  131. }
  132. // Top align screenView if it has been moved to the center (and it is tall enough)
  133. if viewTop > 0, size.height >= parentSize.height {
  134. position = CGPoint(x: position.x, y: 0)
  135. }
  136. // Right align screenView if it has been moved to the center (and it is wide enough)
  137. if viewRight < parentSize.width, size.width >= parentSize.width {
  138. position = CGPoint(x: parentSize.width - size.width, y: position.y)
  139. }
  140. // Bottom align screenView if it has been moved to the center (and it is tall enough)
  141. if viewBottom < parentSize.height, size.height >= parentSize.height {
  142. position = CGPoint(x: position.x, y: parentSize.height - size.height)
  143. }
  144. // Align screenView vertically
  145. if size.width <= parentSize.width {
  146. position = CGPoint(x: parentSize.width / 2 - size.width / 2, y: position.y)
  147. }
  148. // Align screenView horizontally
  149. if size.height <= parentSize.height {
  150. position = CGPoint(x: position.x, y: parentSize.height / 2 - size.height / 2)
  151. }
  152. var frame = self.contentView.frame
  153. frame.origin.x = position.x
  154. frame.origin.y = position.y
  155. UIView.animate(withDuration: 0.3) {
  156. self.contentView.frame = frame
  157. }
  158. }
  159. public func resizeContentView() {
  160. self.contentView.transform = .identity
  161. self.delegate?.contentViewZoomDidChange(self, self.contentView.transform.a)
  162. let bounds = self.bounds
  163. let contentSize = self.contentViewSize
  164. if contentSize.width > 0, contentSize.height > 0 {
  165. let aspectFrame = AVMakeRect(aspectRatio: contentSize, insideRect: bounds)
  166. self.contentView.frame = aspectFrame
  167. self.contentView.center = CGPoint(x: bounds.midX, y: bounds.midY)
  168. } else {
  169. self.contentView.frame = bounds
  170. }
  171. }
  172. func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
  173. return true
  174. }
  175. }