FastScrollCollectionView.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. //
  2. // FastScrollCollectionView.swift
  3. // FastScroll
  4. //
  5. // Created by Arsene Huot on 15/06/2018.
  6. // Copyright © 2018 Frichti. All rights reserved.
  7. //
  8. import Foundation
  9. import UIKit
  10. @objc public protocol FastScrollCollectionViewDelegate {
  11. @objc func hideHandle()
  12. }
  13. open class FastScrollCollectionView: UICollectionView {
  14. public enum BubbleFocus {
  15. case first
  16. case last
  17. case dynamic
  18. }
  19. // Bubble to display your information during scroll
  20. public var deactivateBubble: Bool = false
  21. public var bubble: UITextView?
  22. public var bubbleFont: UIFont = UIFont.systemFont(ofSize: 12.0)
  23. public var bubbleTextSize: CGFloat = 12.0
  24. public var bubbleTextColor: UIColor = UIColor.white
  25. public var bubbleRadius: CGFloat = 20.0
  26. public var bubblePadding: CGFloat = 12.0
  27. public var bubbleMarginRight: CGFloat = 30.0
  28. public var bubbleColor: UIColor = UIColor.darkGray
  29. public var bubbleShadowColor: UIColor = UIColor.darkGray
  30. public var bubbleShadowOpacity: Float = 0.7
  31. public var bubbleShadowRadius: CGFloat = 3.0
  32. public var bubbleShadowOffset: CGSize = CGSize(width: 0.0, height: 5.0)
  33. public var bubbleFocus: BubbleFocus = .first
  34. // Handler to scroll
  35. public var handle: UIView?
  36. public var handleImage: UIImage?
  37. public var handleWidth: CGFloat = 30.0
  38. public var handleHeight: CGFloat = 30.0
  39. public var handleRadius: CGFloat = 15.0
  40. public var handleMarginRight: CGFloat = 6.0
  41. public var handleShadowColor: UIColor = UIColor.darkGray
  42. public var handleShadowOpacity: Float = 0.7
  43. public var handleShadowOffset: CGSize = CGSize(width: 0.0, height: 5.0)
  44. public var handleShadowRadius: CGFloat = 3.0
  45. public var handleColor: UIColor = UIColor.darkGray
  46. public var handleTimeToDisappear: CGFloat = 1.5
  47. public var handleDisappearAnimationDuration: CGFloat = 0.2
  48. fileprivate var handleTouched: Bool = false
  49. // Gesture center on handler
  50. public var gestureHandleView: UIView?
  51. public var gestureWidth: CGFloat = 50.0
  52. public var gestureHeight: CGFloat = 50.0
  53. // Scrollbar
  54. public var scrollbar: UIView?
  55. public var scrollbarWidth: CGFloat = 2.0
  56. public var scrollbarColor: UIColor = UIColor(red: 220.0 / 255.0, green: 220.0 / 255.0, blue: 220.0 / 255.0, alpha: 1.0)
  57. public var scrollbarRadius: CGFloat = 1.0
  58. public var scrollbarMarginTop: CGFloat = 40.0
  59. public var scrollbarMarginBottom: CGFloat = 20.0
  60. public var scrollbarMarginRight: CGFloat = 20.0
  61. // Timer to dismiss handle
  62. fileprivate var handleTimer: Timer?
  63. // Action callback
  64. public var bubbleNameForIndexPath: (IndexPath) -> String = { _ in return ""}
  65. // Delegate
  66. public var fastScrollDelegate: FastScrollCollectionViewDelegate?
  67. // MARK: LifeCycle
  68. override open func draw(_ rect: CGRect) {
  69. super.draw(rect)
  70. setup()
  71. setupCollectionView()
  72. }
  73. // MARK: Setups
  74. fileprivate func setupCollectionView() {
  75. showsVerticalScrollIndicator = false
  76. }
  77. public func setup() {
  78. cleanViews()
  79. setupScrollbar()
  80. setupHandle()
  81. setupBubble()
  82. }
  83. public func cleanViews() {
  84. guard let bubble = bubble, let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  85. return
  86. }
  87. bubble.removeFromSuperview()
  88. handle.removeFromSuperview()
  89. scrollbar.removeFromSuperview()
  90. gestureHandleView.removeFromSuperview()
  91. self.bubble = nil
  92. self.handle = nil
  93. self.scrollbar = nil
  94. self.gestureHandleView = nil
  95. }
  96. fileprivate func setupHandle() {
  97. if handle == nil {
  98. handle = UIView(frame: CGRect(x: self.frame.width - handleWidth - handleMarginRight, y: scrollbarMarginTop, width: handleWidth, height: handleHeight))
  99. self.superview?.addSubview(handle!)
  100. gestureHandleView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: gestureWidth, height: gestureHeight))
  101. gestureHandleView!.center = handle!.center
  102. self.superview?.addSubview(handle!)
  103. self.superview?.addSubview(gestureHandleView!)
  104. }
  105. //config layer
  106. handle!.backgroundColor = handleColor
  107. handle!.layer.cornerRadius = handleRadius
  108. handle!.layer.shadowColor = handleShadowColor.cgColor
  109. handle!.layer.shadowOffset = handleShadowOffset
  110. handle!.layer.shadowRadius = handleShadowRadius
  111. handle!.layer.shadowOpacity = handleShadowOpacity
  112. //set imageView
  113. if let handleImage = handleImage {
  114. let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: handleWidth, height: handleHeight))
  115. imageView.image = handleImage
  116. handle!.addSubview(imageView)
  117. }
  118. //set gesture
  119. let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
  120. gestureHandleView!.addGestureRecognizer(panGesture)
  121. //hide
  122. handle!.alpha = 0.0
  123. handle!.isHidden = true
  124. gestureHandleView!.isHidden = true
  125. //position
  126. positionHandle(scrollbarMarginTop)
  127. }
  128. fileprivate func setupBubble() {
  129. if bubble == nil {
  130. bubble = UITextView()
  131. self.superview?.addSubview(bubble!)
  132. }
  133. bubble!.font = bubbleFont
  134. bubble!.font = UIFont(name: bubbleFont.fontName, size: bubbleTextSize)
  135. bubble!.text = "Test"
  136. bubble!.textColor = bubbleTextColor
  137. bubble!.textAlignment = NSTextAlignment.center
  138. bubble!.textContainerInset = UIEdgeInsets(top: bubblePadding, left: bubblePadding, bottom: bubblePadding, right: bubblePadding)
  139. bubble!.contentMode = UIView.ContentMode.scaleAspectFit
  140. bubble!.sizeToFit()
  141. bubble!.backgroundColor = bubbleColor
  142. bubble!.layer.cornerRadius = bubbleRadius
  143. bubble!.layer.shadowColor = bubbleShadowColor.cgColor
  144. bubble!.layer.shadowOffset = bubbleShadowOffset
  145. bubble!.layer.shadowRadius = bubbleRadius
  146. bubble!.layer.shadowOpacity = bubbleShadowOpacity
  147. bubble!.layer.shadowRadius = bubbleShadowRadius
  148. bubble!.layer.masksToBounds = false
  149. bubble!.isHidden = true
  150. updateBubblePosition()
  151. }
  152. fileprivate func setupScrollbar() {
  153. guard let superview = self.superview else {
  154. return
  155. }
  156. if scrollbar == nil {
  157. scrollbar = UIView(frame: CGRect(x: self.frame.width - scrollbarWidth - scrollbarMarginRight, y: scrollbarMarginTop, width: scrollbarWidth, height: superview.bounds.height - scrollbarMarginBottom - scrollbarMarginTop))
  158. self.superview?.addSubview(scrollbar!)
  159. }
  160. scrollbar!.backgroundColor = scrollbarColor
  161. scrollbar!.layer.cornerRadius = scrollbarRadius
  162. scrollbar!.alpha = 0.0
  163. scrollbar!.isHidden = true
  164. }
  165. // MARK: Helpers
  166. @objc func hideHandle() {
  167. guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  168. return
  169. }
  170. if gestureHandleView.isHidden == false {
  171. self.fastScrollDelegate?.hideHandle()
  172. }
  173. gestureHandleView.isHidden = true
  174. UIView.animate(withDuration: TimeInterval(handleDisappearAnimationDuration), animations: {
  175. handle.alpha = 0.0
  176. scrollbar.alpha = 0.0
  177. }, completion: { finished in
  178. if finished {
  179. handle.isHidden = true
  180. scrollbar.isHidden = true
  181. }
  182. })
  183. }
  184. fileprivate func updateBubblePosition() {
  185. guard let scrollbar = scrollbar, let bubble = bubble, let handle = handle else {
  186. return
  187. }
  188. bubble.frame.origin.x = scrollbar.frame.origin.x - bubble.frame.size.width - bubbleMarginRight
  189. bubble.center.y = handle.center.y
  190. }
  191. fileprivate func positionHandle(_ y: CGFloat) {
  192. guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  193. return
  194. }
  195. handle.frame.origin.y = y >= scrollbarMarginTop ?
  196. (y > scrollbarMarginTop + scrollbar.frame.height - handle.frame.height) ? scrollbarMarginTop + scrollbar.frame.height - handle.frame.height : y
  197. :
  198. scrollbarMarginTop
  199. gestureHandleView.center = handle.center
  200. }
  201. fileprivate func scrollCollectionFromHandle() {
  202. guard let handle = handle, let scrollbar = scrollbar else {
  203. return
  204. }
  205. let collectionContentHeight = self.contentSize.height - self.bounds.height
  206. let scrollBarHeight = scrollbar.frame.height
  207. let scrollY = (handle.frame.origin.y - scrollbarMarginTop) * (collectionContentHeight / (scrollBarHeight - handle.frame.size.height))
  208. self.setContentOffset(CGPoint(x: 0.0, y: scrollY), animated: false)
  209. }
  210. @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
  211. guard let superview = superview, let bubble = bubble, let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  212. return
  213. }
  214. // get translation
  215. let translation = panGesture.translation(in: superview)
  216. panGesture.setTranslation(CGPoint.zero, in: superview)
  217. // manage start stop pan
  218. if panGesture.state == UIGestureRecognizer.State.began {
  219. bubble.isHidden = deactivateBubble ? true : false
  220. handleTouched = true
  221. //invalid hide timer
  222. if let handleTimer = handleTimer {
  223. handleTimer.invalidate()
  224. }
  225. handle.alpha = 1.0
  226. scrollbar.alpha = 1.0
  227. handle.isHidden = false
  228. scrollbar.isHidden = false
  229. gestureHandleView.isHidden = false
  230. }
  231. if panGesture.state == UIGestureRecognizer.State.ended {
  232. bubble.isHidden = true
  233. handleTouched = false
  234. if contentOffset.y < 0 {
  235. self.setContentOffset(CGPoint(x: 0.0, y: 0), animated: false)
  236. }
  237. self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
  238. }
  239. if panGesture.state == UIGestureRecognizer.State.changed {
  240. //invalid hide timer
  241. if let handleTimer = handleTimer {
  242. handleTimer.invalidate()
  243. }
  244. handle.alpha = 1.0
  245. scrollbar.alpha = 1.0
  246. handle.isHidden = false
  247. scrollbar.isHidden = false
  248. gestureHandleView.isHidden = false
  249. }
  250. // views positions
  251. positionHandle(handle.frame.origin.y + translation.y)
  252. updateBubblePosition()
  253. scrollCollectionFromHandle()
  254. // manage bubble info
  255. manageBubbleInfo()
  256. }
  257. fileprivate func manageBubbleInfo() {
  258. guard let bubble = bubble else {
  259. return
  260. }
  261. let visibleCells = self.visibleCells
  262. var currentCellIndex: Int
  263. switch bubbleFocus {
  264. case .first:
  265. currentCellIndex = 0
  266. case .last:
  267. currentCellIndex = visibleCells.count - 1
  268. case .dynamic:
  269. //Calcul scroll percentage
  270. let scrollY = contentOffset.y
  271. let collectionContentHeight = self.contentSize.height > self.bounds.height ? self.contentSize.height - self.bounds.height : self.bounds.height
  272. let scrollPercentage = scrollY / collectionContentHeight
  273. currentCellIndex = Int(floor(CGFloat(visibleCells.count) * scrollPercentage))
  274. if currentCellIndex < 0 {
  275. currentCellIndex = 0
  276. }
  277. }
  278. if currentCellIndex < visibleCells.count {
  279. if let indexPath = indexPath(for: visibleCells[currentCellIndex]) {
  280. bubble.text = bubbleNameForIndexPath(indexPath)
  281. let newSize = bubble.sizeThatFits(CGSize(width: self.bounds.width - (self.bounds.width - (bubble.frame.origin.x + bubble.frame.size.width)), height: bubble.frame.size.height))
  282. let oldSize = bubble.frame.size
  283. bubble.frame = CGRect(x: bubble.frame.origin.x + (oldSize.width - newSize.width), y: bubble.frame.origin.y, width: newSize.width, height: newSize.height)
  284. }
  285. }
  286. }
  287. }
  288. // MARK: Scroll Management
  289. extension FastScrollCollectionView {
  290. public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  291. guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
  292. return
  293. }
  294. handle.alpha = 1.0
  295. scrollbar.alpha = 1.0
  296. handle.isHidden = false
  297. scrollbar.isHidden = false
  298. gestureHandleView.isHidden = false
  299. }
  300. public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  301. if !decelerate {
  302. self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
  303. }
  304. }
  305. public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  306. self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
  307. }
  308. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  309. guard let handle = handle, let scrollbar = scrollbar else {
  310. return
  311. }
  312. //invalid timer
  313. if let handleTimer = handleTimer {
  314. handleTimer.invalidate()
  315. }
  316. //scroll position
  317. let scrollY = scrollView.contentOffset.y
  318. let collectionContentHeight = self.contentSize.height > self.bounds.height ? self.contentSize.height - self.bounds.height : self.bounds.height
  319. let scrollBarHeight = scrollbar.frame.height
  320. let handlePosition = (scrollY / collectionContentHeight) * (scrollBarHeight - handle.frame.size.height) + scrollbarMarginTop
  321. if (handleTouched == false) {
  322. positionHandle(handlePosition)
  323. }
  324. updateBubblePosition()
  325. }
  326. }