本文展示了如何定義響應(yīng)拖動(dòng)手勢(shì)的圓環(huán)滑塊控件。可以設(shè)置滑塊視圖的大小,并且滑塊按預(yù)期工作??梢韵蚩丶砑痈鄥?shù)以設(shè)置顏色或圓環(huán)內(nèi)顯示的值的格式。 GitHub 上提供了 Circular Slider 的代碼。
 
前言
Slider 控件是一種允許用戶從一系列值中選擇一個(gè)值的 UI 控件。在 SwiftUI 中,它通常呈現(xiàn)為直線上的拇指選擇器。有時(shí)將這種類型的選擇器呈現(xiàn)為一個(gè)圓圈,拇指繞著圓周移動(dòng)可能會(huì)更好。本文介紹如何在 SwiftUI 中定義一個(gè)環(huán)形的 Slider。
初始化環(huán)形輪廓
從ZStack中的三個(gè)圓環(huán)開始。一個(gè)灰色的圓環(huán)代表滑塊的路徑輪廓,一個(gè)淡紅色的圓弧代表沿著圓環(huán)的進(jìn)度,一個(gè)圓圈代表當(dāng)前光標(biāo)或拇指的位置。將滑塊的范圍設(shè)置為0.0到1.0,并硬編碼一個(gè)直徑和一個(gè)的當(dāng)前位置進(jìn)度 - 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)
    }
}
將進(jìn)度值和拇指位置綁定
將進(jìn)度變量更改為狀態(tài)[1]變量并添加默認(rèn) Slider。這個(gè) Slider 用于修改進(jìn)度值,并在圓形滑塊上實(shí)現(xiàn)足夠的代碼以使拇指和進(jìn)度弧響應(yīng)。當(dāng)前值顯示在環(huán)形 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()
        }
    }
}
添加觸摸手勢(shì)
DragGesture[2] 被添加到滑塊圓圈,并且使用臨時(shí)文本視圖顯示拖動(dòng)手勢(shì)的當(dāng)前位置??梢钥吹?x 和 y 坐標(biāo)圍繞包含環(huán)形  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()
        }
    }
}
為不同的坐標(biāo)值設(shè)置滑塊位置
圓形滑塊上有兩個(gè)表示進(jìn)度的值,用于顯示進(jìn)度弧度的progress值和用于顯示滑塊光標(biāo)的rotationAngle。應(yīng)該只有一個(gè)屬性來保存滑塊進(jìn)度。視圖被提取到一個(gè)單獨(dú)的結(jié)構(gòu)中,該結(jié)構(gòu)具有圓形滑塊上進(jìn)度的一個(gè)綁定值。
滑塊的range的可選參數(shù)也是可用的。這需要對(duì)進(jìn)度進(jìn)行一些調(diào)整,以計(jì)算已設(shè)置的角度以及拇指在圓形滑塊上位置的旋轉(zhuǎn)角度。另外調(diào)用onAppear根據(jù)View出現(xiàn)前的進(jìn)度值計(jì)算旋轉(zhuǎn)角度。
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) {
        // 為位置創(chuàng)建一個(gè)向量(在 iOS 上反轉(zhuǎn) y 坐標(biāo)系統(tǒng))
        let vector = CGVector(dx: location.x, dy: -location.y)
        
        // 計(jì)算向量的角度
        let angleRadians = atan2(vector.dx, vector.dy)
        
        // 將角度轉(zhuǎn)換為 0 到 360 的范圍(而不是負(fù)角度)
        let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians
        
        // 根據(jù)角度更新滑塊進(jìn)度值
        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)
            }
        }
    }
}CircularSliderView 的三種不同視圖被添加到View中以測(cè)試和演示 Circular Slider 視圖的不同功能。
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()
        }
    }
}
總結(jié)
本文展示了如何定義響應(yīng)拖動(dòng)手勢(shì)的圓環(huán)滑塊控件??梢栽O(shè)置滑塊視圖的大小,并且滑塊按預(yù)期工作??梢韵蚩丶砑痈鄥?shù)以設(shè)置顏色或圓環(huán)內(nèi)顯示的值的格式。 GitHub 上提供了 Circular Slider[3] 的代碼。
參考資料
[1]state: https://developer.apple.com/documentation/swiftui/state?。
[2]DragGesture: https://developer.apple.com/documentation/swiftui/draggesture/?。
[3]Circular Slider: https://github.com/SwiftCommunityRes/swift?。