Create a Ring Slider in SwiftUI

foreword

A Slider control is a UI control that allows the user to select a value from a range of values. In SwiftUI, it's usually rendered as a thumb selector on a straight line. Sometimes it might be better to present this type of selector as a circle with the thumb moving around the circle. This article describes how to define a circular Slider in SwiftUI.

Initialize the circular profile

ZStackStart with the three rings in it A gray ring represents the slider's path outline, a reddish arc represents progress along the ring, and a circle represents the current cursor or thumb position. Set the slider's range to 0.0 to 1.0, and hardcode a diameter and a current position progress of - 0.33.

struct CircularSliderView1: View {
    let progress = 0.33
    let ringDiameter = 300.0
    
    private var rotationAngle: Angle {
        return Angle(degrees: (360.0 * progress))
    }
    
    var body: some View {
        VStack {
            ZStack {
                Circle()
                    .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
                Circle()
                    .trim(from: 0, to: progress)
                    .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                            style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
                    )
                    .rotationEffect(Angle(degrees: -90))
                Circle()
                    .fill(Color.white)
                    .frame(width: 21, height: 21)
                    .offset(y: -ringDiameter / 2.0)
                    .rotationEffect(rotationAngle)
            }
            .frame(width: ringDiameter, height: ringDiameter)

            Spacer()
        }
        .padding(80)
    }
}

复制代码

Bind the progress value to the thumb position

Change progress variable to state variable and add default Slider. This Slider is used to modify the progress value and implement enough code on the circular slider to make the thumb and progress arc responsive. The current value is displayed in the center of the ring Slider.

struct CircularSliderView2: View {
    @State var progress = 0.33
    let ringDiameter = 300.0
    
    private var rotationAngle: Angle {
        return Angle(degrees: (360.0 * progress))
    }
    
    var body: some View {
        ZStack {
            Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
                .edgesIgnoringSafeArea(.all)
            
            VStack {
                ZStack {
                    Circle()
                        .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
                        .overlay() {
                            Text("\(progress, specifier: "%.1f")")
                                .font(.system(size: 78, weight: .bold, design:.rounded))
                        }
                    Circle()
                        .trim(from: 0, to: progress)
                        .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                                style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
                        )
                        .rotationEffect(Angle(degrees: -90))
                    Circle()
                        .fill(Color.white)
                        .shadow(radius: 3)
                        .frame(width: 21, height: 21)
                        .offset(y: -ringDiameter / 2.0)
                        .rotationEffect(rotationAngle)
                }
                .frame(width: ringDiameter, height: ringDiameter)
                
                
                VStack {
                    Text("Progress: \(progress, specifier: "%.1f")")
                    Slider(value: $progress,
                           in: 0...1,
                           minimumValueLabel: Text("0.0"),
                           maximumValueLabel: Text("1.0")
                    ) {}
                }
                .padding(.vertical, 40)
                
                Spacer()
            }
            .padding(.vertical, 40)
            .padding()
        }
    }
}
复制代码

Add touch gestures

A DragGesture is added to the slider circle and a temporary text view is used to display the current position of the drag gesture. You can see how the x and y coordinates change around the center of the position containing the ring Slider.

struct CircularSliderView3: View {
    @State var progress = 0.33
    let ringDiameter = 300.0
    
    @State var loc = CGPoint(x: 0, y: 0)
    
    private var rotationAngle: Angle {
        return Angle(degrees: (360.0 * progress))
    }
    
    private func changeAngle(location: CGPoint) {
        loc = location
    }
    
    var body: some View {
        ZStack {
            Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
                .edgesIgnoringSafeArea(.all)
            
            VStack {
                ZStack {
                    Circle()
                        .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
                        .overlay() {
                            Text("\(progress, specifier: "%.1f")")
                                .font(.system(size: 78, weight: .bold, design:.rounded))
                        }
                    Circle()
                        .trim(from: 0, to: progress)
                        .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                                style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
                        )
                        .rotationEffect(Angle(degrees: -90))
                    Circle()
                        .fill(Color.blue)
                        .shadow(radius: 3)
                        .frame(width: 21, height: 21)
                        .offset(y: -ringDiameter / 2.0)
                        .rotationEffect(rotationAngle)
                        .gesture(
                            DragGesture(minimumDistance: 0.0)
                                .onChanged() { value in
                                    changeAngle(location: value.location)
                                }
                        )
                }
                .frame(width: ringDiameter, height: ringDiameter)
                
                Spacer().frame(height:50)
                
                Text("Location = (\(loc.x, specifier: "%.1f"), \(loc.y, specifier: "%.1f"))")
                
                Spacer()
            }
            .padding(.vertical, 40)
            .padding()
        }
    }
}
复制代码

