jonhull / GradientSlider

MIT License
241 stars 29 forks source link

Swift 3 #5

Closed danhhgl closed 6 years ago

danhhgl commented 7 years ago
//
//  GradientSlider.swift
//  GradientSlider
//
//  Created by Jonathan Hull on 8/5/15.
//  Copyright © 2015 Jonathan Hull. All rights reserved.
//

import UIKit

@IBDesignable class GradientSlider: UIControl {

    static var defaultThickness:CGFloat = 2.0
    static var defaultThumbSize:CGFloat = 28.0

    //MARK: Properties
    @IBInspectable var hasRainbow:Bool  = false {didSet{updateTrackColors()}}//Uses saturation & lightness from minColor
    @IBInspectable var minColor:UIColor = UIColor.blue {didSet{updateTrackColors()}}
    @IBInspectable var maxColor:UIColor = UIColor.orange {didSet{updateTrackColors()}}

    @IBInspectable var value: CGFloat {
        get{return _value}
        set{setValue(value: newValue, animated: true)}
    }

    func setValue(value:CGFloat, animated:Bool = true) {
        _value = max(min(value,self.maximumValue),self.minimumValue)
        updateThumbPosition(animated: animated)
    }

    @IBInspectable var minimumValue: CGFloat = 0.0 // default 0.0. the current value may change if outside new min value
    @IBInspectable var maximumValue: CGFloat = 1.0 // default 1.0. the current value may change if outside new max value

    @IBInspectable var minimumValueImage: UIImage? = nil { // default is nil. image that appears to left of control (e.g. speaker off)
        didSet{
            if let img = minimumValueImage {
                let imgLayer = _minTrackImageLayer ?? {
                    let l = CALayer()
                    l.anchorPoint = CGPoint(x: 0.0, y: 0.5)
                    self.layer.addSublayer(l)
                    return l
                }()
                imgLayer.contents = img.cgImage
                imgLayer.bounds = CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height)
                _minTrackImageLayer = imgLayer

            }else{
                _minTrackImageLayer?.removeFromSuperlayer()
                _minTrackImageLayer = nil
            }
            self.layer.needsLayout()
        }
    }
    @IBInspectable var maximumValueImage: UIImage? = nil { // default is nil. image that appears to right of control (e.g. speaker max)
        didSet{
            if let img = maximumValueImage {
                let imgLayer = _maxTrackImageLayer ?? {
                    let l = CALayer()
                    l.anchorPoint = CGPoint(x: 1.0, y: 0.5)
                    self.layer.addSublayer(l)
                    return l
                    }()
                imgLayer.contents = img.cgImage
                imgLayer.bounds = CGRect(x: 0, y: 0, width: img.size.width, height: img.size.height)
                _maxTrackImageLayer = imgLayer

            }else{
                _maxTrackImageLayer?.removeFromSuperlayer()
                _maxTrackImageLayer = nil
            }
            self.layer.needsLayout()
        }
    }

    var continuous: Bool = true // if set, value change events are generated any time the value changes due to dragging. default = YES

    var actionBlock:(GradientSlider,CGFloat)->() = {slider,newValue in }

    @IBInspectable var thickness:CGFloat = defaultThickness {
        didSet{
            _trackLayer.cornerRadius = thickness / 2.0
            self.layer.setNeedsLayout()
        }
    }

    var trackBorderColor:UIColor? {
        set{
            _trackLayer.borderColor = newValue?.cgColor
        }
        get{
            if let color = _trackLayer.borderColor {
                return UIColor(cgColor: color)
            }
            return nil
        }
    }

    var trackBorderWidth:CGFloat {
        set{
            _trackLayer.borderWidth = newValue
        }
        get{
            return _trackLayer.borderWidth
        }
    }

    var thumbSize:CGFloat = defaultThumbSize {
        didSet{
            _thumbLayer.cornerRadius = thumbSize / 2.0
            _thumbLayer.bounds = CGRect(x: 0, y: 0, width: thumbSize, height: thumbSize)
            self.invalidateIntrinsicContentSize()
        }
    }

    @IBInspectable var thumbIcon:UIImage? = nil {
        didSet{
            _thumbIconLayer.contents = thumbIcon?.cgImage
        }
    }

    var thumbColor:UIColor {
        get {
            if let color = _thumbIconLayer.backgroundColor {
                return UIColor(cgColor: color)
            }
            return UIColor.white
        }
        set {
            _thumbIconLayer.backgroundColor = newValue.cgColor
            thumbIcon = nil
        }
    }

    //MARK: - Convienience Colors

    func setGradientForHueWithSaturation(saturation:CGFloat,brightness:CGFloat){
        minColor = UIColor(hue: 0.0, saturation: saturation, brightness: brightness, alpha: 1.0)
        hasRainbow = true
    }

    func setGradientForSaturationWithHue(hue:CGFloat,brightness:CGFloat){
        hasRainbow = false
        minColor = UIColor(hue: hue, saturation: 0.0, brightness: brightness, alpha: 1.0)
        maxColor = UIColor(hue: hue, saturation: 1.0, brightness: brightness, alpha: 1.0)
    }

    func setGradientForBrightnessWithHue(hue:CGFloat,saturation:CGFloat){
        hasRainbow = false
        minColor = UIColor.black
        maxColor = UIColor(hue: hue, saturation: saturation, brightness: 1.0, alpha: 1.0)
    }

    func setGradientForRedWithGreen(green:CGFloat,blue:CGFloat){
        hasRainbow = false
        minColor = UIColor(red: 0.0, green: green, blue: blue, alpha: 1.0)
        maxColor = UIColor(red: 1.0, green: green, blue: blue, alpha: 1.0)
    }

    func setGradientForGreenWithRed(red:CGFloat,blue:CGFloat){
        hasRainbow = false
        minColor = UIColor(red: red, green: 0.0, blue: blue, alpha: 1.0)
        maxColor = UIColor(red: red, green: 1.0, blue: blue, alpha: 1.0)
    }

    func setGradientForBlueWithRed(red:CGFloat,green:CGFloat){
        hasRainbow = false
        minColor = UIColor(red: red, green: green, blue: 0.0, alpha: 1.0)
        maxColor = UIColor(red: red, green: green, blue: 1.0, alpha: 1.0)
    }

    func setGradientForGrayscale(){
        hasRainbow = false
        minColor = UIColor.black
        maxColor = UIColor.white
    }

    //MARK: - Private Properties

    private var _value:CGFloat = 0.0 // default 0.0. this value will be pinned to min/max

    private var _thumbLayer:CALayer = {
        let thumb = CALayer()
        thumb.cornerRadius = defaultThumbSize/2.0
        thumb.bounds = CGRect(x: 0, y: 0, width: defaultThumbSize, height: defaultThumbSize)
        thumb.backgroundColor = UIColor.white.cgColor
        thumb.shadowColor = UIColor.black.cgColor
        thumb.shadowOffset = CGSize(width: 0.0, height: 2.5)
        thumb.shadowRadius = 2.0
        thumb.shadowOpacity = 0.25
        thumb.borderColor = UIColor.black.withAlphaComponent(0.15).cgColor
        thumb.borderWidth = 0.5
        return thumb
    }()

    private var _trackLayer:CAGradientLayer = {
        let track = CAGradientLayer()
        track.cornerRadius = defaultThickness / 2.0
        track.startPoint = CGPoint(x: 0.0, y: 0.5)
        track.endPoint = CGPoint(x: 1.0, y: 0.5)
        track.locations = [0.0,1.0]
        track.colors = [UIColor.blue.cgColor,UIColor.orange.cgColor]
        track.borderColor = UIColor.black.cgColor
        return track
    }()

    private var _minTrackImageLayer:CALayer? = nil
    private var _maxTrackImageLayer:CALayer? = nil

    private var _thumbIconLayer:CALayer = {
        let size = defaultThumbSize - 4
        let iconLayer = CALayer()
        iconLayer.cornerRadius = size/2.0
        iconLayer.bounds = CGRect(x: 0, y: 0, width: size, height: size)
        iconLayer.backgroundColor = UIColor.white.cgColor
        return iconLayer
    }()

    //MARK: - Init

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonSetup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        minColor = aDecoder.decodeObject(forKey: "minColor") as? UIColor ?? UIColor.lightGray
        maxColor = aDecoder.decodeObject(forKey: "maxColor") as? UIColor ?? UIColor.darkGray

        value = aDecoder.decodeObject(forKey: "value") as? CGFloat ?? 0.0
        minimumValue = aDecoder.decodeObject(forKey: "minimumValue") as? CGFloat ?? 0.0
        maximumValue = aDecoder.decodeObject(forKey: "maximumValue") as? CGFloat ?? 1.0

        minimumValueImage = aDecoder.decodeObject(forKey: "minimumValueImage") as? UIImage
        maximumValueImage = aDecoder.decodeObject(forKey: "maximumValueImage") as? UIImage

        thickness = aDecoder.decodeObject(forKey: "thickness") as? CGFloat ?? 2.0

        thumbIcon = aDecoder.decodeObject(forKey: "thumbIcon") as? UIImage

        commonSetup()
    }

    override func encode(with aCoder: NSCoder) {
        super.encode(with: aCoder)

        aCoder.encode(minColor, forKey: "minColor")
        aCoder.encode(maxColor, forKey: "maxColor")

        aCoder.encode(value, forKey: "value")
        aCoder.encode(minimumValue, forKey: "minimumValue")
        aCoder.encode(maximumValue, forKey: "maximumValue")

        aCoder.encode(minimumValueImage, forKey: "minimumValueImage")
        aCoder.encode(maximumValueImage, forKey: "maximumValueImage")

        aCoder.encode(thickness, forKey: "thickness")

        aCoder.encode(thumbIcon, forKey: "thumbIcon")

    }

    private func commonSetup() {
        self.layer.delegate = self
        self.layer.addSublayer(_trackLayer)
        self.layer.addSublayer(_thumbLayer)
        _thumbLayer.addSublayer(_thumbIconLayer)
    }

    //MARK: - Layout

    override var intrinsicContentSize: CGSize {
        return CGSize(width: UIViewNoIntrinsicMetric, height: thumbSize)
    }

    override var alignmentRectInsets: UIEdgeInsets {
        return UIEdgeInsetsMake(4.0, 2.0, 4.0, 2.0)
    }

    override func layoutSublayers(of layer: CALayer) {
//        super.layoutSublayersOfLayer(layer)

        if layer != self.layer {return}

        var w = self.bounds.width
        let h = self.bounds.height
        var left:CGFloat = 2.0

        if let minImgLayer = _minTrackImageLayer {
            minImgLayer.position = CGPoint(x: 0.0, y: h/2.0)
            left = minImgLayer.bounds.width + 13.0
        }
        w -= left

        if let maxImgLayer = _maxTrackImageLayer {
            maxImgLayer.position = CGPoint(x: self.bounds.width, y: h/2.0)
            w -= (maxImgLayer.bounds.width + 13.0)
        }else{
            w -= 2.0
        }

        _trackLayer.bounds = CGRect(x: 0, y: 0, width: w, height: thickness)
        _trackLayer.position = CGPoint(x: w/2.0 + left, y: h/2.0)

        let halfSize = thumbSize/2.0
        var layerSize = thumbSize - 4.0
        if let icon = thumbIcon {
            layerSize = min(max(icon.size.height,icon.size.width),layerSize)
            _thumbIconLayer.cornerRadius = 0.0
            _thumbIconLayer.backgroundColor = UIColor.clear.cgColor
        }else{
            _thumbIconLayer.cornerRadius = layerSize/2.0
        }
        _thumbIconLayer.position = CGPoint(x: halfSize, y: halfSize)
        _thumbIconLayer.bounds = CGRect(x: 0, y: 0, width: layerSize, height: layerSize)

        updateThumbPosition(animated: false)
    }

    //MARK: - Touch Tracking

    override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let pt = touch.location(in: self)

        let center = _thumbLayer.position
        let diameter = max(thumbSize,44.0)
        let r = CGRect(x: center.x - diameter/2.0, y: center.y - diameter/2.0, width: diameter, height: diameter)
        if r.contains(pt){
            sendActions(for: UIControlEvents.touchDown)
            return true
        }
        return false
    }

    override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
        let pt = touch.location(in: self)
        let newValue = valueForLocation(point: pt)
        setValue(value: newValue, animated: false)
        if(continuous){
            sendActions(for: UIControlEvents.valueChanged)
            actionBlock(self,newValue)
        }
        return true
    }

    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        if let pt = touch?.location(in: self){
            let newValue = valueForLocation(point: pt)
            setValue(value: newValue, animated: false)
        }
        actionBlock(self,_value)
        sendActions(for: [UIControlEvents.valueChanged, UIControlEvents.touchUpInside])

    }

    //MARK: - Private Functions

    private func updateThumbPosition(animated:Bool){
        let diff = maximumValue - minimumValue
        let perc = CGFloat((value - minimumValue) / diff)

        let halfHeight = self.bounds.height / 2.0
        let trackWidth = _trackLayer.bounds.width - thumbSize
        let left = _trackLayer.position.x - trackWidth/2.0

        if !animated{
            CATransaction.begin() //Move the thumb position without animations
            CATransaction.setValue(true, forKey: kCATransactionDisableActions)
            _thumbLayer.position = CGPoint(x: left + (trackWidth * perc), y: halfHeight)
            CATransaction.commit()
        } else {
            _thumbLayer.position = CGPoint(x: left + (trackWidth * perc), y: halfHeight)
        }
    }

    private func valueForLocation(point:CGPoint)->CGFloat {

        var left = self.bounds.origin.x
        var w = self.bounds.width
        if let minImgLayer = _minTrackImageLayer {
            let amt = minImgLayer.bounds.width + 13.0
            w -= amt
            left += amt
        }else{
            w -= 2.0
            left += 2.0
        }

        if let maxImgLayer = _maxTrackImageLayer {
            w -= (maxImgLayer.bounds.width + 13.0)
        }else{
            w -= 2.0
        }

        let diff = CGFloat(self.maximumValue - self.minimumValue)

        let perc = max(min((point.x - left) / w ,1.0), 0.0)

        return (perc * diff) + CGFloat(self.minimumValue)
    }

    private func updateTrackColors() {
        if !hasRainbow {
            _trackLayer.colors = [minColor.cgColor,maxColor.cgColor]
            _trackLayer.locations = [0.0,1.0]
            return
        }
        //Otherwise make a rainbow with the saturation & lightness of the min color
        var h:CGFloat = 0.0
        var s:CGFloat = 0.0
        var l:CGFloat = 0.0
        var a:CGFloat = 1.0

        minColor.getHue(&h, saturation: &s, brightness: &l, alpha: &a)

        let cnt = 40
        let step:CGFloat = 1.0 / CGFloat(cnt)
        let locations:[CGFloat] = (0...cnt).map({ i in return (step * CGFloat(i))})
        _trackLayer.colors = locations.map({return UIColor(hue: $0, saturation: s, brightness: l, alpha: a).cgColor})
        _trackLayer.locations = locations as [NSNumber]?
    }
}
jonhull commented 7 years ago

Awesome, thanks. Updating to Swift 3 is high on my list (and shouldn't be much work). I have just been swamped with another project for a while.

vkcldhkd commented 7 years ago

thanks!!