MediaContentView.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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 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. func zoomScaleOne() {
  186. if zoomScale == 1 { return }
  187. let width = bounds.size.width
  188. let height = bounds.size.height
  189. let zoomRect = CGRect(
  190. x: bounds.size.width/2 - width * 0.5,
  191. y: bounds.size.height/2 - height * 0.5,
  192. width: width,
  193. height: height
  194. )
  195. zoom(to: zoomRect, animated: true)
  196. }
  197. @objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) {
  198. let locationInImage = recognizer.location(in: imageView)
  199. let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width &&
  200. imageView.frame.size.height > bounds.size.height
  201. let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale/2) ? minimumZoomScale : maximumZoomScale/2
  202. guard zoomTo != zoomScale else {
  203. return
  204. }
  205. let width = bounds.size.width / zoomTo
  206. let height = bounds.size.height / zoomTo
  207. let zoomRect = CGRect(
  208. x: locationInImage.x - width * 0.5,
  209. y: locationInImage.y - height * 0.5,
  210. width: width,
  211. height: height
  212. )
  213. zoom(to: zoomRect, animated: true)
  214. }
  215. }
  216. // MARK: - UIScrollViewDelegate
  217. extension MediaContentView: UIScrollViewDelegate {
  218. public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
  219. let shouldAllowZoom = (image != nil && position == 0.0)
  220. return shouldAllowZoom ? imageView : nil
  221. }
  222. public func scrollViewDidZoom(_ scrollView: UIScrollView) {
  223. centerImageView()
  224. }
  225. private func centerImageView() {
  226. var imageViewFrame = imageView.frame
  227. if imageViewFrame.size.width < bounds.size.width {
  228. imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0
  229. } else {
  230. imageViewFrame.origin.x = 0.0
  231. }
  232. if imageViewFrame.size.height < bounds.size.height {
  233. imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0
  234. } else {
  235. imageViewFrame.origin.y = 0.0
  236. }
  237. imageView.frame = imageViewFrame
  238. }
  239. private func updateImageView() {
  240. imageView.image = image
  241. if let contentImage = image {
  242. let imageViewSize = bounds.size
  243. let imageSize = contentImage.size
  244. var targetImageSize = imageViewSize
  245. if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height {
  246. targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height
  247. } else {
  248. targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width
  249. }
  250. imageView.frame = CGRect(origin: .zero, size: targetImageSize)
  251. }
  252. centerImageView()
  253. }
  254. }