MediaContentView.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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: 3.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. internal 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 doubleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
  111. let gesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:)))
  112. gesture.numberOfTapsRequired = 2
  113. gesture.numberOfTouchesRequired = 1
  114. return gesture
  115. }()
  116. init(index itemIndex: Int, position: CGFloat, frame: CGRect) {
  117. self.index = itemIndex
  118. self.position = position
  119. super.init(frame: frame)
  120. initializeViewComponents()
  121. }
  122. required init?(coder aDecoder: NSCoder) {
  123. fatalError("Do nto use `init?(coder:)`")
  124. }
  125. }
  126. // MARK: - View Composition and Events
  127. extension MediaContentView {
  128. private func initializeViewComponents() {
  129. addSubview(imageView)
  130. imageView.frame = frame
  131. setupIndicatorView()
  132. configureScrollView()
  133. addGestureRecognizer(doubleTapGestureRecognizer)
  134. updateTransform()
  135. }
  136. private func configureScrollView() {
  137. isMultipleTouchEnabled = true
  138. showsHorizontalScrollIndicator = false
  139. showsVerticalScrollIndicator = false
  140. contentSize = imageView.bounds.size
  141. canCancelContentTouches = false
  142. zoomLevels = ZoomScale.default
  143. delegate = self
  144. bouncesZoom = false
  145. }
  146. private func resetZoom() {
  147. setZoomScale(1.0, animated: false)
  148. imageView.transform = CGAffineTransform.identity
  149. contentSize = imageView.frame.size
  150. contentOffset = .zero
  151. }
  152. private func setupIndicatorView() {
  153. addSubview(indicatorContainer)
  154. indicatorContainer.translatesAutoresizingMaskIntoConstraints = false
  155. NSLayoutConstraint.activate([
  156. indicatorContainer.widthAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
  157. indicatorContainer.heightAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
  158. indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
  159. indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor)
  160. ])
  161. indicatorContainer.addSubview(indicator)
  162. indicator.translatesAutoresizingMaskIntoConstraints = false
  163. NSLayoutConstraint.activate([
  164. indicator.leadingAnchor.constraint(equalTo: indicatorContainer.leadingAnchor),
  165. indicator.trailingAnchor.constraint(equalTo: indicatorContainer.trailingAnchor),
  166. indicator.topAnchor.constraint(equalTo: indicatorContainer.topAnchor),
  167. indicator.bottomAnchor.constraint(equalTo: indicatorContainer.bottomAnchor)
  168. ])
  169. indicatorContainer.setNeedsLayout()
  170. indicatorContainer.layoutIfNeeded()
  171. indicatorContainer.isHidden = true
  172. }
  173. internal func updateTransform() {
  174. MediaContentView.contentTransformer(self, position)
  175. }
  176. internal func handleChangeInViewSize(to size: CGSize) {
  177. let oldScale = zoomScale
  178. zoomScale = 1.0
  179. imageView.frame = CGRect(origin: .zero, size: size)
  180. updateImageView()
  181. updateTransform()
  182. setZoomScale(oldScale, animated: false)
  183. contentSize = imageView.frame.size
  184. }
  185. @objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) {
  186. let locationInImage = recognizer.location(in: imageView)
  187. let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width &&
  188. imageView.frame.size.height > bounds.size.height
  189. let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale) ? minimumZoomScale : maximumZoomScale
  190. guard zoomTo != zoomScale else {
  191. return
  192. }
  193. let width = bounds.size.width / zoomTo
  194. let height = bounds.size.height / zoomTo
  195. let zoomRect = CGRect(
  196. x: locationInImage.x - width * 0.5,
  197. y: locationInImage.y - height * 0.5,
  198. width: width,
  199. height: height
  200. )
  201. zoom(to: zoomRect, animated: true)
  202. }
  203. }
  204. // MARK: - UIScrollViewDelegate
  205. extension MediaContentView: UIScrollViewDelegate {
  206. internal func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  207. let shouldAllowZoom = (image != nil && position == 0.0)
  208. return shouldAllowZoom ? imageView : nil
  209. }
  210. internal func scrollViewDidZoom(_ scrollView: UIScrollView) {
  211. centerImageView()
  212. }
  213. private func centerImageView() {
  214. var imageViewFrame = imageView.frame
  215. if imageViewFrame.size.width < bounds.size.width {
  216. imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0
  217. } else {
  218. imageViewFrame.origin.x = 0.0
  219. }
  220. if imageViewFrame.size.height < bounds.size.height {
  221. imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0
  222. } else {
  223. imageViewFrame.origin.y = 0.0
  224. }
  225. imageView.frame = imageViewFrame
  226. }
  227. private func updateImageView() {
  228. imageView.image = image
  229. if let contentImage = image {
  230. let imageViewSize = bounds.size
  231. let imageSize = contentImage.size
  232. var targetImageSize = imageViewSize
  233. if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height {
  234. targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height
  235. } else {
  236. targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width
  237. }
  238. imageView.frame = CGRect(origin: .zero, size: targetImageSize)
  239. }
  240. centerImageView()
  241. }
  242. }