|
@@ -1,403 +0,0 @@
|
|
|
-//
|
|
|
-// 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()
|
|
|
- }
|
|
|
-}
|
|
|
-
|