简体   繁体   中英

SwiftUI Circular slider issue not displaying the correct stroke when using two control point

I have a circular view with two control knobs and a stroke between the two of them as you can see from the image.

The values are correct but the problem is how could I do display the blue stroke to be the opposite? The user might want the range to be NW, N to NE, and not NE including S to NE but the user needs the ability to pick from both. The code below can be just be pasted in to show the same as the image.

Swiftui 圆形滑块

import SwiftUI

struct CircularSliderView: View {
var body: some View {
    VStack(){
        DirectionView()
        
        Spacer()
     }
   }
}

struct SwellCircularSliderView_Previews: PreviewProvider {
static var previews: some View {
    CircularSliderView()
 }
    }

    struct DirectionView: View {

    @State var directionValue: CGFloat = 0.0
    @State var secondaryDirectionValue: CGFloat = 0.0

var body: some View {
    VStack {
        Text("\(directionValue, specifier: "%.0f")° - \(secondaryDirectionValue, specifier: "%.0f")°   \(Double().degreesToCompassDirection(degree: Double(directionValue))) - \(Double().degreesToCompassDirection(degree: Double(secondaryDirectionValue)))")
            .font(.body)
        
        
         DirectionControlView(directionValue: $directionValue, secondaryDirectionValue: $secondaryDirectionValue)
            .padding(.top, 60)
        
        Spacer()
    }//: VSTACK
}
}

struct DirectionControlView: View {
@Binding var directionValue: CGFloat
@State var dirAngleValue: CGFloat = 0.0

@Binding var secondaryDirectionValue: CGFloat
@State var secondaryDirAngleValue: CGFloat = 0.0


let minimumValue: CGFloat = 0
let maximumValue: CGFloat = 360.0
let totalValue: CGFloat = 360.0
let knobRadius: CGFloat = 10.0
let radius: CGFloat = 125.0

private let tickHeight: CGFloat = 8
private let longTickHeight: CGFloat = 14
private let tickWidth: CGFloat = 2

func minimumTrimValue() -> CGFloat{
    if directionValue > secondaryDirectionValue {
        return secondaryDirectionValue/totalValue
    } else {
        return directionValue/totalValue
    }
}

func maximumTrimValue() -> CGFloat{
    if  directionValue > secondaryDirectionValue {
        return directionValue/totalValue
    } else {
        return secondaryDirectionValue/totalValue
    }
}

var body: some View {
    ZStack {
        
        Circle()
            .trim(from: minimumTrimValue(), to: maximumTrimValue())
            .stroke(
                AngularGradient(gradient: Gradient(
                                    colors: [Color.blue.opacity(0.2), Color.blue.opacity(1), Color.blue.opacity(0.2)]),
                                center: .center,
                                startAngle: .degrees(Double(secondaryDirectionValue)),
                                endAngle: .degrees(Double(directionValue))),
                style: StrokeStyle(lineWidth: 8, lineCap: .round)
            )
            .frame(width: radius * 2, height: radius * 2)
            .rotationEffect(.degrees(-90))
        
        KnobCircle(radius: knobRadius * 2, padding: 6)
            .offset(y: -radius)
            .rotationEffect(Angle.degrees(Double(dirAngleValue)))
            .shadow(color: Color.black.opacity(0.2), radius: 3, x: -3)
            .gesture(DragGesture(minimumDistance: 0.0)
                        .onChanged({ angleValue in
                            knobChange(location: angleValue.location)
                        }))
        
        KnobCircle(radius: knobRadius * 2, padding: 6)
            .offset(y: -radius)
            .rotationEffect(Angle.degrees(Double(secondaryDirectionValue)))
            .shadow(color: Color.black.opacity(0.2), radius: 3, x: -3)
            .gesture(DragGesture(minimumDistance: 0.0)
                        .onChanged({ angleValue in
                            knobSecondaryChange(location: angleValue.location)
                        }))
        
        CompassView(count: 240,
                    longDivider: 15,
                    longTickHeight: self.longTickHeight,
                    tickHeight: self.tickHeight,
                    tickWidth: self.tickWidth,
                    highlightedColorDivider: 30,
                    highlightedColor: .blue,
                    normalColor: .black.opacity(0.2))
            .frame(width: 350, height: 350)
        
        
        CompassNumber(numbers: self.getNumbers(count: 16))
            .frame(width: 310, height: 310)
        
    }//: ZSTACK
    .onAppear(){
        updateInitialValue()
    }
}

private func getNumbers(count: Int) -> [Float] {
    var numbers: [Float] = []
    numbers.append(Float(count) * 30)
    for index in 1..<count {
        numbers.append(Float(index) * 30)
    }
    return numbers
}

private func updateInitialValue(){
    directionValue = minimumValue
    dirAngleValue = CGFloat(directionValue/totalValue) * 360
}


private func knobChange(location: CGPoint) {
    let vector = CGVector(dx: location.x, dy: location.y)
    let angle = atan2(vector.dy - knobRadius, vector.dx - knobRadius) + .pi/2.0
    let fixedAngle = angle < 0.0 ? angle + 2.0 * .pi : angle
    let value = fixedAngle / (2.0 * .pi) * totalValue
    
    if value > minimumValue && value < maximumValue {
        directionValue = value
        dirAngleValue = fixedAngle * 180 / .pi
    }
    
}

private func knobSecondaryChange(location: CGPoint) {
    let vector = CGVector(dx: location.x, dy: location.y)
    let angle = atan2(vector.dy - knobRadius, vector.dx - knobRadius) + .pi/2.0
    let fixedAngle = angle < 0.0 ? angle + 2.0 * .pi : angle
    let value = fixedAngle / (2.0 * .pi) * totalValue
    
    if value > minimumValue && value < maximumValue {
        secondaryDirectionValue = value
        secondaryDirAngleValue = fixedAngle * 180 / .pi
    }
    
  }
}

