SwiftUI 使用DragGesture創建顏色選擇旋轉盤
xcode 13.4.1, swift 5.5, iOS 15.4
2022-08-25
SwiftUI提供了很多方便的元件和方法,這次就看看如何有很短的程式代碼實現一個顏色選擇旋轉盤。
1. 首先創建一個有多種顏色的圓餅。
struct WheelPicker : View {
let colors : [Color] = [.red, .orange, .yellow, .green, .indigo, .blue, .purple, .orange, .mint, .teal]
var body: some View {
GeometryReader { proxy in
let radius = min(proxy.size.width/2, proxy.size.height/2)
ZStack {
ForEach(0..<colors.count, id:\.self) { i in
Circle()
.inset(by: (radius)/2)
.trim(from: CGFloat(i) / CGFloat(colors.count), to: CGFloat(i+1) / CGFloat(colors.count))
.stroke(colors[i], lineWidth: radius)
}
}
}
}
}
2. 為了達到撥動圓盤時,可以有轉動的效果,所以需要知道撥動的開始和結束角度,可以使用DragGestrue去取後第一次點按屏中的位置和圓盤的圓心來計算開始角度,然後現在拖曳的位置來計算結束的角度,這兩個角度的差就是圓盤需要轉動的角度,分析完後我們需要兩個函數來幫忙計算,就是當前位置的角度getAngle和拖曳的開始結束角度差deltaDegree。
import SwiftUI
struct WheelPicker : View {
let colors : [Color] = [.red, .orange, .yellow, .green, .indigo, .blue, .purple, .orange, .mint, .teal]
var body: some View {
GeometryReader { proxy in
let radius = min(proxy.size.width/2, proxy.size.height/2)
ZStack {
ForEach(0..<colors.count, id:\.self) { i in
Circle()
.inset(by: (radius)/2)
.trim(from: CGFloat(i) / CGFloat(colors.count), to: CGFloat(i+1) / CGFloat(colors.count))
.stroke(colors[i], lineWidth: radius)
}
}
}
}
func getAngle(center : CGPoint, point : CGPoint) -> CGFloat {
let deltaX = point.x - center.x
let deltaY = point.y - center.y
var theangle = atan(deltaY / deltaX) * 360 / (2 * CGFloat.pi)
if deltaY > 0 && deltaX < 0 {
theangle += 180
} else if deltaY < 0 && deltaX < 0 {
theangle += 180
} else if deltaY == 0 && deltaX < 0 {
theangle = 180
} else if deltaY == 0 && deltaX > 0 {
theangle = 0
} else if deltaY < 0 && deltaX > 0 {
theangle += 360
} else if deltaY < 0 && deltaX == 0 {
theangle = 270
}
return theangle
}
func deltaDegree(center : CGPoint, startPoint : CGPoint, endPoint : CGPoint) -> CGFloat {
let startDegree = getAngle(center: center, point: startPoint)
let endDegree = getAngle(center: center, point: endPoint)
return (endDegree - startDegree)
}
}
3. 然後使用DragGesture來取後拖曳的開始和結束位置後,並使用以上的兩個函數來計算拖曳的角度差,currentDegree代表上一次圓盤停留的角度,degree代表當前圓盤被撥動的角度,兩個加起來就是圓盤應停的的角度,實作如下:
import SwiftUI
struct WheelPicker : View {
let colors : [Color] = [.red, .orange, .yellow, .green, .indigo, .blue, .purple, .orange, .mint, .teal]
@State private var currentDegree = Double.zero
@State private var degree = Double.zero
var body: some View {
GeometryReader { proxy in
let center = CGPoint(x: proxy.size.width/2, y: proxy.size.height/2)
let radius = min(proxy.size.width/2, proxy.size.height/2)
let dragGesture = DragGesture()
.onChanged({ v in
degree = deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
})
.onEnded({ v in
currentDegree += deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
degree = .zero
})
ZStack {
ForEach(0..<colors.count, id:\.self) { i in
Circle()
.inset(by: (radius)/2)
.trim(from: CGFloat(i) / CGFloat(colors.count), to: CGFloat(i+1) / CGFloat(colors.count))
.stroke(colors[i], lineWidth: radius)
}
}
.rotationEffect(.degrees(currentDegree + degree))
.gesture(dragGesture)
}
}
func getAngle(center : CGPoint, point : CGPoint) -> CGFloat {
let deltaX = point.x - center.x
let deltaY = point.y - center.y
var theangle = atan(deltaY / deltaX) * 360 / (2 * CGFloat.pi)
if deltaY > 0 && deltaX < 0 {
theangle += 180
} else if deltaY < 0 && deltaX < 0 {
theangle += 180
} else if deltaY == 0 && deltaX < 0 {
theangle = 180
} else if deltaY == 0 && deltaX > 0 {
theangle = 0
} else if deltaY < 0 && deltaX > 0 {
theangle += 360
} else if deltaY < 0 && deltaX == 0 {
theangle = 270
}
return theangle
}
func deltaDegree(center : CGPoint, startPoint : CGPoint, endPoint : CGPoint) -> CGFloat {
let startDegree = getAngle(center: center, point: startPoint)
let endDegree = getAngle(center: center, point: endPoint)
return (endDegree - startDegree)
}
}
4. 在圓盤的右邊加一個小三角形作為指示當前圓盤選擇的位置,將指示三角形放在圓盤0度的位置。
import SwiftUI
struct WheelPicker : View {
let colors : [Color] = [.red, .orange, .yellow, .green, .indigo, .blue, .purple, .orange, .mint, .teal]
@State private var currentDegree = Double.zero
@State private var degree = Double.zero
var body: some View {
GeometryReader { proxy in
let center = CGPoint(x: proxy.size.width/2, y: proxy.size.height/2)
let radius = min(proxy.size.width/2, proxy.size.height/2)
let dragGesture = DragGesture()
.onChanged({ v in
degree = deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
})
.onEnded({ v in
currentDegree += deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
degree = .zero
})
ZStack {
ForEach(0..<colors.count, id:\.self) { i in
Circle()
.inset(by: (radius)/2)
.trim(from: CGFloat(i) / CGFloat(colors.count), to: CGFloat(i+1) / CGFloat(colors.count))
.stroke(colors[i], lineWidth: radius)
}
Image(systemName: "arrowtriangle.left.fill")
.offset(x: radius, y: 0)
}
.rotationEffect(.degrees(currentDegree + degree))
.gesture(dragGesture)
}
}
func getAngle(center : CGPoint, point : CGPoint) -> CGFloat {
let deltaX = point.x - center.x
let deltaY = point.y - center.y
var theangle = atan(deltaY / deltaX) * 360 / (2 * CGFloat.pi)
if deltaY > 0 && deltaX < 0 {
theangle += 180
} else if deltaY < 0 && deltaX < 0 {
theangle += 180
} else if deltaY == 0 && deltaX < 0 {
theangle = 180
} else if deltaY == 0 && deltaX > 0 {
theangle = 0
} else if deltaY < 0 && deltaX > 0 {
theangle += 360
} else if deltaY < 0 && deltaX == 0 {
theangle = 270
}
return theangle
}
func deltaDegree(center : CGPoint, startPoint : CGPoint, endPoint : CGPoint) -> CGFloat {
let startDegree = getAngle(center: center, point: startPoint)
let endDegree = getAngle(center: center, point: endPoint)
return (endDegree - startDegree)
}
}
5. 現在只要知道圓盤0度位置的顏色,就可以達到選擇圓盤選擇的效果,以下函數就是用來計算當前圓盤在0度位置的顏色 getColor,然後計算的顏色作為背景色,實作如下。
import SwiftUI
struct WheelPicker : View {
let colors : [Color] = [.red, .orange, .yellow, .green, .indigo, .blue, .purple, .orange, .mint, .teal]
@State private var currentDegree = Double.zero
@State private var degree = Double.zero
@State private var selectedColor : Color = .red
var body: some View {
GeometryReader { proxy in
let center = CGPoint(x: proxy.size.width/2, y: proxy.size.height/2)
let radius = min(proxy.size.width/2, proxy.size.height/2)
let dragGesture = DragGesture()
.onChanged({ v in
degree = deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
selectedColor = getColor(degree: currentDegree + degree)
})
.onEnded({ v in
currentDegree += deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
degree = .zero
selectedColor = getColor(degree: currentDegree + degree)
})
ZStack {
selectedColor.opacity(0.5)
ZStack {
ForEach(0..<colors.count, id:\.self) { i in
Circle()
.inset(by: (radius)/2)
.trim(from: CGFloat(i) / CGFloat(colors.count), to: CGFloat(i+1) / CGFloat(colors.count))
.stroke(colors[i], lineWidth: radius)
}
}
.rotationEffect(.degrees(currentDegree + degree))
.gesture(dragGesture)
Image(systemName: "arrowtriangle.left.fill")
.offset(x: radius, y: 0)
}
}
}
func getAngle(center : CGPoint, point : CGPoint) -> CGFloat {
let deltaX = point.x - center.x
let deltaY = point.y - center.y
var theangle = atan(deltaY / deltaX) * 360 / (2 * CGFloat.pi)
if deltaY > 0 && deltaX < 0 {
theangle += 180
} else if deltaY < 0 && deltaX < 0 {
theangle += 180
} else if deltaY == 0 && deltaX < 0 {
theangle = 180
} else if deltaY == 0 && deltaX > 0 {
theangle = 0
} else if deltaY < 0 && deltaX > 0 {
theangle += 360
} else if deltaY < 0 && deltaX == 0 {
theangle = 270
}
return theangle
}
func deltaDegree(center : CGPoint, startPoint : CGPoint, endPoint : CGPoint) -> CGFloat {
let startDegree = getAngle(center: center, point: startPoint)
let endDegree = getAngle(center: center, point: endPoint)
return (endDegree - startDegree)
}
func getColor(degree : CGFloat) -> Color {
let anglePerColor = 360.0 / CGFloat(colors.count)
let fixDegree = (degree+360).truncatingRemainder(dividingBy: 360.0)
return colors[colors.count-Int(fixDegree/anglePerColor)-1]
}
}
5. 將創建顏色選擇旋轉盤打包成元件,方便在其他App上使用。
import SwiftUI
struct WheelPicker : View {
let colors : [Color] = [.red, .orange, .yellow, .green, .indigo, .blue, .purple, .orange, .mint, .teal]
@State private var currentDegree = Double.zero
@State private var degree = Double.zero
@Binding var selectedColor : Color
var body: some View {
GeometryReader { proxy in
let center = CGPoint(x: proxy.size.width/2, y: proxy.size.height/2)
let radius = min(proxy.size.width/2, proxy.size.height/2)
let dragGesture = DragGesture()
.onChanged({ v in
degree = deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
selectedColor = getColor(degree: currentDegree + degree)
})
.onEnded({ v in
currentDegree += deltaDegree(center: center, startPoint: v.startLocation, endPoint: v.location)
degree = .zero
selectedColor = getColor(degree: currentDegree + degree)
})
ZStack {
ZStack {
ForEach(0..<colors.count, id:\.self) { i in
Circle()
.inset(by: (radius)/2)
.trim(from: CGFloat(i) / CGFloat(colors.count), to: CGFloat(i+1) / CGFloat(colors.count))
.stroke(colors[i], lineWidth: radius)
}
}
.rotationEffect(.degrees(currentDegree + degree))
.gesture(dragGesture)
Image(systemName: "arrowtriangle.left.fill")
.offset(x: radius, y: 0)
}
}
}
func getAngle(center : CGPoint, point : CGPoint) -> CGFloat {
let deltaX = point.x - center.x
let deltaY = point.y - center.y
var theangle = atan(deltaY / deltaX) * 360 / (2 * CGFloat.pi)
if deltaY > 0 && deltaX < 0 {
theangle += 180
} else if deltaY < 0 && deltaX < 0 {
theangle += 180
} else if deltaY == 0 && deltaX < 0 {
theangle = 180
} else if deltaY == 0 && deltaX > 0 {
theangle = 0
} else if deltaY < 0 && deltaX > 0 {
theangle += 360
} else if deltaY < 0 && deltaX == 0 {
theangle = 270
}
return theangle
}
func deltaDegree(center : CGPoint, startPoint : CGPoint, endPoint : CGPoint) -> CGFloat {
let startDegree = getAngle(center: center, point: startPoint)
let endDegree = getAngle(center: center, point: endPoint)
return (endDegree - startDegree)
}
func getColor(degree : CGFloat) -> Color {
let anglePerColor = 360.0 / CGFloat(colors.count)
let fixDegree = (degree+360).truncatingRemainder(dividingBy: 360.0)
return colors[colors.count-Int(fixDegree/anglePerColor)-1]
}
}
struct WheelPickerExample : View {
@State private var selectedColor : Color = .red
var body: some View {
ZStack {
selectedColor.opacity(0.5)
WheelPicker(selectedColor: $selectedColor)
}
}
}
5. 使用創建顏色選擇旋轉盤元件來改變星星的顏色。
struct WheelPickerExample : View {
@State private var selectedColor : Color = .red
var body: some View {
ZStack {
selectedColor.opacity(0.2)
VStack {
WheelPicker(selectedColor: $selectedColor)
.rotationEffect(.degrees(90))
Image(systemName: "star.fill")
.resizable()
.scaledToFit()
.foregroundColor(selectedColor)
.padding()
}
}
}
}