MediaContentView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. //
  2. // MediaContentView.swift
  3. // ATGMediaBrowser
  4. //
  5. // Created by Suraj Thomas K on 7/10/18.
  6. // Copyright © 2018 Al Tayer Group LLC.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  9. // and associated documentation files (the "Software"), to deal in the Software without
  10. // restriction, including without limitation the rights to use, copy, modify, merge, publish,
  11. // distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
  12. // Software is furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in all copies or
  15. // substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  18. // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  19. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
  20. // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. //
  23. /// Holds the value of minimumZoomScale and maximumZoomScale of the image.
  24. public struct ZoomScale {
  25. /// Minimum zoom level, the image can be zoomed out to.
  26. public var minimumZoomScale: CGFloat
  27. /// Maximum zoom level, the image can be zoomed into.
  28. public var maximumZoomScale: CGFloat
  29. /// Default zoom scale. minimum is 1.0 and maximum is 3.0
  30. public static let `default` = ZoomScale(
  31. minimum: 1.0,
  32. maximum: 10.0
  33. )
  34. /// Identity zoom scale. Pass this to disable zoom.
  35. public static let identity = ZoomScale(
  36. minimum: 1.0,
  37. maximum: 1.0
  38. )
  39. /**
  40. Initializer.
  41. - parameter minimum: The minimum zoom level.
  42. - parameter maximum: The maximum zoom level.
  43. */
  44. public init(minimum: CGFloat, maximum: CGFloat) {
  45. minimumZoomScale = minimum
  46. maximumZoomScale = maximum
  47. }
  48. }
  49. public class MediaContentView: UIScrollView {
  50. // MARK: - Exposed variables
  51. internal static var interItemSpacing: CGFloat = 0.0
  52. internal var index: Int {
  53. didSet {
  54. resetZoom()
  55. }
  56. }
  57. internal static var contentTransformer: ContentTransformer = DefaultContentTransformers.horizontalMoveInOut
  58. internal var position: CGFloat {
  59. didSet {
  60. updateTransform()
  61. }
  62. }
  63. internal var image: UIImage? {
  64. didSet {
  65. updateImageView()
  66. }
  67. }
  68. internal var isLoading: Bool = false {
  69. didSet {
  70. indicatorContainer.isHidden = !isLoading
  71. if isLoading {
  72. indicator.startAnimating()
  73. } else {
  74. indicator.stopAnimating()
  75. }
  76. }
  77. }
  78. internal var zoomLevels: ZoomScale? {
  79. didSet {
  80. zoomScale = ZoomScale.default.minimumZoomScale
  81. minimumZoomScale = zoomLevels?.minimumZoomScale ?? ZoomScale.default.minimumZoomScale
  82. maximumZoomScale = zoomLevels?.maximumZoomScale ?? ZoomScale.default.maximumZoomScale
  83. }
  84. }
  85. // MARK: - Private enumerations
  86. private enum Constants {
  87. static let indicatorViewSize: CGFloat = 60.0
  88. }
  89. // MARK: - Private variables
  90. private lazy var imageView: UIImageView = {
  91. let imageView = UIImageView()
  92. imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  93. imageView.contentMode = .scaleAspectFit
  94. imageView.clipsToBounds = true
  95. return imageView
  96. }()
  97. private lazy var indicator: UIActivityIndicatorView = {
  98. let indicatorView = UIActivityIndicatorView()
  99. indicatorView.style = .whiteLarge
  100. indicatorView.hidesWhenStopped = true
  101. return indicatorView
  102. }()
  103. private lazy var indicatorContainer: UIView = {
  104. let container = UIView()
  105. container.backgroundColor = .darkGray
  106. container.layer.cornerRadius = Constants.indicatorViewSize * 0.5
  107. container.layer.masksToBounds = true
  108. return container
  109. }()
  110. private lazy var singleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
  111. let gesture = UITapGestureRecognizer(target: self, action: #selector(didSingleTap(_:)))
  112. gesture.numberOfTapsRequired = 1
  113. gesture.numberOfTouchesRequired = 1
  114. return gesture
  115. }()
  116. private lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
  117. let gesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:)))
  118. gesture.numberOfTapsRequired = 2
  119. gesture.numberOfTouchesRequired = 1
  120. return gesture
  121. }()
  122. private var mediaBrowserViewControllerDelegate: MediaBrowserViewControllerDelegate?
  123. init(index itemIndex: Int, position: CGFloat, frame: CGRect, delegate: MediaBrowserViewControllerDelegate?) {
  124. self.index = itemIndex
  125. self.position = position
  126. self.mediaBrowserViewControllerDelegate = delegate
  127. super.init(frame: frame)
  128. initializeViewComponents()
  129. }
  130. required init?(coder aDecoder: NSCoder) {
  131. fatalError("Do nto use `init?(coder:)`")
  132. }
  133. }
  134. // MARK: - View Composition and Events
  135. extension MediaContentView {
  136. private func initializeViewComponents() {
  137. addSubview(imageView)
  138. imageView.frame = frame
  139. setupIndicatorView()
  140. configureScrollView()
  141. addGestureRecognizer(singleTapGestureRecognizer)
  142. addGestureRecognizer(doubleTapGestureRecognizer)
  143. singleTapGestureRecognizer.require(toFail: doubleTapGestureRecognizer)
  144. updateTransform()
  145. }
  146. private func configureScrollView() {
  147. isMultipleTouchEnabled = true
  148. showsHorizontalScrollIndicator = false
  149. showsVerticalScrollIndicator = false
  150. contentSize = imageView.bounds.size
  151. canCancelContentTouches = false
  152. zoomLevels = ZoomScale.default
  153. delegate = self
  154. bouncesZoom = false
  155. }
  156. private func resetZoom() {
  157. setZoomScale(1.0, animated: false)
  158. imageView.transform = CGAffineTransform.identity
  159. contentSize = imageView.frame.size
  160. contentOffset = .zero
  161. }
  162. private func setupIndicatorView() {
  163. addSubview(indicatorContainer)
  164. indicatorContainer.translatesAutoresizingMaskIntoConstraints = false
  165. NSLayoutConstraint.activate([
  166. indicatorContainer.widthAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
  167. indicatorContainer.heightAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
  168. indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
  169. indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor)
  170. ])
  171. indicatorContainer.addSubview(indicator)
  172. indicator.translatesAutoresizingMaskIntoConstraints = false
  173. NSLayoutConstraint.activate([
  174. indicator.leadingAnchor.constraint(equalTo: indicatorContainer.leadingAnchor),
  175. indicator.trailingAnchor.constraint(equalTo: indicatorContainer.trailingAnchor),
  176. indicator.topAnchor.constraint(equalTo: indicatorContainer.topAnchor),
  177. indicator.bottomAnchor.constraint(equalTo: indicatorContainer.bottomAnchor)
  178. ])
  179. indicatorContainer.setNeedsLayout()
  180. indicatorContainer.layoutIfNeeded()
  181. indicatorContainer.isHidden = true
  182. }
  183. internal func updateTransform() {
  184. MediaContentView.contentTransformer(self, position)
  185. }
  186. internal func handleChangeInViewSize(to size: CGSize) {
  187. let oldScale = zoomScale
  188. zoomScale = 1.0
  189. imageView.frame = CGRect(origin: .zero, size: size)
  190. updateImageView()
  191. updateTransform()
  192. setZoomScale(oldScale, animated: false)
  193. contentSize = imageView.frame.size
  194. }
  195. @objc private func didSingleTap(_ recognizer: UITapGestureRecognizer) {
  196. if zoomScale != 1 {
  197. let locationInImage = recognizer.location(in: imageView)
  198. let width = bounds.size.width
  199. let height = bounds.size.height
  200. let zoomRect = CGRect(
  201. x: locationInImage.x - width * 0.5,
  202. y: locationInImage.y - height * 0.5,
  203. width: width,
  204. height: height
  205. )
  206. zoom(to: zoomRect, animated: true)
  207. }
  208. self.mediaBrowserViewControllerDelegate?.mediaBrowserTap(self)
  209. }
  210. @objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) {
  211. let locationInImage = recognizer.location(in: imageView)
  212. let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width &&
  213. imageView.frame.size.height > bounds.size.height
  214. let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale/2) ? minimumZoomScale : maximumZoomScale/2
  215. guard zoomTo != zoomScale else {
  216. return
  217. }
  218. let width = bounds.size.width / zoomTo
  219. let height = bounds.size.height / zoomTo
  220. let zoomRect = CGRect(
  221. x: locationInImage.x - width * 0.5,
  222. y: locationInImage.y - height * 0.5,
  223. width: width,
  224. height: height
  225. )
  226. zoom(to: zoomRect, animated: true)
  227. }
  228. }
  229. // MARK: - UIScrollViewDelegate
  230. extension MediaContentView: UIScrollViewDelegate {
  231. public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  232. let shouldAllowZoom = (image != nil && position == 0.0)
  233. return shouldAllowZoom ? imageView : nil
  234. }
  235. public func scrollViewDidZoom(_ scrollView: UIScrollView) {
  236. centerImageView()
  237. }
  238. private func centerImageView() {
  239. var imageViewFrame = imageView.frame
  240. if imageViewFrame.size.width < bounds.size.width {
  241. imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0
  242. } else {
  243. imageViewFrame.origin.x = 0.0
  244. }
  245. if imageViewFrame.size.height < bounds.size.height {
  246. imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0
  247. } else {
  248. imageViewFrame.origin.y = 0.0
  249. }
  250. imageView.frame = imageViewFrame
  251. }
  252. private func updateImageView() {
  253. imageView.image = image
  254. if let contentImage = image {
  255. let imageViewSize = bounds.size
  256. let imageSize = contentImage.size
  257. var targetImageSize = imageViewSize
  258. if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height {
  259. targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height
  260. } else {
  261. targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width
  262. }
  263. imageView.frame = CGRect(origin: .zero, size: targetImageSize)
  264. }
  265. centerImageView()
  266. }
  267. }