From b9f7ae669f2b0ebcfa15ea9432a9ea6f2854b6b5 Mon Sep 17 00:00:00 2001 From: GiladR1979 Date: Wed, 18 Oct 2017 09:22:09 +0300 Subject: [PATCH] Added move thumbs by touch + fix for stuck thumbs Now touching slider moves the closest thumb to touch point. Also- fixed a bug- when two thumbs are at the extreme- trying to drag open the range doesn't work because the most extreme thumb gets the touch (instead of the inner thumb that needs to move). Also, dragging one thumb over the other opens the range in the opposite direction (the thumbs "flip sides" and continue to move). --- RangeSlider/RangeSlider.swift | 590 ++++++++++++++++++---------------- 1 file changed, 317 insertions(+), 273 deletions(-) diff --git a/RangeSlider/RangeSlider.swift b/RangeSlider/RangeSlider.swift index ccaa0f2..8ff1bf2 100755 --- a/RangeSlider/RangeSlider.swift +++ b/RangeSlider/RangeSlider.swift @@ -10,284 +10,328 @@ import UIKit import QuartzCore class RangeSliderTrackLayer: CALayer { - weak var rangeSlider: RangeSlider? - - override func draw(in ctx: CGContext) { - guard let slider = rangeSlider else { - return - } - - // Clip - let cornerRadius = bounds.height * slider.curvaceousness / 2.0 - let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius) - ctx.addPath(path.cgPath) - - // Fill the track - ctx.setFillColor(slider.trackTintColor.cgColor) - ctx.addPath(path.cgPath) - ctx.fillPath() - - // Fill the highlighted range - ctx.setFillColor(slider.trackHighlightTintColor.cgColor) - let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue)) - let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue)) - let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height) - ctx.fill(rect) - } + weak var rangeSlider: RangeSlider? + + override func draw(in ctx: CGContext) { + guard let slider = rangeSlider else { + return + } + + // Clip + let cornerRadius = bounds.height * slider.curvaceousness / 2.0 + let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius) + ctx.addPath(path.cgPath) + + // Fill the track + ctx.setFillColor(slider.trackTintColor.cgColor) + ctx.addPath(path.cgPath) + ctx.fillPath() + + // Fill the highlighted range + ctx.setFillColor(slider.trackHighlightTintColor.cgColor) + let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue)) + let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue)) + let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height) + ctx.fill(rect) + } } class RangeSliderThumbLayer: CALayer { - - var highlighted: Bool = false { - didSet { - setNeedsDisplay() - } - } - weak var rangeSlider: RangeSlider? - - var strokeColor: UIColor = UIColor.gray { - didSet { - setNeedsDisplay() - } - } - var lineWidth: CGFloat = 0.5 { - didSet { - setNeedsDisplay() - } - } - - override func draw(in ctx: CGContext) { - guard let slider = rangeSlider else { - return - } - - let thumbFrame = bounds.insetBy(dx: 2.0, dy: 2.0) - let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0 - let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius) - - // Fill - ctx.setFillColor(slider.thumbTintColor.cgColor) - ctx.addPath(thumbPath.cgPath) - ctx.fillPath() - - // Outline - ctx.setStrokeColor(strokeColor.cgColor) - ctx.setLineWidth(lineWidth) - ctx.addPath(thumbPath.cgPath) - ctx.strokePath() - - if highlighted { - ctx.setFillColor(UIColor(white: 0.0, alpha: 0.1).cgColor) - ctx.addPath(thumbPath.cgPath) - ctx.fillPath() - } - } + + var highlighted: Bool = false { + didSet { + setNeedsDisplay() + } + } + weak var rangeSlider: RangeSlider? + + var strokeColor: UIColor = UIColor.gray { + didSet { + setNeedsDisplay() + } + } + var lineWidth: CGFloat = 0.5 { + didSet { + setNeedsDisplay() + } + } + + override func draw(in ctx: CGContext) { + guard let slider = rangeSlider else { + return + } + + let thumbFrame = bounds.insetBy(dx: 1.0, dy: 1.0) //bounds.insetBy(dx: 2.0, dy: 2.0) + let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0 + let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius) + + // Fill + ctx.setFillColor(slider.thumbTintColor.cgColor) + ctx.addPath(thumbPath.cgPath) + ctx.fillPath() + + // Outline + ctx.setStrokeColor(strokeColor.cgColor) + ctx.setLineWidth(lineWidth) + ctx.addPath(thumbPath.cgPath) + ctx.strokePath() + + if highlighted { + ctx.setFillColor(UIColor(white: 0.0, alpha: 0.1).cgColor) + ctx.addPath(thumbPath.cgPath) + ctx.fillPath() + } + } } @IBDesignable public class RangeSlider: UIControl { - @IBInspectable public var minimumValue: Double = 0.0 { - willSet(newValue) { - assert(newValue < maximumValue, "RangeSlider: minimumValue should be lower than maximumValue") - } - didSet { - updateLayerFrames() - } - } - - @IBInspectable public var maximumValue: Double = 1.0 { - willSet(newValue) { - assert(newValue > minimumValue, "RangeSlider: maximumValue should be greater than minimumValue") - } - didSet { - updateLayerFrames() - } - } - - @IBInspectable public var lowerValue: Double = 0.2 { - didSet { - if lowerValue < minimumValue { - lowerValue = minimumValue - } - updateLayerFrames() - } - } - - @IBInspectable public var upperValue: Double = 0.8 { - didSet { - if upperValue > maximumValue { - upperValue = maximumValue - } - updateLayerFrames() - } - } - - var gapBetweenThumbs: Double { - return 0.5 * Double(thumbWidth) * (maximumValue - minimumValue) / Double(bounds.width) - } - - @IBInspectable public var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) { - didSet { - trackLayer.setNeedsDisplay() - } - } - - @IBInspectable public var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) { - didSet { - trackLayer.setNeedsDisplay() - } - } - - @IBInspectable public var thumbTintColor: UIColor = UIColor.white { - didSet { - lowerThumbLayer.setNeedsDisplay() - upperThumbLayer.setNeedsDisplay() - } - } - - @IBInspectable public var thumbBorderColor: UIColor = UIColor.gray { - didSet { - lowerThumbLayer.strokeColor = thumbBorderColor - upperThumbLayer.strokeColor = thumbBorderColor - } - } - - @IBInspectable public var thumbBorderWidth: CGFloat = 0.5 { - didSet { - lowerThumbLayer.lineWidth = thumbBorderWidth - upperThumbLayer.lineWidth = thumbBorderWidth - } - } - - @IBInspectable public var curvaceousness: CGFloat = 1.0 { - didSet { - if curvaceousness < 0.0 { - curvaceousness = 0.0 - } - - if curvaceousness > 1.0 { - curvaceousness = 1.0 - } - - trackLayer.setNeedsDisplay() - lowerThumbLayer.setNeedsDisplay() - upperThumbLayer.setNeedsDisplay() - } - } - - fileprivate var previouslocation = CGPoint() - - fileprivate let trackLayer = RangeSliderTrackLayer() - fileprivate let lowerThumbLayer = RangeSliderThumbLayer() - fileprivate let upperThumbLayer = RangeSliderThumbLayer() - - fileprivate var thumbWidth: CGFloat { - return CGFloat(bounds.height) - } - - override public var frame: CGRect { - didSet { - updateLayerFrames() - } - } - - override public init(frame: CGRect) { - super.init(frame: frame) - initializeLayers() - } - - required public init?(coder: NSCoder) { - super.init(coder: coder) - initializeLayers() - } - - override public func layoutSublayers(of: CALayer) { - super.layoutSublayers(of:layer) - updateLayerFrames() - } - - fileprivate func initializeLayers() { - layer.backgroundColor = UIColor.clear.cgColor - - trackLayer.rangeSlider = self - trackLayer.contentsScale = UIScreen.main.scale - layer.addSublayer(trackLayer) - - lowerThumbLayer.rangeSlider = self - lowerThumbLayer.contentsScale = UIScreen.main.scale - layer.addSublayer(lowerThumbLayer) - - upperThumbLayer.rangeSlider = self - upperThumbLayer.contentsScale = UIScreen.main.scale - layer.addSublayer(upperThumbLayer) - } - - func updateLayerFrames() { - CATransaction.begin() - CATransaction.setDisableActions(true) - - trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height/3) - trackLayer.setNeedsDisplay() - - let lowerThumbCenter = CGFloat(positionForValue(lowerValue)) - lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth/2.0, y: 0.0, width: thumbWidth, height: thumbWidth) - lowerThumbLayer.setNeedsDisplay() - - let upperThumbCenter = CGFloat(positionForValue(upperValue)) - upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth/2.0, y: 0.0, width: thumbWidth, height: thumbWidth) - upperThumbLayer.setNeedsDisplay() - - CATransaction.commit() - } - - func positionForValue(_ value: Double) -> Double { - return Double(bounds.width - thumbWidth) * (value - minimumValue) / - (maximumValue - minimumValue) + Double(thumbWidth/2.0) - } - - func boundValue(_ value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double { - return min(max(value, lowerValue), upperValue) - } - - - // MARK: - Touches - - override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - previouslocation = touch.location(in: self) - - // Hit test the thumb layers - if lowerThumbLayer.frame.contains(previouslocation) { - lowerThumbLayer.highlighted = true - } else if upperThumbLayer.frame.contains(previouslocation) { - upperThumbLayer.highlighted = true - } - - return lowerThumbLayer.highlighted || upperThumbLayer.highlighted - } - - override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { - let location = touch.location(in: self) - - // Determine by how much the user has dragged - let deltaLocation = Double(location.x - previouslocation.x) - let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height) - - previouslocation = location - - // Update the values - if lowerThumbLayer.highlighted { - lowerValue = boundValue(lowerValue + deltaValue, toLowerValue: minimumValue, upperValue: upperValue - gapBetweenThumbs) - } else if upperThumbLayer.highlighted { - upperValue = boundValue(upperValue + deltaValue, toLowerValue: lowerValue + gapBetweenThumbs, upperValue: maximumValue) - } - - sendActions(for: .valueChanged) - - return true - } - - override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { - lowerThumbLayer.highlighted = false - upperThumbLayer.highlighted = false - } + @IBInspectable public var minimumValue: Double = 0.0 { + willSet(newValue) { + assert(newValue < maximumValue, "RangeSlider: minimumValue should be lower than maximumValue") + } + didSet { + updateLayerFrames() + } + } + + @IBInspectable public var maximumValue: Double = 1.0 { + willSet(newValue) { + assert(newValue > minimumValue, "RangeSlider: maximumValue should be greater than minimumValue") + } + didSet { + updateLayerFrames() + } + } + + @IBInspectable public var lowerValue: Double = 0.2 { + didSet { + if lowerValue < minimumValue { + lowerValue = minimumValue + } + updateLayerFrames() + } + } + + @IBInspectable public var upperValue: Double = 0.8 { + didSet { + if upperValue > maximumValue { + upperValue = maximumValue + } + updateLayerFrames() + } + } + + var gapBetweenThumbs: Double { + return 0.5 * Double(thumbWidth) * (maximumValue - minimumValue) / Double(bounds.width) + } + + @IBInspectable public var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) { + didSet { + trackLayer.setNeedsDisplay() + } + } + + @IBInspectable public var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) { + didSet { + trackLayer.setNeedsDisplay() + } + } + + @IBInspectable public var thumbTintColor: UIColor = UIColor.white { + didSet { + lowerThumbLayer.setNeedsDisplay() + upperThumbLayer.setNeedsDisplay() + } + } + + @IBInspectable public var thumbBorderColor: UIColor = UIColor.gray { + didSet { + lowerThumbLayer.strokeColor = thumbBorderColor + upperThumbLayer.strokeColor = thumbBorderColor + } + } + + @IBInspectable public var thumbBorderWidth: CGFloat = 0.5 { + didSet { + lowerThumbLayer.lineWidth = thumbBorderWidth + upperThumbLayer.lineWidth = thumbBorderWidth + } + } + + @IBInspectable public var curvaceousness: CGFloat = 1.0 { + didSet { + if curvaceousness < 0.0 { + curvaceousness = 0.0 + } + + if curvaceousness > 1.0 { + curvaceousness = 1.0 + } + + trackLayer.setNeedsDisplay() + lowerThumbLayer.setNeedsDisplay() + upperThumbLayer.setNeedsDisplay() + } + } + + fileprivate var previouslocation = CGPoint() + + fileprivate let trackLayer = RangeSliderTrackLayer() + fileprivate let lowerThumbLayer = RangeSliderThumbLayer() + fileprivate let upperThumbLayer = RangeSliderThumbLayer() + + fileprivate var thumbWidth: CGFloat { + return CGFloat(bounds.height) + } + + override public var frame: CGRect { + didSet { + updateLayerFrames() + } + } + + override public init(frame: CGRect) { + super.init(frame: frame) + initializeLayers() + } + + required public init?(coder: NSCoder) { + super.init(coder: coder) + initializeLayers() + } + + override public func layoutSublayers(of: CALayer) { + super.layoutSublayers(of:layer) + updateLayerFrames() + } + + fileprivate func initializeLayers() { + layer.backgroundColor = UIColor.clear.cgColor + + trackLayer.rangeSlider = self + trackLayer.contentsScale = UIScreen.main.scale + layer.addSublayer(trackLayer) + + lowerThumbLayer.rangeSlider = self + lowerThumbLayer.contentsScale = UIScreen.main.scale + layer.addSublayer(lowerThumbLayer) + + upperThumbLayer.rangeSlider = self + upperThumbLayer.contentsScale = UIScreen.main.scale + layer.addSublayer(upperThumbLayer) + } + + func updateLayerFrames() { + CATransaction.begin() + CATransaction.setDisableActions(true) + + trackLayer.frame = bounds.insetBy(dx: 0.0, dy: bounds.height/3) + trackLayer.setNeedsDisplay() + + let lowerThumbCenter = CGFloat(positionForValue(lowerValue)) + lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth/2.0, y: 0.0, width: thumbWidth, height: thumbWidth) + lowerThumbLayer.setNeedsDisplay() + + let upperThumbCenter = CGFloat(positionForValue(upperValue)) + upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth/2.0, y: 0.0, width: thumbWidth, height: thumbWidth) + upperThumbLayer.setNeedsDisplay() + + CATransaction.commit() + } + + func positionForValue(_ value: Double) -> Double { + return Double(bounds.width - thumbWidth) * (value - minimumValue) / + (maximumValue - minimumValue) + Double(thumbWidth/2.0) + } + + func boundValue(_ value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double { + return min(max(value, lowerValue), upperValue) + } + + func getThumbLayerCloserToTouch(touchLocation: CGPoint) -> RangeSliderThumbLayer { + let xLower = lowerThumbLayer.frame.origin.x + let yLower = lowerThumbLayer.frame.origin.y + let xUpper = upperThumbLayer.frame.origin.x + let yUpper = upperThumbLayer.frame.origin.y + + let distanceSquaredToLower = pow(xLower - touchLocation.x, 2.0) + pow(yLower - touchLocation.y, 2.0) + let distanceSquaredToUpper = pow(xUpper - touchLocation.x, 2.0) + pow(yUpper - touchLocation.y, 2.0) + + if distanceSquaredToLower < distanceSquaredToUpper { + return lowerThumbLayer + } else { + return upperThumbLayer + } + } + + + // MARK: - Touches + + override public func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + previouslocation = touch.location(in: self) + + // Hit test the thumb layers + // if lowerThumbLayer.frame.contains(previouslocation) { + // lowerThumbLayer.highlighted = true + // } else if upperThumbLayer.frame.contains(previouslocation) { + // upperThumbLayer.highlighted = true + // } + + // Alternative method (Calculating squared distance to each thumb): + let closerThumb = getThumbLayerCloserToTouch(touchLocation: previouslocation) + + if closerThumb == lowerThumbLayer { + let lowerThumbCenter = CGFloat(positionForValue(lowerValue)) + let deltaLocation = Double(previouslocation.x - lowerThumbCenter) + let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height) + lowerValue = boundValue(lowerValue + deltaValue, toLowerValue: minimumValue, upperValue: upperValue - gapBetweenThumbs) + lowerThumbLayer.highlighted = true + } else if closerThumb == upperThumbLayer { + let upperThumbCenter = CGFloat(positionForValue(upperValue)) + let deltaLocation = Double(previouslocation.x - upperThumbCenter) + let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height) + upperValue = boundValue(upperValue + deltaValue, toLowerValue: lowerValue + gapBetweenThumbs, upperValue: maximumValue) + upperThumbLayer.highlighted = true + } + + sendActions(for: .valueChanged) + + return lowerThumbLayer.highlighted || upperThumbLayer.highlighted + } + + override public func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + let location = touch.location(in: self) + + // Determine by how much the user has dragged + let deltaLocation = Double(location.x - previouslocation.x) + let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height) + + previouslocation = location + + if lowerThumbLayer.highlighted && lowerValue + deltaValue > upperValue - gapBetweenThumbs { + lowerThumbLayer.highlighted = false + upperThumbLayer.highlighted = true + } else if upperThumbLayer.highlighted && upperValue + deltaValue < lowerValue + gapBetweenThumbs { + upperThumbLayer.highlighted = false + lowerThumbLayer.highlighted = true + } + + // Update the values + if lowerThumbLayer.highlighted { + lowerValue = boundValue(lowerValue + deltaValue, toLowerValue: minimumValue, upperValue: upperValue - gapBetweenThumbs) + } else if upperThumbLayer.highlighted { + upperValue = boundValue(upperValue + deltaValue, toLowerValue: lowerValue + gapBetweenThumbs, upperValue: maximumValue) + } + + sendActions(for: .valueChanged) + + return true + } + + override public func endTracking(_ touch: UITouch?, with event: UIEvent?) { + lowerThumbLayer.highlighted = false + upperThumbLayer.highlighted = false + } + }