Visual_Novel_iOS/crush/Crush/Src/Components/UI/CustomViews/GestureSlider.swift

137 lines
3.6 KiB
Swift
Raw Normal View History

2025-10-09 10:29:35 +00:00
//
// 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)
}
}
}