我正在尝试完成一个自定义滑块,这不是一个普通的滑块。它涉及通过在屏幕上拖动圆圈来按内部和外部。圆圈的拖动量缩小了 10 倍。我已经完成了圆圈拖动部分,并且自定义滑块在拖动圆圈时按照我的预期工作。
但是,当我尝试通过拖动自定义滑块本身来反转该过程时,问题就出现了。除了对 CustomSliderView 中的 .onChange(of: PercentageDX) 部分进行逆向工程之外,我已经完成了所有部分。我已经为此苦苦挣扎了几个小时,尝试了大约 50 种不同的试错尝试,但我无法理解它。我需要帮助来完成 CustomSliderView 中旋钮手势缺失的部分。
以下是有关代码工作原理的一些背景信息:我在应用程序中有一个屏幕,可用作测量工具来找出圆圈阻力的百分比。然后我将此百分比发送到 CustomSliderView 并将其缩小 10 倍。此操作会将滑块上的位移量减少 10 倍,使其能够与圆的拖动一起正常工作。您可以尝试拖动圆圈,看看当圆圈向左或向右拖动或位移为零时,滑块处于 100%。当圆圈开始向左或向右移动时,滑块开始向相反方向按压。
现在,我想拖动滑块来反转这个按压过程,使其在圆圈返回到原始位置时逐渐压回 100%。
import SwiftUI
struct ContentView: View {
@State private var lastCircleOffset: CGSize = CGSize()
@State private var currentCircleOffset: CGSize = CGSize()
@State private var circleIsDragging: Bool = Bool()
@State private var dxKnobIsDragging: Bool = Bool()
@State private var percentageDX: CGFloat? = nil
private let maxScreenVisibilitySize: CGSize = CGSize(width: 400.0, height: 300.0)
var body: some View {
ZStack {
ZStack {
Color.clear
Color.white
.frame(width: maxScreenVisibilitySize.width, height: maxScreenVisibilitySize.height)
Circle()
.fill(.red)
.frame(width: 50.0, height: 50.0)
.offset(x: currentCircleOffset.width, y: currentCircleOffset.height)
.gesture(circleGesture)
if let unwrappedPercentageDX: CGFloat = percentageDX {
Text("percentageDX: \(String(format: "%.2f", unwrappedPercentageDX*100.0))%").bold().monospaced().foregroundStyle(.black).offset(y: 50)
}
}
.clipped()
.onChange(of: currentCircleOffset) { (oldValue, newValue) in
if (!dxKnobIsDragging) && (oldValue != newValue) {
percentageDX = (newValue.width/(maxScreenVisibilitySize.width/2.0))
}
}
.onChange(of: percentageDX) { (oldValue, newValue) in
if (!circleIsDragging) && (dxKnobIsDragging) {
if let unwrappedOldValue: CGFloat = oldValue, let unwrappedNewValue: CGFloat = newValue, (unwrappedOldValue != unwrappedNewValue) {
// I may made mistake here!
currentCircleOffset.width -= (unwrappedNewValue*maxScreenVisibilitySize.width)/2.0
}
}
}
VStack {
Spacer()
CustomSliderView(percentageDX: $percentageDX, dxKnobIsDragging: $dxKnobIsDragging)
}
}
.padding()
}
private var circleGesture: some Gesture {
return DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local)
.onChanged() { value in
circleIsDragging = true
currentCircleOffset = CGSize(width: value.translation.width + lastCircleOffset.width,
height: value.translation.height + lastCircleOffset.height)
}
.onEnded() { value in
lastCircleOffset = currentCircleOffset
circleIsDragging = false
}
}
}
struct CGSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize { get { return CGSize() } }
static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
}
struct CustomSliderView: View {
@Binding var percentageDX: CGFloat?
@Binding var dxKnobIsDragging: Bool
@State private var lastKnobOffset: CGFloat = CGFloat()
@State private var currentKnobOffset: CGFloat = CGFloat()
@State private var currentOffsetPercentage: CGFloat = CGFloat()
@State private var maxWidthSize: CGFloat = CGFloat()
@State private var knobWidth: CGFloat = CGFloat()
private let knobHeight: CGFloat = 25.0
var body: some View {
GeometryReader { proxy in
ZStack {
Color.gray
Color.white.opacity(knobWidth.isZero ? .zero : 0.85)
.frame(width: knobWidth)
.shadow(radius: 10)
.offset(x: currentKnobOffset)
.gesture(knobGesture)
}
.preference(key: CGSizePreferenceKey.self, value: proxy.size)
}
.frame(height: knobHeight)
.onPreferenceChange(CGSizePreferenceKey.self) { newValue in
maxWidthSize = newValue.width
if percentageDX == nil {
knobWidth = newValue.width
}
}
.onChange(of: percentageDX) { (oldValue, newValue) in
// This part is okay; it works as expected.
if (!dxKnobIsDragging) {
if let unwrappedOldValue: CGFloat = oldValue, let unwrappedNewValue: CGFloat = newValue, (unwrappedOldValue != unwrappedNewValue) {
if let unwrappedNewValue: CGFloat = newValue {
let newPercentageDX: CGFloat = abs(unwrappedNewValue)
let knobScale: CGFloat = 10.0
knobWidth = (maxWidthSize - (newPercentageDX*maxWidthSize)/knobScale)
if (unwrappedNewValue >= .zero) {
currentKnobOffset = -(newPercentageDX*maxWidthSize)/(2.0*knobScale)
lastKnobOffset = currentKnobOffset
}
else {
currentKnobOffset = (newPercentageDX*maxWidthSize)/(2.0*knobScale)
lastKnobOffset = currentKnobOffset
}
}
}
}
}
}
private var knobGesture: some Gesture {
return DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local)
.onChanged() { value in
dxKnobIsDragging = true
// I need to finish this part with the help of reverse coding on .onChange(of: percentageDX)
}
.onEnded() { value in
lastKnobOffset = currentKnobOffset
dxKnobIsDragging = false
}
}
}
感谢您在评论中提供额外信息和答案。
据我从您的回答中了解到,滑块不需要改变宽度。它可以简单地占据整个可用宽度,然后按照百分比值进行偏移。
所以,我认为可以用更简单的方式来解决,用更少的状态变量和更少的逻辑。
PreferenceKey
也不需要。
GestureState
变量用于记录基于当前拖动运动的百分比变化。使用
GestureState
的优点是,当手势结束时,它会自动重置回初始值。
这是您的示例的更新版本。我希望它能按照您想要的方式工作。
struct ContentView: View {
@State private var percentageDX: CGFloat = 0
@GestureState private var dragPercent: CGFloat = 0
private let maxScreenVisibilitySize: CGSize = CGSize(width: 400.0, height: 200.0)
var body: some View {
VStack(spacing: 40) {
Spacer()
ZStack {
Color.white
Circle()
.fill(.red)
.frame(width: 50.0, height: 50.0)
.offset(x: xOffsetCircle)
.gesture(circleGesture)
}
.frame(maxWidth: maxScreenVisibilitySize.width, maxHeight: maxScreenVisibilitySize.height)
Text(String(format: "%.0f%%", (percentageDX + dragPercent) * 100))
.font(.title2)
CustomSliderView(percentageDX: $percentageDX, dragPercent: $dragPercent)
Spacer()
}
}
private var xOffsetCircle: CGFloat {
(percentageDX + dragPercent) * (maxScreenVisibilitySize.width / 2.0)
}
private var circleGesture: some Gesture {
DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local)
.updating($dragPercent) { value, state, trans in
state = value.translation.width / (maxScreenVisibilitySize.width / 2.0)
}
.onEnded { value in
let dPercent = value.translation.width / (maxScreenVisibilitySize.width / 2.0)
percentageDX += dPercent
}
}
}
struct CustomSliderView: View {
@Binding var percentageDX: CGFloat
var dragPercent: GestureState<CGFloat>
private let scalingFactor: CGFloat = 10.0
private let sliderHeight: CGFloat = 25.0
var body: some View {
GeometryReader { proxy in
ZStack {
Color.gray
Color.white
.shadow(radius: 10)
.offset(x: xOffsetSlider(maxWidthSize: proxy.size.width))
.gesture(
DragGesture(minimumDistance: CGFloat.zero, coordinateSpace: .local)
.updating(dragPercent) { value, state, trans in
state = -(value.translation.width / proxy.size.width) * 10
}
.onEnded { value in
let dPercent = -(value.translation.width / proxy.size.width) * 10
percentageDX += dPercent
}
)
}
}
.frame(height: sliderHeight)
}
private func xOffsetSlider(maxWidthSize: CGFloat) -> CGFloat {
-((percentageDX + dragPercent.wrappedValue) / scalingFactor ) * maxWidthSize
}
}