// // ScrollScrubber.swift // FolioReaderKit // // Created by Heberti Almeida on 7/14/16. // Copyright © 2016 FolioReader. All rights reserved. // import UIKit func < (lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l < r case (nil, _?): return true default: return false } } func > (lhs: T?, rhs: T?) -> Bool { switch (lhs, rhs) { case let (l?, r?): return l > r default: return rhs < lhs } } enum ScrollType: Int { case page // `chapter` is only for the collection view if vertical with horizontal content is used case chapter } enum ScrollDirection: Int { case none case right case left case up case down init() { self = .none } } class ScrollScrubber: NSObject, UIScrollViewDelegate { weak var delegate: FolioReaderCenter? var showSpeed = 0.6 var hideSpeed = 0.6 var hideDelay = 1.0 var visible = false var usingSlider = false var slider: UISlider! var hideTimer: Timer! var scrollStart: CGFloat! var scrollDelta: CGFloat! var scrollDeltaTimer: Timer! fileprivate weak var readerContainer: FolioReaderContainer? fileprivate var readerConfig: FolioReaderConfig { guard let readerContainer = readerContainer else { return FolioReaderConfig() } return readerContainer.readerConfig } fileprivate var folioReader: FolioReader { guard let readerContainer = readerContainer else { return FolioReader() } return readerContainer.folioReader } var frame: CGRect { didSet { self.slider.frame = frame } } init(frame:CGRect, withReaderContainer readerContainer: FolioReaderContainer) { self.frame = frame self.readerContainer = readerContainer super.init() slider = UISlider() slider.layer.anchorPoint = CGPoint(x: 0, y: 0) slider.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2)) slider.alpha = 0 self.reloadColors() // less obtrusive knob and fixes jump: http://stackoverflow.com/a/22301039/484780 let thumbImg = UIImage(readerImageNamed: "knob") let thumbImgColor = thumbImg?.imageTintColor(readerConfig.tintColor)?.withRenderingMode(.alwaysOriginal) slider.setThumbImage(thumbImgColor, for: UIControlState()) slider.setThumbImage(thumbImgColor, for: .selected) slider.setThumbImage(thumbImgColor, for: .highlighted) slider.addTarget(self, action: #selector(ScrollScrubber.sliderChange(_:)), for: .valueChanged) slider.addTarget(self, action: #selector(ScrollScrubber.sliderTouchDown(_:)), for: .touchDown) slider.addTarget(self, action: #selector(ScrollScrubber.sliderTouchUp(_:)), for: .touchUpInside) slider.addTarget(self, action: #selector(ScrollScrubber.sliderTouchUp(_:)), for: .touchUpOutside) } func reloadColors() { slider.minimumTrackTintColor = readerConfig.tintColor slider.maximumTrackTintColor = folioReader.isNight(readerConfig.nightModeSeparatorColor, readerConfig.menuSeparatorColor) } // MARK: - slider events @objc func sliderTouchDown(_ slider:UISlider) { usingSlider = true show() } @objc func sliderTouchUp(_ slider:UISlider) { usingSlider = false hideAfterDelay() } @objc func sliderChange(_ slider:UISlider) { let movePosition = (height() * CGFloat(slider.value)) let offset = readerConfig.isDirection(CGPoint(x: 0, y: movePosition), CGPoint(x: movePosition, y: 0), CGPoint(x: 0, y: movePosition)) scrollView()?.setContentOffset(offset, animated: false) } // MARK: - show / hide func show() { cancelHide() visible = true if slider.alpha <= 0 { UIView.animate(withDuration: showSpeed, animations: { self.slider.alpha = 1 }, completion: { (Bool) -> Void in self.hideAfterDelay() }) } else { slider.alpha = 1 if usingSlider == false { hideAfterDelay() } } } @objc func hide() { visible = false resetScrollDelta() UIView.animate(withDuration: hideSpeed, animations: { self.slider.alpha = 0 }) } func hideAfterDelay() { cancelHide() hideTimer = Timer.scheduledTimer(timeInterval: hideDelay, target: self, selector: #selector(ScrollScrubber.hide), userInfo: nil, repeats: false) } func cancelHide() { if hideTimer != nil { hideTimer.invalidate() hideTimer = nil } if visible == false { slider.layer.removeAllAnimations() } visible = true } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if scrollDeltaTimer != nil { scrollDeltaTimer.invalidate() scrollDeltaTimer = nil } if scrollStart == nil { scrollStart = scrollView.contentOffset.forDirection(withConfiguration: readerConfig) } } func scrollViewDidScroll(_ scrollView: UIScrollView) { guard (readerConfig.scrollDirection == .vertical || readerConfig.scrollDirection == .defaultVertical || readerConfig.scrollDirection == .horizontalWithVerticalContent) else { return } if visible && usingSlider == false { setSliderVal() } if (slider.alpha > 0) { self.show() } else if delegate?.currentPage != nil && scrollStart != nil { scrollDelta = scrollView.contentOffset.forDirection(withConfiguration: readerConfig) - scrollStart guard let pageHeight = folioReader.readerCenter?.pageHeight, (scrollDeltaTimer == nil && scrollDelta > (pageHeight * 0.2 ) || (scrollDelta * -1) > (pageHeight * 0.2)) else { return } self.show() self.resetScrollDelta() } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { resetScrollDelta() } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { scrollDeltaTimer = Timer(timeInterval:0.5, target: self, selector: #selector(ScrollScrubber.resetScrollDelta), userInfo: nil, repeats: false) RunLoop.current.add(scrollDeltaTimer, forMode: RunLoopMode.commonModes) } @objc func resetScrollDelta() { if scrollDeltaTimer != nil { scrollDeltaTimer.invalidate() scrollDeltaTimer = nil } scrollStart = (scrollView()?.contentOffset.forDirection(withConfiguration: readerConfig) ?? 0) scrollDelta = 0 } func setSliderVal() { slider.value = Float(scrollTop() / height()) } // MARK: - utility methods fileprivate func scrollView() -> UIScrollView? { return delegate?.currentPage?.webView?.scrollView } fileprivate func height() -> CGFloat { guard let currentPage = delegate?.currentPage, let pageHeight = folioReader.readerCenter?.pageHeight, let webView = currentPage.webView else { return 0 } return webView.scrollView.contentSize.height - pageHeight + 44 } fileprivate func scrollTop() -> CGFloat { guard let currentPage = delegate?.currentPage, let webView = currentPage.webView else { return 0 } return webView.scrollView.contentOffset.forDirection(withConfiguration: readerConfig) } }