123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- //
- // FastScrollCollectionView.swift
- // FastScroll
- //
- // Created by Arsene Huot on 15/06/2018.
- // Copyright © 2018 Frichti. All rights reserved.
- //
- import Foundation
- import UIKit
- @objc public protocol FastScrollCollectionViewDelegate {
- @objc func hideHandle()
- }
- open class FastScrollCollectionView: UICollectionView {
- public enum BubbleFocus {
- case first
- case last
- case dynamic
- }
-
- // Bubble to display your information during scroll
- public var deactivateBubble: Bool = false
- public var bubble: UITextView?
- public var bubbleFont: UIFont = UIFont.systemFont(ofSize: 12.0)
- public var bubbleTextSize: CGFloat = 12.0
- public var bubbleTextColor: UIColor = UIColor.white
- public var bubbleRadius: CGFloat = 20.0
- public var bubblePadding: CGFloat = 12.0
- public var bubbleMarginRight: CGFloat = 30.0
- public var bubbleColor: UIColor = UIColor.darkGray
- public var bubbleShadowColor: UIColor = UIColor.darkGray
- public var bubbleShadowOpacity: Float = 0.7
- public var bubbleShadowRadius: CGFloat = 3.0
- public var bubbleShadowOffset: CGSize = CGSize(width: 0.0, height: 5.0)
- public var bubbleFocus: BubbleFocus = .first
-
- // Handler to scroll
- public var handle: UIView?
- public var handleImage: UIImage?
- public var handleWidth: CGFloat = 30.0
- public var handleHeight: CGFloat = 30.0
- public var handleRadius: CGFloat = 15.0
- public var handleMarginRight: CGFloat = 6.0
- public var handleShadowColor: UIColor = UIColor.darkGray
- public var handleShadowOpacity: Float = 0.7
- public var handleShadowOffset: CGSize = CGSize(width: 0.0, height: 5.0)
- public var handleShadowRadius: CGFloat = 3.0
- public var handleColor: UIColor = UIColor.darkGray
- public var handleTimeToDisappear: CGFloat = 1.5
- public var handleDisappearAnimationDuration: CGFloat = 0.2
- fileprivate var handleTouched: Bool = false
-
- // Gesture center on handler
- public var gestureHandleView: UIView?
- public var gestureWidth: CGFloat = 50.0
- public var gestureHeight: CGFloat = 50.0
-
- // Scrollbar
- public var scrollbar: UIView?
- public var scrollbarWidth: CGFloat = 2.0
- public var scrollbarColor: UIColor = UIColor(red: 220.0 / 255.0, green: 220.0 / 255.0, blue: 220.0 / 255.0, alpha: 1.0)
- public var scrollbarRadius: CGFloat = 1.0
- public var scrollbarMarginTop: CGFloat = 40.0
- public var scrollbarMarginBottom: CGFloat = 20.0
- public var scrollbarMarginRight: CGFloat = 20.0
-
- // Timer to dismiss handle
- fileprivate var handleTimer: Timer?
-
- // Action callback
- public var bubbleNameForIndexPath: (IndexPath) -> String = { _ in return ""}
-
- // Delegate
- public var fastScrollDelegate: FastScrollCollectionViewDelegate?
-
- // MARK: LifeCycle
-
- override open func draw(_ rect: CGRect) {
- super.draw(rect)
- setup()
- setupCollectionView()
- }
-
- // MARK: Setups
-
- fileprivate func setupCollectionView() {
- showsVerticalScrollIndicator = false
- }
-
- public func setup() {
- cleanViews()
-
- setupScrollbar()
- setupHandle()
- setupBubble()
- }
-
- public func cleanViews() {
- guard let bubble = bubble, let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
- return
- }
-
- bubble.removeFromSuperview()
- handle.removeFromSuperview()
- scrollbar.removeFromSuperview()
- gestureHandleView.removeFromSuperview()
-
- self.bubble = nil
- self.handle = nil
- self.scrollbar = nil
- self.gestureHandleView = nil
- }
-
- fileprivate func setupHandle() {
- if handle == nil {
- handle = UIView(frame: CGRect(x: self.frame.width - handleWidth - handleMarginRight, y: scrollbarMarginTop, width: handleWidth, height: handleHeight))
- self.superview?.addSubview(handle!)
-
- gestureHandleView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: gestureWidth, height: gestureHeight))
- gestureHandleView!.center = handle!.center
-
- self.superview?.addSubview(handle!)
- self.superview?.addSubview(gestureHandleView!)
- }
-
- //config layer
- handle!.backgroundColor = handleColor
- handle!.layer.cornerRadius = handleRadius
- handle!.layer.shadowColor = handleShadowColor.cgColor
- handle!.layer.shadowOffset = handleShadowOffset
- handle!.layer.shadowRadius = handleShadowRadius
- handle!.layer.shadowOpacity = handleShadowOpacity
-
- //set imageView
- if let handleImage = handleImage {
- let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: handleWidth, height: handleHeight))
- imageView.image = handleImage
- handle!.addSubview(imageView)
- }
-
- //set gesture
- let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
- gestureHandleView!.addGestureRecognizer(panGesture)
-
- //hide
- handle!.alpha = 0.0
- handle!.isHidden = true
- gestureHandleView!.isHidden = true
-
- //position
- positionHandle(scrollbarMarginTop)
- }
-
- fileprivate func setupBubble() {
- if bubble == nil {
- bubble = UITextView()
- self.superview?.addSubview(bubble!)
- }
-
- bubble!.font = bubbleFont
- bubble!.font = UIFont(name: bubbleFont.fontName, size: bubbleTextSize)
- bubble!.text = "Test"
- bubble!.textColor = bubbleTextColor
- bubble!.textAlignment = NSTextAlignment.center
- bubble!.textContainerInset = UIEdgeInsets(top: bubblePadding, left: bubblePadding, bottom: bubblePadding, right: bubblePadding)
- bubble!.contentMode = UIView.ContentMode.scaleAspectFit
- bubble!.sizeToFit()
-
- bubble!.backgroundColor = bubbleColor
- bubble!.layer.cornerRadius = bubbleRadius
- bubble!.layer.shadowColor = bubbleShadowColor.cgColor
- bubble!.layer.shadowOffset = bubbleShadowOffset
- bubble!.layer.shadowRadius = bubbleRadius
- bubble!.layer.shadowOpacity = bubbleShadowOpacity
- bubble!.layer.shadowRadius = bubbleShadowRadius
- bubble!.layer.masksToBounds = false
-
- bubble!.isHidden = true
-
- updateBubblePosition()
- }
-
- fileprivate func setupScrollbar() {
- guard let superview = self.superview else {
- return
- }
-
- if scrollbar == nil {
- scrollbar = UIView(frame: CGRect(x: self.frame.width - scrollbarWidth - scrollbarMarginRight, y: scrollbarMarginTop, width: scrollbarWidth, height: superview.bounds.height - scrollbarMarginBottom - scrollbarMarginTop))
- self.superview?.addSubview(scrollbar!)
- }
-
- scrollbar!.backgroundColor = scrollbarColor
- scrollbar!.layer.cornerRadius = scrollbarRadius
- scrollbar!.alpha = 0.0
- scrollbar!.isHidden = true
- }
-
- // MARK: Helpers
- @objc func hideHandle() {
- guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
- return
- }
-
- if gestureHandleView.isHidden == false {
- self.fastScrollDelegate?.hideHandle()
- }
-
- gestureHandleView.isHidden = true
-
- UIView.animate(withDuration: TimeInterval(handleDisappearAnimationDuration), animations: {
- handle.alpha = 0.0
- scrollbar.alpha = 0.0
- }, completion: { finished in
- if finished {
- handle.isHidden = true
- scrollbar.isHidden = true
- }
- })
- }
-
- fileprivate func updateBubblePosition() {
- guard let scrollbar = scrollbar, let bubble = bubble, let handle = handle else {
- return
- }
-
- bubble.frame.origin.x = scrollbar.frame.origin.x - bubble.frame.size.width - bubbleMarginRight
- bubble.center.y = handle.center.y
- }
-
- fileprivate func positionHandle(_ y: CGFloat) {
- guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
- return
- }
-
- handle.frame.origin.y = y >= scrollbarMarginTop ?
- (y > scrollbarMarginTop + scrollbar.frame.height - handle.frame.height) ? scrollbarMarginTop + scrollbar.frame.height - handle.frame.height : y
- :
- scrollbarMarginTop
-
- gestureHandleView.center = handle.center
- }
-
- fileprivate func scrollCollectionFromHandle() {
- guard let handle = handle, let scrollbar = scrollbar else {
- return
- }
-
- let collectionContentHeight = self.contentSize.height - self.bounds.height
- let scrollBarHeight = scrollbar.frame.height
-
- let scrollY = (handle.frame.origin.y - scrollbarMarginTop) * (collectionContentHeight / (scrollBarHeight - handle.frame.size.height))
-
- self.setContentOffset(CGPoint(x: 0.0, y: scrollY), animated: false)
- }
-
- @objc func handlePanGesture(_ panGesture: UIPanGestureRecognizer) {
- guard let superview = superview, let bubble = bubble, let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
- return
- }
-
- // get translation
- let translation = panGesture.translation(in: superview)
- panGesture.setTranslation(CGPoint.zero, in: superview)
-
- // manage start stop pan
- if panGesture.state == UIGestureRecognizer.State.began {
- bubble.isHidden = deactivateBubble ? true : false
- handleTouched = true
-
- //invalid hide timer
- if let handleTimer = handleTimer {
- handleTimer.invalidate()
- }
-
- handle.alpha = 1.0
- scrollbar.alpha = 1.0
- handle.isHidden = false
- scrollbar.isHidden = false
- gestureHandleView.isHidden = false
- }
-
- if panGesture.state == UIGestureRecognizer.State.ended {
- bubble.isHidden = true
- handleTouched = false
- if contentOffset.y < 0 {
- self.setContentOffset(CGPoint(x: 0.0, y: 0), animated: false)
- }
- self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
- }
-
- if panGesture.state == UIGestureRecognizer.State.changed {
- //invalid hide timer
- if let handleTimer = handleTimer {
- handleTimer.invalidate()
- }
-
- handle.alpha = 1.0
- scrollbar.alpha = 1.0
- handle.isHidden = false
- scrollbar.isHidden = false
- gestureHandleView.isHidden = false
- }
-
- // views positions
- positionHandle(handle.frame.origin.y + translation.y)
- updateBubblePosition()
- scrollCollectionFromHandle()
-
- // manage bubble info
- manageBubbleInfo()
- }
-
- fileprivate func manageBubbleInfo() {
- guard let bubble = bubble else {
- return
- }
-
- let visibleCells = self.visibleCells
-
- var currentCellIndex: Int
-
- switch bubbleFocus {
- case .first:
- currentCellIndex = 0
-
- case .last:
- currentCellIndex = visibleCells.count - 1
-
- case .dynamic:
- //Calcul scroll percentage
- let scrollY = contentOffset.y
- let collectionContentHeight = self.contentSize.height > self.bounds.height ? self.contentSize.height - self.bounds.height : self.bounds.height
- let scrollPercentage = scrollY / collectionContentHeight
- currentCellIndex = Int(floor(CGFloat(visibleCells.count) * scrollPercentage))
- if currentCellIndex < 0 {
- currentCellIndex = 0
- }
- }
-
- if currentCellIndex < visibleCells.count {
- if let indexPath = indexPath(for: visibleCells[currentCellIndex]) {
- bubble.text = bubbleNameForIndexPath(indexPath)
- 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))
- let oldSize = bubble.frame.size
- bubble.frame = CGRect(x: bubble.frame.origin.x + (oldSize.width - newSize.width), y: bubble.frame.origin.y, width: newSize.width, height: newSize.height)
- }
- }
- }
- }
- // MARK: Scroll Management
- extension FastScrollCollectionView {
- public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
- guard let handle = handle, let scrollbar = scrollbar, let gestureHandleView = gestureHandleView else {
- return
- }
-
- handle.alpha = 1.0
- scrollbar.alpha = 1.0
- handle.isHidden = false
- scrollbar.isHidden = false
- gestureHandleView.isHidden = false
- }
-
- public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
- if !decelerate {
- self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
- }
- }
-
- public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
- self.handleTimer = Timer.scheduledTimer(timeInterval: TimeInterval(handleTimeToDisappear), target: self, selector: #selector(hideHandle), userInfo: nil, repeats: false)
- }
-
- public func scrollViewDidScroll(_ scrollView: UIScrollView) {
- guard let handle = handle, let scrollbar = scrollbar else {
- return
- }
-
- //invalid timer
- if let handleTimer = handleTimer {
- handleTimer.invalidate()
- }
-
- //scroll position
- let scrollY = scrollView.contentOffset.y
- let collectionContentHeight = self.contentSize.height > self.bounds.height ? self.contentSize.height - self.bounds.height : self.bounds.height
- let scrollBarHeight = scrollbar.frame.height
-
-
- let handlePosition = (scrollY / collectionContentHeight) * (scrollBarHeight - handle.frame.size.height) + scrollbarMarginTop
- if (handleTouched == false) {
- positionHandle(handlePosition)
- }
-
- updateBubblePosition()
- }
- }
|