【问题标题】:Add ticker to snapping UISlider添加代码以捕捉 UISlider
【发布时间】:2021-12-17 09:55:28
【问题描述】:

我在创建自定义 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)
    }

【问题讨论】:

    标签: ios swift uislider


    【解决方案1】:

    您需要处理各种问题。

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

    • 顶部是最小值(尽可能向左)
    • 中间是50%
    • 而底部是最大值(尽可能远)

    如我们所见,拇指的水平中心在轨迹矩形的末端(边界)不是

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

    下一个问题是您可能不希望轨道 rect / images 延伸到刻度线的左/右。

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

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

    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) 图像:

    【讨论】:

    • 感谢您的回复,快速提问。我实现了这一点,当移动滑块时,它与刻度不对齐,但是当完成移动滑块时,它会捕捉到一个刻度线,但不会捕捉到正确的值。我在滑块上还有一个自定义的拇指图像,这会影响实现吗?
    • @aFella -- 它是否没有自定义拇指图像捕捉到正确的值?
    • 它没有。我最终摆脱了您的 setValue 和 endTracking 函数并使用了我的 valueChanged 函数,它运行良好。刻度正是滑块捕捉的位置,非常感谢您的帮助!
    • @aFella - 查看我的编辑以获得更准确的刻度线位置。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多