将自动收报机添加到捕捉 UISlider

Add ticker to snapping UISlider

我在创建自定义 UISlider 时遇到一些问题,该自定义 UISlider 在某些值处捕捉,并且在这些值处也有刻度线。我有捕捉部分工作,但刻度线是一个问题。滑块卡在 12 个位置,从 15-70 每 5 个。我尝试了多种方法,包括将 12 个子视图添加到堆栈视图并将其放置在滑块上,还计算了每个子视图之间的步长。这两种方法都将刻度线放在拇指卡住的地方。有谁知道如何正确执行此操作?

这是第二种方法:

        let stepCount = 12
        print("THE WIDTH: \(self.bounds.width)")
        guard let imageWidth = self.currentThumbImage?.size.width else {return}

        guard let imageWidth = self.currentThumbImage?.size.width else {return}
        let stepWidth = bounds.width / CGFloat(stepCount)
        for i in 0..<stepCount {
            let view = UIView(frame: CGRect(x: stepWidth / 2 + stepWidth * CGFloat(i) - imageWidth / 2, y: 0, width: 1, height: 29))
            view.backgroundColor = .lightGray
            self.insertSubview(view, at: 0)
        }

这是捕捉代码:

@objc func valueChanged(_ sender: UISlider){
        let step: Float = 5
        let roundedValue = round(sender.value / step) * step
        self.value = roundedValue
        delegate?.sliderChanged(Int(roundedValue), sender)
    }

您需要处理各种宽度问题。

首先,看看这三个“默认”UISlider 控件。拇指垂直偏移所以我们可以看到轨道,红色虚线轮廓是滑块框架:

  • 最上面的是最小值(最左边)
  • 中间那个是50%
  • 底部的那个是最大值(最右边)

我们可以看到,拇指的水平 center NOT 在轨道矩形的结束(边界)。

如果我们希望刻度线与拇指的水平中心对齐,我们需要根据拇指中心的原点和宽度在最小值和最大值处计算 x 位置:

下一个问题是您可能不希望轨道矩形/图像延伸到刻度线的 left/right。

那样的话,我们需要清除内置的轨迹图,自己绘制:

下面是一些您可以尝试使用的示例代码:

protocol TickerSliderDelegate: NSObject {
    func sliderChanged(_ newValue: Int, sender: Any)
}
extension TickerSliderDelegate {
    // make this delegate func optional
    func sliderChanged(_ newValue: Int, sender: Any) {}
}

class TickerSlider: UISlider {
    
    var delegate: TickerSliderDelegate?
    
    var stepCount = 12
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        // clear min and max track images
        //  because we'll be drawing our own
        setMinimumTrackImage(UIImage(), for: [])
        setMaximumTrackImage(UIImage(), for: [])
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        // get the track rect
        let trackR: CGRect = self.trackRect(forBounds: bounds)
        
        // get the thumb rect at min and max values
        let minThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: minimumValue)
        let maxThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: maximumValue)
        
        // usable width is center of thumb to center of thumb at min and max values
        let usableWidth: CGFloat = maxThumbR.midX - minThumbR.midX
        
        // Tick Height (or use desired explicit height)
        let tickHeight: CGFloat = bounds.height
        
        // "gap" between tick marks
        let stepWidth: CGFloat = usableWidth / CGFloat(stepCount)
        
        // a reusable path
        var pth: UIBezierPath!
        
        // a reusable point
        var pt: CGPoint!
                
        // new path
        pth = UIBezierPath()
        
        // left end of our track rect
        pt = CGPoint(x: minThumbR.midX, y: bounds.height * 0.5)
        
        // top of vertical tick lines
        pt.y = (bounds.height - tickHeight) * 0.5
        
        // we have to draw stepCount + 1 lines
        //  so use
        //      0...stepCount
        //  not
        //      0..<stepCount
        for _ in 0...stepCount {
            pth.move(to: pt)
            pth.addLine(to: CGPoint(x: pt.x, y: pt.y + tickHeight))
            pt.x += stepWidth
        }
        UIColor.lightGray.setStroke()
        pth.stroke()
        
        // new path
        pth = UIBezierPath()
        
        // left end of our track lines
        pt = CGPoint(x: minThumbR.midX, y: bounds.height * 0.5)
        
        // move to left end
        pth.move(to: pt)
        
        // draw the "right-side" of the track first
        //  it will be the full width of "our track"
        pth.addLine(to: CGPoint(x: pt.x + usableWidth, y: pt.y))
        pth.lineWidth = 3
        UIColor.lightGray.setStroke()
        pth.stroke()
        
        // new path
        pth = UIBezierPath()
        
        // move to left end
        pth.move(to: pt)
        
        // draw the "left-side" of the track on top of the "right-side"
        //  at percentage width
        let rng: Float = maximumValue - minimumValue
        let val: Float = value - minimumValue
        let pct: Float = val / rng
        pth.addLine(to: CGPoint(x: pt.x + (usableWidth * CGFloat(pct)), y: pt.y))
        pth.lineWidth = 3
        UIColor.systemBlue.setStroke()
        pth.stroke()

    }
    
    override func setValue(_ value: Float, animated: Bool) {
        // don't allow value outside range of min and max values
        let newVal: Float = min(max(minimumValue, value), maximumValue)
        super.setValue(newVal, animated: animated)
        
        // we need to trigger draw() when the value changes
        setNeedsDisplay()
        let steps: Float = Float(stepCount)
        let rng: Float = maximumValue - minimumValue
        // get the percentage along the track
        let pct: Float = newVal / rng
        // use that pct to get the rounded step position
        let pos: Float = round(steps * pct)
        // tell the delegate which Tick the thumb snapped to
        delegate?.sliderChanged(Int(pos), sender: self)
    }
    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        super.endTracking(touch, with: event)
        
        let steps: Float = Float(stepCount)
        let rng: Float = maximumValue - minimumValue
        // get the percentage along the track
        let pct: Float = value / rng
        // use that pct to get the rounded step position
        let pos: Float = round(steps * pct)
        // use that pos to calculate the new percentage
        let newPct: Float = (pos / steps)
        let newVal: Float = minimumValue + (rng * newPct)
        self.value = newVal
    }
    override var bounds: CGRect {
        willSet {
            // we need to trigger draw() when the bounds changes
            setNeedsDisplay()
        }
    }
    
}

注意:这只是此任务的一个方法(并且是仅示例代码).搜索会发现许多您可能想要查看的其他方法/示例/等。


编辑

这是一个非常轻微的修改版本,以获得更“点准确”的刻度线/拇指对齐:

class TickerSlider: UISlider {
    
    var delegate: TickerSliderDelegate?
    
    var stepCount = 12
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {

        // clear min and max track images
        //  because we'll be drawing our own
        setMinimumTrackImage(UIImage(), for: [])
        setMaximumTrackImage(UIImage(), for: [])
        
        // if we're using a custom thumb image
        if let img = UIImage(named: "CustomThumbA") {
            self.setThumbImage(img, for: [])
        }

    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        // get the track rect
        let trackR: CGRect = self.trackRect(forBounds: bounds)
        
        // get the thumb rect at min and max values
        let minThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: minimumValue)
        let maxThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: maximumValue)
        
        // usable width is center of thumb to center of thumb at min and max values
        let usableWidth: CGFloat = maxThumbR.midX - minThumbR.midX
        
        // Tick Height (or use desired explicit height)
        let tickHeight: CGFloat = bounds.height
        
        // a reusable path
        var pth: UIBezierPath!
        
        // a reusable point
        var pt: CGPoint!
        
        // new path
        pth = UIBezierPath()
        
        pt = .zero
        
        // top of vertical tick lines
        pt.y = (bounds.height - tickHeight) * 0.5
        
        // we have to draw stepCount + 1 lines
        //  so use
        //      0...stepCount
        //  not
        //      0..<stepCount
        for i in 0...stepCount {
            // get center of Thumb at each "step"
            let aThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: Float(i) / Float(stepCount))
            pt.x = aThumbR.midX
            pth.move(to: pt)
            pth.addLine(to: CGPoint(x: pt.x, y: pt.y + tickHeight))
        }
        UIColor.lightGray.setStroke()
        pth.stroke()
        
        // new path
        pth = UIBezierPath()
        
        // left end of our track lines
        pt = CGPoint(x: minThumbR.midX, y: bounds.height * 0.5)
        
        // move to left end
        pth.move(to: pt)
        
        // draw the "right-side" of the track first
        //  it will be the full width of "our track"
        pth.addLine(to: CGPoint(x: pt.x + usableWidth, y: pt.y))
        pth.lineWidth = 3
        UIColor.lightGray.setStroke()
        pth.stroke()
        
        // new path
        pth = UIBezierPath()
        
        // move to left end
        pth.move(to: pt)
        
        // draw the "left-side" of the track on top of the "right-side"
        //  at percentage width
        let rng: Float = maximumValue - minimumValue
        let val: Float = value - minimumValue
        let pct: Float = val / rng
        pth.addLine(to: CGPoint(x: pt.x + (usableWidth * CGFloat(pct)), y: pt.y))
        pth.lineWidth = 3
        UIColor.systemBlue.setStroke()
        pth.stroke()
        
    }
    
    override func setValue(_ value: Float, animated: Bool) {
        // don't allow value outside range of min and max values
        let newVal: Float = min(max(minimumValue, value), maximumValue)
        super.setValue(newVal, animated: animated)
        
        // we need to trigger draw() when the value changes
        setNeedsDisplay()
        let steps: Float = Float(stepCount)
        let rng: Float = maximumValue - minimumValue
        // get the percentage along the track
        let pct: Float = newVal / rng
        // use that pct to get the rounded step position
        let pos: Float = round(steps * pct)
        // tell the delegate which Tick the thumb snapped to
        delegate?.sliderChanged(Int(pos), sender: self)
    }
    override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
        super.endTracking(touch, with: event)
        
        let steps: Float = Float(stepCount)
        let rng: Float = maximumValue - minimumValue
        // get the percentage along the track
        let pct: Float = value / rng
        // use that pct to get the rounded step position
        let pos: Float = round(steps * pct)
        // use that pos to calculate the new percentage
        let newPct: Float = (pos / steps)
        let newVal: Float = minimumValue + (rng * newPct)
        self.value = newVal
    }
    override var bounds: CGRect {
        willSet {
            // we need to trigger draw() when the bounds changes
            setNeedsDisplay()
        }
    }
    
}

主要区别在于,在绘制刻度线的循环中,我们不是计算“间隙”值,而是获取每一步的“拇指中心”:

    for i in 0...stepCount {
        // get center of Thumb at each "step"
        let aThumbR: CGRect = self.thumbRect(forBounds: bounds, trackRect: trackR, value: Float(i) / Float(stepCount))
        pt.x = aThumbR.midX
        pth.move(to: pt)
        pth.addLine(to: CGPoint(x: pt.x, y: pt.y + tickHeight))
    }

这是在第 0、1 和 2 步使用自定义缩略图的图像:

以及我用于拇指的 @2x (62x62) 和 @3x (93x93) 图像: