如何使用swiftUI绘制进度条
这个标题名起完之后,我就觉得小了,格局小了!因为在本篇文章的内容里,不仅仅有一个绘图的功能,还会穿插一下比较实用的开发tips。
本文代码来自于我个人研发的满满财表,感兴趣的小伙伴可以去下载一个体验一下,针对于个人的财务管理工具。
最终效果
拆解问题
这是我们惯用的解题策略,一旦遇到一个相对复杂的问题时,算法题也好,数学题也好,程序开发也好,首先要想到的一定是化繁为简。
如果一个复杂问题,不能被拆解成为若干个或者是单个重复的简单问题,那么,我们的思路一定是错的。
上面这句话并非毫无根据的主观臆断,而是遵循于傅立叶变换的思维方法。
因此第一步,我们先将最终效果的样式拆分一下,首先是环形进度条,与之前的风格一样,不废话直接上代码:
struct XXRingShape: Shape {
var progress: Double = 0.0
var size: CGSize
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: size.width/2, y: size.height/2)
let radius: CGFloat = (size.width - XXCircleProgressBar.spacing)/2
path.addArc(center: center,
radius: radius,
startAngle: .degrees(-90),
endAngle: .degrees(-90 + (360 * progress)),
clockwise: false)
return path
}
}
其中的Shape可以理解为是Core Animation
框架中的CAShapeLayer,如果不清楚CAShapeLayer是什么的,也没有问题,CAShapeLayer就像是一个画家,可以用来绘制和渲染2D形状。它可以创建复杂的形状,如圆形、矩形和曲线,并提供了许多属性来控制形状的外观和行为。它还可以与动画一起使用,以创建流畅的过渡效果,要结合path使用。
path的每个参数都十分简单,因此就不过多介绍了,这里需要注意的是animatableData这个属性。
Shape需要使用animatableData才能执行动画是因为Shape是基于路径绘制的,而animatableData可以帮助我们控制路径的变化,从而实现Shape的动画效果。
在往上一层,在使用XXRingShape的时候,需要明确给它一个size,那么如何给它一个明确的尺寸呢?
使用GeometryReader
之前挖了一个坑,在说到List上拉下拉的时候,我们提过,想要知道父视图的矩形框,swiftUI为我们提供了GeometryReader
用来解决此类问题。
那么,应该如何使用呢?上代码:
struct XXCircleProgressBar: View {
var progress: Double
var color: Color
@State var scaleValue: Double = 0
static let spacing: Double = 8
var lineWidth: Double
init(progress: Double, color: Color, lineWidth: Double) {
self.progress = progress
self.color = color
self.lineWidth = lineWidth
}
var body: some View {
GeometryReader { geometry in
ZStack() {
XXRingShape(progress: 1, size: geometry.size)
.stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(color.opacity(0.2))
XXRingShape(progress: scaleValue, size: geometry.size)
.stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(color)
.animation(.easeOut(duration: 1), value: scaleValue)
.onAppear {
scaleValue = progress
}
.onDisappear {
scaleValue = 0
}
.frame(width: geometry.size.width, height: geometry.size.width)
}
}
}
}
GeometryReader
就像是一个测量工具,用来测量你的swiftUI父视图的大小和位置,帮助你更好地理解和布局你的应用程序。其中的geometry可以理解为是你的父视图,通过对他的大小计算,从而确定包内视图的大小及位置。
一旦我们需要精确布局,实现一些自定义的动画效果,上拉下拉等效果,就离不开使用GeometryReader
。
在第一篇 《swiftUI总结》 中我们提到了属性修饰器-- @propertyWrapper,也简单地讲述了conbime
是什么。而@State
就是swiftUI提供的,用来观察属性变化的属性修饰器。swiftUI中的@State
修饰符用于在视图中声明和管理状态。与其他修饰符不同,@State
是用于管理视图内部状态的修饰符。可以使用@State
来声明变量,并且可以在视图中使用它们。这些变量的值可以更改,并且当值更改时,视图将自动重新绘制。
完整代码
没错,从来不拿你们当外人,直接把开头代码的效果代码甩出来:
import SwiftUI
extension Color {
init(hexString: String) {
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt64()
Scanner(string: hex).scanHexInt64(&int)
let r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(r, g, b) = ((int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(r, g, b) = (int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(r, g, b) = (0, 0, 0)
}
self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255)
}
}
public struct XXChartTargetCircleModel {
var outProgress: Double = 0.3
var midProgress: Double = 0.2
var inProgress: Double = 0.5
var progress: Int = 81
let outColor: Color = Color(hexString: "#1F8A70")
let midColor: Color = Color(hexString: "#BFDB38")
let inColor: Color = Color(hexString: "#FC7300")
var target: String = ""
}
struct XXAimCircleView: View {
@Environment(.colorScheme) var colorScheme: ColorScheme
var title: String
var data: XXChartTargetCircleModel
public var formSize:CGSize
@State var progress: Int = 0
init(title: String, data: XXChartTargetCircleModel) {
self.title = title
self.data = data
self.formSize = CGSize(width: 180, height: 180)
}
var body: some View {
ZStack {
Rectangle()
.fill(Color(hexString: "#F5F5F5"))
.cornerRadius(20)
VStack() {
ZStack {
XXCircleProgressBar(progress: data.outProgress, color: data.outColor, lineWidth: 16)
.frame(width: self.formSize.width - 30,
height: self.formSize.width - 30)
XXCircleProgressBar(progress: data.midProgress, color: data.midColor, lineWidth: 12)
.frame(width: self.formSize.width - 66,
height: self.formSize.width - 66)
XXCircleProgressBar(progress: data.inProgress, color: data.inColor, lineWidth: 10)
.frame(width: self.formSize.width - 94,
height: self.formSize.width - 94)
HStack(alignment: .lastTextBaseline) {
Text("(progress)")
.foregroundColor(.black)
.font(.title3) + Text("%")
.foregroundColor(.black)
.font(.subheadline)
}
.onAppear {
Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { timer in
if progress < Int(data.progress) {
progress += 1
} else {
timer.invalidate()
}
}
}
}
.padding([.top])
HStack {
Text("(Image(systemName: "target"))")
Spacer()
Text("(title)·挑战")
.font(.bold(.body)())
.foregroundColor(.black)
}
.padding([.bottom, .leading, .trailing])
}
}.frame(minWidth:self.formSize.width,
maxWidth:self.formSize.width,
minHeight:self.formSize.height,
maxHeight:self.formSize.height)
}
}
struct XXCircleProgressBar: View {
var progress: Double
var color: Color
@State var scaleValue: Double = 0
static let spacing: Double = 8
var lineWidth: Double
init(progress: Double, color: Color, lineWidth: Double) {
self.progress = progress
self.color = color
self.lineWidth = lineWidth
}
var body: some View {
GeometryReader { geometry in
ZStack() {
XXRingShape(progress: 1, size: geometry.size)
.stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(color.opacity(0.2))
XXRingShape(progress: scaleValue, size: geometry.size)
.stroke(style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round))
.foregroundColor(color)
.animation(.easeOut(duration: 1), value: scaleValue)
.onAppear {
scaleValue = progress
}
.onDisappear {
scaleValue = 0
}
.frame(width: geometry.size.width, height: geometry.size.width)
}
}
}
}
struct XXRingShape: Shape {
var progress: Double = 0.0
var size: CGSize
var animatableData: Double {
get { progress }
set { progress = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: size.width/2, y: size.height/2)
let radius: CGFloat = (size.width - XXCircleProgressBar.spacing)/2
path.addArc(center: center,
radius: radius,
startAngle: .degrees(-90),
endAngle: .degrees(-90 + (360 * progress)),
clockwise: false)
return path
}
}
struct XXAimCircleView_Previews: PreviewProvider {
static var previews: some View {
XXAimCircleView(title: "今日", data: XXChartTargetCircleModel())
}
}
结束语
近期chatGPT的爆火,让一个有趣的问题又重新进入人们的视野——AI是否可以代替人类工作?
这个问题看看chatGPT是怎么回答的。
很巧的是,在我问它这个问题之前,我能想到的答案与它的回答大致相同。没错,随着科技的发展,我们的生产工具的确取得日新月异的进步,因此,一些工具威胁论会产生也是十分自然的可能,人类对于自己无法控制的力量总是充满敬畏。
但我相信,斧子不会自己杀人,能杀死人的只有会用斧子的人。
所以,与其担心会不会斧子砍死,不如先学会使用斧子。
最后,感谢所有为了科学进步,为了社会进步,为了人类进步的伟大科学家。Respect!!!
封面是使用Midjourney绘制。再次感谢那些所有推动科技发展的人!