137 lines
3.6 KiB
Swift
137 lines
3.6 KiB
Swift
|
|
//
|
||
|
|
// GestureSlider.swift
|
||
|
|
// Crush
|
||
|
|
//
|
||
|
|
// Created by Leon on 2025/7/21.
|
||
|
|
//
|
||
|
|
|
||
|
|
import UIKit
|
||
|
|
|
||
|
|
/// 音频专用滑动条
|
||
|
|
class CustomSlider: UIControl {
|
||
|
|
private let trackView = UIView()
|
||
|
|
private var tickViews: [UIView] = []
|
||
|
|
private let thumbView = UIView()
|
||
|
|
|
||
|
|
var numberOfSteps: Int = 11 {
|
||
|
|
didSet {
|
||
|
|
setupTicks()
|
||
|
|
layoutIfNeeded()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@Published var selectedStep: Int = 5 {
|
||
|
|
didSet {
|
||
|
|
updateThumbPosition(animated: true)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private let trackHeight: CGFloat = 20
|
||
|
|
private let tickWidth: CGFloat = 2
|
||
|
|
private let tickHeight: CGFloat = 12
|
||
|
|
private let thumbSize = CGSize(width: 8, height: 28)
|
||
|
|
|
||
|
|
override init(frame: CGRect) {
|
||
|
|
super.init(frame: frame)
|
||
|
|
setup()
|
||
|
|
}
|
||
|
|
|
||
|
|
required init?(coder: NSCoder) {
|
||
|
|
super.init(coder: coder)
|
||
|
|
setup()
|
||
|
|
}
|
||
|
|
|
||
|
|
private func setup() {
|
||
|
|
backgroundColor = .clear
|
||
|
|
|
||
|
|
// Track
|
||
|
|
trackView.backgroundColor = .c.csen
|
||
|
|
trackView.layer.cornerRadius = 12
|
||
|
|
addSubview(trackView)
|
||
|
|
trackView.snp.makeConstraints { make in
|
||
|
|
make.edges.equalToSuperview()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Thumb
|
||
|
|
thumbView.backgroundColor = .c.cpvn
|
||
|
|
thumbView.layer.cornerRadius = thumbSize.width / 2
|
||
|
|
addSubview(thumbView)
|
||
|
|
|
||
|
|
// Gesture
|
||
|
|
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
||
|
|
addGestureRecognizer(pan)
|
||
|
|
|
||
|
|
setupTicks()
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
private func setupTicks() {
|
||
|
|
tickViews.forEach { $0.removeFromSuperview() }
|
||
|
|
tickViews.removeAll()
|
||
|
|
|
||
|
|
for _ in 0 ..< numberOfSteps {
|
||
|
|
let tick = UIView()
|
||
|
|
tick.backgroundColor = UIColor(white: 1, alpha: 0.3)
|
||
|
|
addSubview(tick)
|
||
|
|
tickViews.append(tick)
|
||
|
|
}
|
||
|
|
|
||
|
|
bringSubviewToFront(thumbView)
|
||
|
|
}
|
||
|
|
|
||
|
|
override func layoutSubviews() {
|
||
|
|
super.layoutSubviews()
|
||
|
|
|
||
|
|
// Layout ticks
|
||
|
|
let lrPadding = 24.0
|
||
|
|
let ticksWitdh = bounds.width - lrPadding * 2
|
||
|
|
let spacing = ticksWitdh / CGFloat(numberOfSteps - 1)
|
||
|
|
for (index, tick) in tickViews.enumerated() {
|
||
|
|
let x = CGFloat(index) * spacing + lrPadding // 24 is leftPadding
|
||
|
|
tick.frame = CGRect(
|
||
|
|
x: x - tickWidth / 2,
|
||
|
|
y: (bounds.height - tickHeight) / 2,
|
||
|
|
width: tickWidth,
|
||
|
|
height: tickHeight
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
updateThumbPosition(animated: false)
|
||
|
|
}
|
||
|
|
|
||
|
|
private func updateThumbPosition(animated: Bool) {
|
||
|
|
guard numberOfSteps > 1 else { return }
|
||
|
|
let lrPadding = 24.0
|
||
|
|
let ticksWitdh = bounds.width - lrPadding * 2
|
||
|
|
let spacing = ticksWitdh / CGFloat(numberOfSteps - 1)
|
||
|
|
let x = CGFloat(selectedStep) * spacing - thumbSize.width / 2 + lrPadding
|
||
|
|
let y = (bounds.height - thumbSize.height) / 2
|
||
|
|
|
||
|
|
let update = {
|
||
|
|
self.thumbView.frame = CGRect(origin: CGPoint(x: x, y: y), size: self.thumbSize)
|
||
|
|
}
|
||
|
|
|
||
|
|
if animated {
|
||
|
|
UIView.animate(withDuration: 0.2, animations: update)
|
||
|
|
} else {
|
||
|
|
update()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||
|
|
let location = gesture.location(in: self)
|
||
|
|
let stepWidth = bounds.width / CGFloat(numberOfSteps - 1)
|
||
|
|
var newStep = Int(round(location.x / stepWidth))
|
||
|
|
newStep = max(0, min(numberOfSteps - 1, newStep))
|
||
|
|
|
||
|
|
if selectedStep != newStep {
|
||
|
|
selectedStep = newStep
|
||
|
|
sendActions(for: .valueChanged)
|
||
|
|
}
|
||
|
|
|
||
|
|
if gesture.state == .ended {
|
||
|
|
updateThumbPosition(animated: true)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|