diff --git a/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift b/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift index 3671c27d..057668ef 100644 --- a/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift +++ b/VDS/Components/CarouselScrollbar/CarouselScrollbar.swift @@ -122,7 +122,6 @@ open class CarouselScrollbar: View { internal var containerSize: CGSize { CGSize(width: 45, height: 44) } internal var _selectedLayout: Layout = .oneUP internal var _numberOfSlides: Int? = 1 - internal var heightConstraint: NSLayoutConstraint? internal var totalPositions: Int? = 1 internal var _position: Int? = 1 internal var trayOriginalCenter: CGPoint! @@ -131,41 +130,30 @@ open class CarouselScrollbar: View { private let trackViewHeight: CGFloat = 4 private let minThumbWidth: Float = 16.0 private var thumbWidth: Float = 16.0 - private var actualThumbWidth: Float = 0.0 + private var computedWidth: Float = 0.0 private let cornerRadius: CGFloat = 4.0 private let activeOpacity: Float = 0.15 private let defaultOpacity: Float = 1 - /// Track view with fixed width - internal var trackView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - }() - - /// Left Active Track overlay with variable width - internal var leftActiveOverlay: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.tag = 2 - } - }() - - /// Right Active Track overlay with variable width - internal var rightActiveOverlay: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - $0.tag = 3 - } - }() - - /// Thumb view with variable width - internal var thumbView: UIView = { - return UIView().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } - }() + internal var containerView = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + internal var trackView = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + internal var leftActiveOverlay = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + internal var rightActiveOverlay = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + internal var thumbView = View().with { + $0.translatesAutoresizingMaskIntoConstraints = false + } + internal var rightActiveOverlayLayer: CALayer = CALayer() + internal var leftActiveOverlayLayer: CALayer = CALayer() + internal var thumbViewLayer: CALayer = CALayer() //-------------------------------------------------- // MARK: - Configuration //-------------------------------------------------- @@ -184,46 +172,67 @@ open class CarouselScrollbar: View { super.setup() accessibilityLabel = "Carousel Scrollbar" - //create the wrapping view - heightConstraint = self.heightAnchor.constraint(equalToConstant: containerSize.height) - heightConstraint?.priority = .defaultHigh - heightConstraint?.isActive = true + addSubview(containerView) + containerView + .pinTop() + .pinBottom() + .pinLeadingGreaterThanOrEqualTo() + .pinTrailingLessThanOrEqualTo() + .height(containerSize.height) + .width(CGFloat(trackViewWidth)) + + containerView.centerXAnchor.constraint(equalTo: centerXAnchor).activate() //Trackview - trackView.frame = CGRectMake(20, 20, CGFloat(trackViewWidth), trackViewHeight) + trackView.frame = CGRectMake(0, 20, CGFloat(trackViewWidth), trackViewHeight) trackView.layer.cornerRadius = cornerRadius - addSubview(trackView) + containerView.addSubview(trackView) ///Left active overlay - leftActiveOverlay.frame = CGRectMake(trackView.frame.origin.x, 20, CGFloat(trackViewWidth), trackViewHeight) + leftActiveOverlay.frame = CGRectMake(trackView.frame.origin.x, 0, CGFloat(trackViewWidth), containerSize.height) leftActiveOverlay.isUserInteractionEnabled = true - leftActiveOverlay.layer.cornerRadius = cornerRadius + leftActiveOverlay.backgroundColor = .clear +// leftActiveOverlay.clipsToBounds = true let leftPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.onThumbTouchStart(_:))) leftPressRecognizer.minimumPressDuration = 0 leftActiveOverlay.addGestureRecognizer(leftPressRecognizer) - addSubview(leftActiveOverlay) + containerView.addSubview(leftActiveOverlay) + + leftActiveOverlay.layer.addSublayer(leftActiveOverlayLayer) + leftActiveOverlayLayer.cornerRadius = cornerRadius + leftActiveOverlayLayer.frame = .init(origin: .zero, size: .init(width: leftActiveOverlayLayer.frame.size.width, height: trackViewHeight)) ///Right active overlay - rightActiveOverlay.frame = CGRectMake(thumbView.frame.origin.x + thumbView.frame.size.width, 20, CGFloat(trackViewWidth), trackViewHeight) + rightActiveOverlay.frame = CGRectMake(thumbView.frame.origin.x + thumbView.frame.size.width, 0, CGFloat(trackViewWidth), containerSize.height) rightActiveOverlay.isUserInteractionEnabled = true - rightActiveOverlay.layer.cornerRadius = cornerRadius +// rightActiveOverlay.clipsToBounds = true + rightActiveOverlay.backgroundColor = .clear let rightPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.onThumbTouchEnd(_:))) rightPressRecognizer.minimumPressDuration = 0 rightActiveOverlay.addGestureRecognizer(rightPressRecognizer) - addSubview(rightActiveOverlay) + containerView.addSubview(rightActiveOverlay) + + rightActiveOverlay.layer.addSublayer(rightActiveOverlayLayer) + rightActiveOverlayLayer.cornerRadius = cornerRadius + rightActiveOverlayLayer.frame = .init(origin: .zero, size: .init(width: rightActiveOverlay.frame.size.width, height: trackViewHeight)) //Thumbview - thumbView.frame = CGRectMake(20, 20, CGFloat(thumbWidth), trackViewHeight) - thumbView.layer.cornerRadius = cornerRadius + thumbView.frame = CGRectMake(0, 0, CGFloat(thumbWidth), containerSize.height) + thumbView.backgroundColor = .clear thumbView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action:(#selector(self.onScrubberDrag(_:))))) - addSubview(thumbView) + containerView.addSubview(thumbView) updateActiveOverlays() + + thumbViewLayer.cornerRadius = cornerRadius + thumbViewLayer.backgroundColor = thumbColorConfiguration.getColor(surface).cgColor + thumbViewLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: CGFloat(thumbWidth), height: trackViewHeight)) + thumbView.layer.addSublayer(thumbViewLayer) } open override func updateView() { super.updateView() trackView.backgroundColor = trackColorConfiguration.getColor(surface) - thumbView.backgroundColor = thumbColorConfiguration.getColor(surface) + thumbViewLayer.backgroundColor = thumbColorConfiguration.getColor(surface).cgColor } open override func updateAccessibility() { @@ -252,67 +261,22 @@ open class CarouselScrollbar: View { scrollThumbToPosition(position) } - // Drag scrollbar thumb to move it to the left or right. - // Upon releases of drag, the scrollbar thumb snaps to the closest full position and the scrollbar returns to original size without delay. - @objc func onScrubberDrag(_ sender: UIPanGestureRecognizer) { - let translation = sender.translation(in: thumbView) - if sender.state == UIGestureRecognizer.State.began { - trayOriginalCenter = thumbView.center - } else if sender.state == UIGestureRecognizer.State.changed { - let draggedPositions = Int (ceil (Double(translation.x) / Double(actualThumbWidth))) - setThumb(at: (position ?? 1) + draggedPositions) - } - else if sender.state == UIGestureRecognizer.State.cancelled || sender.state == UIGestureRecognizer.State.ended { - let draggedPositions = Int (ceil (Double(translation.x) / Double(actualThumbWidth))) - position = (((position ?? 1) + draggedPositions) < 1) ? 1 : ((position ?? 1) + draggedPositions) - } - } - - // Move the scrollbar thumb to the left while tapping on the left side of the scrubber. - @objc func onThumbTouchStart(_ gesture: UILongPressGestureRecognizer) { - if gesture.state == .began { - leftActiveOverlay.backgroundColor = activeOverlayColorConfiguration.getColor(self) - leftActiveOverlay.layer.opacity = activeOpacity - } else if gesture.state == .cancelled { - leftActiveOverlay.backgroundColor = .clear - leftActiveOverlay.layer.opacity = defaultOpacity - } else if gesture.state == .ended { - leftActiveOverlay.backgroundColor = .clear - leftActiveOverlay.layer.opacity = defaultOpacity - self.onMoveBackward() - } - } - - // Move the scrollbar thumb to the right while tapping on the right side of the scrubber. - @objc func onThumbTouchEnd(_ gesture: UILongPressGestureRecognizer) { - if gesture.state == .began { - rightActiveOverlay.backgroundColor = activeOverlayColorConfiguration.getColor(self) - rightActiveOverlay.layer.opacity = activeOpacity - } else if gesture.state == .cancelled { - rightActiveOverlay.backgroundColor = .clear - rightActiveOverlay.layer.opacity = defaultOpacity - } else if gesture.state == .ended { - rightActiveOverlay.backgroundColor = .clear - rightActiveOverlay.layer.opacity = defaultOpacity - self.onMoveForward() - } - } - - // Minimum thumb width applied - // Incomplete set moves a shorter distance than the standard increment value. + // Compute track width and should maintain minimum thumb width if needed private func setThumbWidth() { let width = (Float(trackViewWidth) / Float(numberOfSlides ?? 1)) * Float(_selectedLayout.value) - actualThumbWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width + computedWidth = (width > Float(trackViewWidth)) ? Float(trackViewWidth) : width thumbWidth = (width <= Float(trackViewWidth) && width > minThumbWidth) ? width : ((width > Float(trackViewWidth)) ? Float(trackViewWidth) : minThumbWidth) thumbView.frame.size.width = CGFloat(thumbWidth) thumbView.frame.origin.x = trackView.frame.origin.x + thumbViewLayer.frame.size.width = thumbView.frame.size.width checkPositions() updateActiveOverlays() } + // Incomplete set moves a shorter distance than the standard increment value. // Update active overlay frames according to thumb position. private func updateActiveOverlays() { - // adjusting thumb position if it goes beyond trackView. + // adjusting thumb position if it goes beyond trackView on left/right. let thumbPosition = thumbView.frame.origin.x + thumbView.frame.size.width let trackPosition = trackView.frame.origin.x + trackView.frame.size.width if thumbPosition > trackPosition { @@ -323,26 +287,80 @@ open class CarouselScrollbar: View { //left active overlay position update leftActiveOverlay.frame.size.width = thumbView.frame.origin.x - trackView.frame.origin.x + cornerRadius - - //left active overlay position update + leftActiveOverlayLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: leftActiveOverlay.frame.size.width, height: trackViewHeight)) + + //right active overlay position update rightActiveOverlay.frame.origin.x = thumbView.frame.origin.x + thumbView.frame.size.width - cornerRadius rightActiveOverlay.frame.size.width = (trackView.frame.origin.x + trackView.frame.size.width) - (thumbView.frame.origin.x + thumbView.frame.size.width) + cornerRadius + rightActiveOverlayLayer.frame = .init(origin: .init(x: 0, y: 20), size: .init(width: rightActiveOverlay.frame.size.width, height: trackViewHeight)) + } + + // Drag scrollbar thumb to move it to the left or right. + // Upon releases of drag, the scrollbar thumb snaps to the closest full position and the scrollbar returns to original size without delay. + @objc func onScrubberDrag(_ sender: UIPanGestureRecognizer) { + let translation = sender.translation(in: thumbView) + if sender.state == UIGestureRecognizer.State.began { + trayOriginalCenter = thumbView.center + } else if sender.state == UIGestureRecognizer.State.changed { + let draggedPositions = Int (ceil (Double(translation.x) / Double(computedWidth))) + setThumb(at: (position ?? 1) + draggedPositions) + } + else if sender.state == UIGestureRecognizer.State.cancelled || sender.state == UIGestureRecognizer.State.ended { + let draggedPositions = Int (ceil (Double(translation.x) / Double(computedWidth))) + position = (((position ?? 1) + draggedPositions) < 1) ? 1 : ((position ?? 1) + draggedPositions) + } + } + + // Move the scrollbar thumb to the left while tapping on the left side of the scrubber. + @objc func onThumbTouchStart(_ gesture: UILongPressGestureRecognizer) { + UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { [self] in + if gesture.state == .began { + leftActiveOverlayLayer.backgroundColor = activeOverlayColorConfiguration.getColor(self).cgColor + leftActiveOverlayLayer.opacity = activeOpacity + } else if gesture.state == .cancelled { + leftActiveOverlayLayer.backgroundColor = UIColor.clear.cgColor + leftActiveOverlayLayer.opacity = defaultOpacity + } else if gesture.state == .ended { + leftActiveOverlayLayer.backgroundColor = UIColor.clear.cgColor + leftActiveOverlayLayer.opacity = defaultOpacity + UIView.performWithoutAnimation { + self.onMoveBackward() + } + } + }) + } + + // Move the scrollbar thumb to the right while tapping on the right side of the scrubber. + @objc func onThumbTouchEnd(_ gesture: UILongPressGestureRecognizer) { + UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { [self] in + if gesture.state == .began { + rightActiveOverlayLayer.backgroundColor = activeOverlayColorConfiguration.getColor(self).cgColor + rightActiveOverlayLayer.opacity = activeOpacity + } else if gesture.state == .cancelled { + rightActiveOverlayLayer.backgroundColor = UIColor.clear.cgColor + rightActiveOverlayLayer.opacity = defaultOpacity + } else if gesture.state == .ended { + rightActiveOverlayLayer.backgroundColor = UIColor.clear.cgColor + rightActiveOverlayLayer.opacity = defaultOpacity + self.onMoveForward() + } + }) } private func checkPositions() { totalPositions = Int (ceil (Double(numberOfSlides ?? 1) / Double(_selectedLayout.value))) } - + private func scrollThumbToPosition(_ position: Int?) { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in - self?.setThumb(at: position) - self?.onScrubberDidChange?(position ?? 1) - } + setThumb(at: position) + onScrubberDidChange?(position ?? 1) } private func setThumb(at position: Int?) { - thumbView.frame.origin.x = CGFloat(Float((position ?? 1) - 1) * actualThumbWidth) + trackView.frame.origin.x - updateActiveOverlays() + UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveLinear, animations: { + self.thumbView.frame.origin.x = CGFloat(Float((position ?? 1) - 1) * self.computedWidth) + self.trackView.frame.origin.x + self.updateActiveOverlays() + }) } }