Set slider position for different coordinate values

There are two values ​​on the circular slider that represent progress, a value that shows progress in radians progressand a value that shows the slider cursor rotationAngle. There should be only one property to hold the slider progress. The view is extracted into a separate struct that has a bound value for the progress on the circular slider.

Optional parameters for sliders rangeare also available. This requires some adjustments to the progress to account for the angle that has been set and the rotation of the thumb's position on the circular slider. Also called to calculate the rotation angle onAppearbased on Viewthe progress value before appearing.

struct CircularSliderView: View {
    @Binding var progress: Double

    @State private var rotationAngle = Angle(degrees: 0)
    private var minValue = 0.0
    private var maxValue = 1.0
    
    init(value progress: Binding<Double>, in bounds: ClosedRange<Int> = 0...1) {
        self._progress = progress
        
        self.minValue = Double(bounds.first ?? 0)
        self.maxValue = Double(bounds.last ?? 1)
        self.rotationAngle = Angle(degrees: progressFraction * 360.0)
    }
    
    private var progressFraction: Double {
        return ((progress - minValue) / (maxValue - minValue))
    }
    
    private func changeAngle(location: CGPoint) {
        // 为位置创建一个向量(在 iOS 上反转 y 坐标系统)
        let vector = CGVector(dx: location.x, dy: -location.y)
        
        // 计算向量的角度
        let angleRadians = atan2(vector.dx, vector.dy)
        
        // 将角度转换为 0 到 360 的范围(而不是负角度)
        let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians
        
        // 根据角度更新滑块进度值
        progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue)) + minValue
        rotationAngle = Angle(radians: positiveAngle)
    }
    
    var body: some View {
        GeometryReader { gr in
            let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9
            let sliderWidth = radius * 0.1
            
            VStack(spacing:0) {
                ZStack {
                    Circle()
                        .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9),
                                style: StrokeStyle(lineWidth: sliderWidth))
                        .overlay() {
                            Text("\(progress, specifier: "%.1f")")
                                .font(.system(size: radius * 0.7, weight: .bold, design:.rounded))
                        }
                    // 取消注释以显示刻度线
                    //Circle()
                    //    .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.6),
                    //            style: StrokeStyle(lineWidth: sliderWidth * 0.75,
                    //                               dash: [2, (2 * .pi * radius)/24 - 2]))
                    //    .rotationEffect(Angle(degrees: -90))
                    Circle()
                        .trim(from: 0, to: progressFraction)
                        .stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
                                style: StrokeStyle(lineWidth: sliderWidth, lineCap: .round)
                        )
                        .rotationEffect(Angle(degrees: -90))
                    Circle()
                        .fill(Color.white)
                        .shadow(radius: (sliderWidth * 0.3))
                        .frame(width: sliderWidth, height: sliderWidth)
                        .offset(y: -radius)
                        .rotationEffect(rotationAngle)
                        .gesture(
                            DragGesture(minimumDistance: 0.0)
                                .onChanged() { value in
                                    changeAngle(location: value.location)
                                }
                        )
                }
                .frame(width: radius * 2.0, height: radius * 2.0, alignment: .center)
                .padding(radius * 0.1)
            }
            
            .onAppear {
                self.rotationAngle = Angle(degrees: progressFraction * 360.0)
            }
        }
    }
}

复制代码

CircularSliderViewThree different views of were added to the View to test and demonstrate different features of the Circular Slider view.

struct CircularSliderView5: View {
    @State var progress1 = 0.75
    @State var progress2 = 37.5
    @State var progress3 = 7.5
    
    var body: some View {
        ZStack {
            Color(hue: 0.58, saturation: 0.06, brightness: 1.0)
                .edgesIgnoringSafeArea(.all)

            VStack {
                CircularSliderView(value: $progress1)
                    .frame(width:250, height: 250)
                
                HStack {
                    CircularSliderView(value: $progress2, in: 1...10)

                    CircularSliderView(value: $progress3, in: 0...100)
                }
                
                Spacer()
            }
            .padding()
        }
    }
}
复制代码

Summarize

本文展示了如何定义响应拖动手势的圆环滑块控件。可以设置滑块视图的大小,并且滑块按预期工作。可以向控件添加更多参数以设置颜色或圆环内显示的值的格式。

本文正在参加「金石计划」

Guess you like

Origin juejin.im/post/7215416975605661755