You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

UIPanGestureRecognizer无法记录拖拽起点及滑块值异常排查

修复RangeSelectorView的拖拽手势问题

我帮你分析一下代码里的核心问题,然后给出完整的修复方案:

问题根源分析

你遇到的两个问题(只有lowerThumbValue被更新、新拖拽开始滑块跳回初始值),主要来自两个错误:

  1. 错误使用手势的translation而非绝对位置
    你在thumbDragged中用recognizer.translation(in: self)来计算值,但translation是手势起始点到当前点的偏移量,不是触摸点的绝对位置。这会导致计算出的数值完全偏离轨道范围,而且每次新手势开始时translation会重置为(0,0),直接触发滑块跳回初始值。

  2. 未在手势开始时明确拖拽目标
    你在.changed状态才判断触摸位置属于哪个thumb,但因为位置计算错误,导致逻辑始终命中lowerThumb的分支;同时没有记录当前拖拽的thumb,手势过程中无法稳定追踪目标。

修复步骤

1. 添加拖拽状态追踪变量

首先定义一个枚举区分两个滑块,并添加变量记录当前拖拽的目标:

private enum ThumbType {
    case lower, upper
}

private var draggingThumb: ThumbType?

2. 修复轨道值计算方法

原来的getTrackValue没有正确处理轨道的边界范围,修改为:

private func getTrackValue(for position: CGPoint) -> Int {
    // 将触摸点X坐标限制在轨道有效范围内(去掉左右offset)
    let trackStartX = trackOffset
    let trackEndX = bounds.width - trackOffset
    let clampedX = max(trackStartX, min(position.x, trackEndX))
    
    // 计算当前位置对应的百分比,转换为数值
    let trackLength = trackEndX - trackStartX
    let percentage = (clampedX - trackStartX) / trackLength
    let rawValue = min + Int(percentage * CGFloat(max - min))
    
    // 确保数值在min和max之间
    return max(min, min(rawValue, max))
}

3. 重构拖拽手势处理逻辑

在手势的.began状态判断要拖拽的滑块,.changed状态根据目标更新对应值,.ended状态重置拖拽状态:

@objc func thumbDragged(recognizer: UIPanGestureRecognizer) {
    let currentLocation = recognizer.location(in: self)
    let currentValue = getTrackValue(for: currentLocation)
    
    switch recognizer.state {
    case .began:
        // 计算两个滑块的X位置,判断触摸点离哪个更近
        let lowerThumbX = trackOffset + getRelativeThumbPosition(for: lowerThumbValue)
        let upperThumbX = trackOffset + getRelativeThumbPosition(for: upperThumbValue)
        
        let distanceToLower = abs(currentLocation.x - lowerThumbX)
        let distanceToUpper = abs(currentLocation.x - upperThumbX)
        
        draggingThumb = distanceToLower <= distanceToUpper ? .lower : .upper
    case .changed:
        guard let targetThumb = draggingThumb else { return }
        // 更新对应滑块的值,确保lower不超过upper,upper不低于lower
        switch targetThumb {
        case .lower:
            lowerThumbValue = min(currentValue, upperThumbValue)
        case .upper:
            upperThumbValue = max(currentValue, lowerThumbValue)
        }
        setNeedsDisplay()
    case .ended, .cancelled:
        draggingThumb = nil
    default:
        break
    }
}

4. 优化辅助方法(可选但推荐)

将getRelativeThumbPosition中的frame改为bounds(draw方法中应该使用视图自身的bounds而非相对父视图的frame):

private func getRelativeThumbPosition(for value: Int) -> CGFloat {
    let trackLength = bounds.width - trackOffset * 2
    let percentage = CGFloat(value - min) / CGFloat(max - min)
    return percentage * trackLength
}

完整修复后的代码

import UIKit

@IBDesignable
class RangeSelectorView: UIView {
    private let trackWidth: CGFloat = 3.0
    private let thumbRadius: CGFloat = 10.0
    private let trackOffset: CGFloat = 16
    
    private enum ThumbType {
        case lower, upper
    }
    private var draggingThumb: ThumbType?
    
    @IBInspectable let min: Int = 0
    @IBInspectable let max: Int = 100
    @IBInspectable var lowerThumbValue: Int = 0 {
        didSet {
            // 确保值在合法范围内
            lowerThumbValue = max(min, min(lowerThumbValue, upperThumbValue))
            setNeedsDisplay()
        }
    }
    @IBInspectable var upperThumbValue: Int = 100 {
        didSet {
            upperThumbValue = max(lowerThumbValue, min(upperThumbValue, max))
            setNeedsDisplay()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    func setup() {
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(trackTapped))
        addGestureRecognizer(tapGestureRecognizer)
        
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(thumbDragged))
        addGestureRecognizer(panGestureRecognizer)
    }
    
    override func draw(_ rect: CGRect) {
        let centerY = bounds.height / 2
        
        // 绘制基础轨道
        if let trackContext = UIGraphicsGetCurrentContext() {
            trackContext.setLineWidth(trackWidth)
            trackContext.setStrokeColor(UIColor.lightGray.cgColor)
            trackContext.move(to: CGPoint(x: trackOffset, y: centerY))
            trackContext.addLine(to: CGPoint(x: bounds.width - trackOffset, y: centerY))
            trackContext.strokePath()
        }
        
        // 绘制两个滑块
        if let thumbContext = UIGraphicsGetCurrentContext() {
            let lowerThumbX = trackOffset + getRelativeThumbPosition(for: lowerThumbValue)
            let upperThumbX = trackOffset + getRelativeThumbPosition(for: upperThumbValue)
            
            let lowerRect = CGRect(
                x: lowerThumbX - thumbRadius,
                y: centerY - thumbRadius,
                width: thumbRadius * 2,
                height: thumbRadius * 2
            )
            let upperRect = CGRect(
                x: upperThumbX - thumbRadius,
                y: centerY - thumbRadius,
                width: thumbRadius * 2,
                height: thumbRadius * 2
            )
            
            thumbContext.addEllipse(in: lowerRect)
            thumbContext.addEllipse(in: upperRect)
            thumbContext.setFillColor(UIColor.blue.cgColor) // 替换成你的Constants.Colors.titleBlue
            thumbContext.fillPath()
        }
        
        // 绘制高亮区间
        if let highlightedRangeContext = UIGraphicsGetCurrentContext() {
            let lowerThumbX = trackOffset + getRelativeThumbPosition(for: lowerThumbValue)
            let upperThumbX = trackOffset + getRelativeThumbPosition(for: upperThumbValue)
            
            highlightedRangeContext.setLineWidth(trackWidth)
            highlightedRangeContext.setStrokeColor(UIColor.blue.cgColor) // 替换成你的Constants.Colors.titleBlue
            highlightedRangeContext.move(to: CGPoint(x: lowerThumbX, y: centerY))
            highlightedRangeContext.addLine(to: CGPoint(x: upperThumbX, y: centerY))
            highlightedRangeContext.strokePath()
        }
    }
    
    private func getRelativeThumbPosition(for value: Int) -> CGFloat {
        let trackLength = bounds.width - trackOffset * 2
        let percentage = CGFloat(value - min) / CGFloat(max - min)
        return percentage * trackLength
    }
    
    private func getTrackValue(for position: CGPoint) -> Int {
        let trackStartX = trackOffset
        let trackEndX = bounds.width - trackOffset
        let clampedX = max(trackStartX, min(position.x, trackEndX))
        
        let trackLength = trackEndX - trackStartX
        let percentage = (clampedX - trackStartX) / trackLength
        let rawValue = min + Int(percentage * CGFloat(max - min))
        
        return max(min, min(rawValue, max))
    }
    
    @objc func trackTapped(recognizer: UITapGestureRecognizer) {
        let tappedValue = getTrackValue(for: recognizer.location(in: self))
        let midValue = (lowerThumbValue + upperThumbValue) / 2
        
        if tappedValue <= midValue {
            lowerThumbValue = tappedValue
        } else {
            upperThumbValue = tappedValue
        }
        setNeedsDisplay()
    }
    
    @objc func thumbDragged(recognizer: UIPanGestureRecognizer) {
        let currentLocation = recognizer.location(in: self)
        let currentValue = getTrackValue(for: currentLocation)
        
        switch recognizer.state {
        case .began:
            let lowerThumbX = trackOffset + getRelativeThumbPosition(for: lowerThumbValue)
            let upperThumbX = trackOffset + getRelativeThumbPosition(for: upperThumbValue)
            
            let distanceToLower = abs(currentLocation.x - lowerThumbX)
            let distanceToUpper = abs(currentLocation.x - upperThumbX)
            
            draggingThumb = distanceToLower <= distanceToUpper ? .lower : .upper
        case .changed:
            guard let targetThumb = draggingThumb else { return }
            switch targetThumb {
            case .lower:
                lowerThumbValue = currentValue
            case .upper:
                upperThumbValue = currentValue
            }
        case .ended, .cancelled:
            draggingThumb = nil
        default:
            break
        }
    }
}

额外优化说明

  • 给lowerThumbValue和upperThumbValue添加了didSet监听,确保它们始终保持合法范围(lower ≤ upper,且都在min/max之间)
  • 简化了draw方法中的坐标计算,让代码更清晰
  • 替换了示例中的颜色,你可以改回自己的Constants.Colors.titleBlue

内容的提问来源于stack exchange,提问作者procra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:50:12