UIPanGestureRecognizer无法记录拖拽起点及滑块值异常排查
修复RangeSelectorView的拖拽手势问题
我帮你分析一下代码里的核心问题,然后给出完整的修复方案:
问题根源分析
你遇到的两个问题(只有lowerThumbValue被更新、新拖拽开始滑块跳回初始值),主要来自两个错误:
错误使用手势的
translation而非绝对位置
你在thumbDragged中用recognizer.translation(in: self)来计算值,但translation是手势起始点到当前点的偏移量,不是触摸点的绝对位置。这会导致计算出的数值完全偏离轨道范围,而且每次新手势开始时translation会重置为(0,0),直接触发滑块跳回初始值。未在手势开始时明确拖拽目标
你在.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
相关产品推荐
相关产品推荐