struct KnobCircle: View {
let radius: CGFloat
let padding: CGFloat

var body: some View {
    ZStack(){
        Circle()
            .fill(Color.init(white: 0.96))
            .frame(width: radius, height: radius)
            .shadow(color: Color.black.opacity(0.1), radius: 10, x: -10, y: 8)
        
        Circle()
            .fill(Color.white)
            .frame(width: radius - padding, height: radius - padding)
    }//: ZSTACK
  }
}

struct CompassView: View {
let count: Int
let longDivider: Int
let longTickHeight: CGFloat
let tickHeight: CGFloat
let tickWidth: CGFloat

let highlightedColorDivider: Int
let highlightedColor: Color
let normalColor: Color

var body: some View {
    ZStack(){
        ForEach(0..<self.count) { index in
            let height = (index % self.longDivider == 0) ? self.longTickHeight : self.tickHeight
            let color = (index % self.highlightedColorDivider == 0) ? self.highlightedColor : self.normalColor
            let degree: Double = Double.pi * 2 / Double(self.count)
            TickShape(tickHeight: height)
                .stroke(lineWidth: self.tickWidth)
                .rotationEffect(.radians(degree * Double(index)))
                .foregroundColor(color)
            
        }
    }//: ZSTACK
}//: VIEW
}

struct TickShape: Shape {
let tickHeight: CGFloat

func path(in rect: CGRect) -> Path {
    var path = Path()
    path.move(to: CGPoint(x: rect.midX, y: rect.minY))
    path.addLine(to: CGPoint(x: rect.midX, y: rect.minY + self.tickHeight))
    return path
}
}


struct CompassNumber: View {
let numbers: [Float]
let direction: [String] = ["N","NE","E","SE","S","SW","W","NW"]

var body: some View {
    ZStack(){
        ForEach(0..<self.direction.count) { index in
            let degree: Double = Double.pi * 2 / Double(self.direction.count)
            let itemDegree = degree * Double(index)
            VStack(){
                Text(self.direction[index])
                    .font(.footnote)
                    .rotationEffect(.radians(-itemDegree))
                    .foregroundColor(.blue)
                Spacer()
            }//: VSTACK
            .rotationEffect(.radians(itemDegree))
        }
    }//: ZSTACK
}
}


extension Double {

func degreesToCompassDirection(degree: Double) -> String {
    
    switch degree {
    case 0..<11.25:
        return "N"
    case 11.25..<33.75:
        return "NNE"
    case 33.75..<56.25:
        return "NE"
    case 56.25..<78.75:
        return "ENE"
    case 78.75..<101.25:
        return "E"
    case 101.25..<123.75:
        return "ESE"
    case 123.75..<146.25:
        return "SE"
    case 146.25..<168.75:
        return "SSE"
    case 168.75..<191.25:
        return "S"
    case 191.25..<213.75:
        return "SSW"
    case 213.75..<236.25:
        return "SW"
    case 236.25..<258.75:
        return "WSW"
    case 258.75..<281.25:
        return "W"
    case 281.25..<303.75:
        return "WNW"
    case 303.75..<326.25:
        return "NW"
    case 326.25..<348.75:
        return "NNW"
    case 348.75..<360:
        return "N"
    default:
        return "ERROR"
    }
}
}

Thanks for your help.

I did a ZStack and displayed Background Stroke. In Case I want to display over the 0 Position (North) I just swapped Background and foreground colors.

var body: some View {
ZStack {
    
    ZStack{
        Circle() //Background
            .stroke((directionValue < secondaryDirectionValue) ? Color.white : Color.blue)
            .frame(width: radius * 2, height: radius * 2)
            .rotationEffect(.degrees(-90))
        Circle() //Foreground
            .trim(from: minimumTrimValue(), to: maximumTrimValue())
            .stroke((directionValue < secondaryDirectionValue) ? Color.blue : Color.white)
            .frame(width: radius * 2, height: radius * 2)
            .rotationEffect(.degrees(-90))
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM