Tutorial Minimalista SwiftUI 32: Crie uma barra de progresso circular usando Forma e Animação

Neste capítulo, você aprenderá a criar uma barra de progresso circular usando Shape shapes e Animationanimações.

1.png

Se você possui um AppleWatch, definitivamente notará AppleWatcho anel de exercícios na parte superior, que registra seu exercício diário em três aspectos: caminhar, respirar e subir escadas.

AppleWatchO usuário é informado sobre o andamento do exercício por meio de uma barra circular de progresso, quando atingir o alvo, os AppleWatchtrês anéis circulares se fecharão.

Portanto, neste capítulo, tentaremos criar uma barra de progresso circular semelhante.

criação do projeto

Primeiro, crie um novo projeto chamado SwiftUIProgress .

2.png

Vamos analisar a estrutura primeiro.

Vamos primeiro olhar para um círculo, descobrimos que o círculo de registros de aptidão é feito de 2 círculos empilhados, podemos criar 2 círculos e depois usar a ZStacksuperposição juntos.

Antes disso, como são muitas as cores usadas dessa vez, podemos extrair o grupo de cores e referenciá-lo diretamente na view, o que pode tornar o código mais simplificado.

grupo de cores

Criamos um novo Swiftarquivo e o nomeamos ColorExt.swift.

import SwiftUI

extension Color {
    public init(red: Int, green: Int, blue: Int, opacity: Double = 1.0) {
        let redValue = Double(red) / 255.0
        let greenValue = Double(green) / 255.0
        let blueValue = Double(blue) / 255.0

        self.init(red: redValue, green: greenValue, blue: blueValue, opacity: opacity)
    }
    public static let gradientPink = Color(red: 210, green: 153, blue: 194)
    public static let gradientYellow = Color(red: 254, green: 249, blue: 215)
}
复制代码

3.png

Criamos um initmétodo que aceita valores redde vermelho, greenverde e blueazul. Em seguida, inicializamos a Colorinstância.

Aqui definimos duas cores, uma é um rosa mais bonito gradientPink, a outra é um amarelo mais bonito gradientYellow, usaremos essas duas cores como gradiente de cor do anel posteriormente.

Estilo Básico - Anel Externo

Vamos voltar para ContentViewa página inicial e primeiro criar o fundo do anel na parte de trás.

import SwiftUI

struct ContentView: View {

    var thickness: CGFloat = 30.0
    var width: CGFloat = 250.0

    var body: some View {

        ZStack {
            Circle()
                .stroke(Color(.systemGray6),lineWidth: thickness)
        }
        .frame(width: width, height: width, alignment: .center)
    }
}
复制代码

4.png

Primeiro definimos a Circleespessura thicknessdo anel e a largura do anel width, depois usamos strokeo traço para traçar o anel e adicionar um systemGray6cinza claro à borda.

然后设置圆环的大小。这样我们就获得了第一个圆环:背景圆环。

基础样式-内环

接下来,我们来完成内环,这是一个跟随进度变动的圆环,我们就不能直接用Circle圆形绘制。

还记得之前的章节,我们使用Shape形状绘制各种各样的图形么,这里我们也使用Shape形状的方法绘制内环。

我们创建一个新的结构体,命名为RingShape

//内环
struct RingShape: Shape {

    var progress: Double = 0.0
    var thickness: CGFloat = 30.0
    var startAngle: Double = -90.0

    func path(in rect: CGRect) -> Path {

        var path = Path()

        path.addArc(center: CGPoint(x: rect.width / 2.0, y: rect.height / 2.0), radius: min(rect.width, rect.height) / 2.0,startAngle: .degrees(startAngle),endAngle: .degrees(360 * progress+startAngle), clockwise: false)
        
        return path.strokedPath(.init(lineWidth: thickness, lineCap: .round))
    }
}
复制代码

我们创建了RingShape结构体,它遵循Shape协议。然后我们和外环一样,定义了它的厚度thickness,另外还有内环的进度百分比progress参数。

startAngle开始角度为-90,这是因为我们圆放在坐标轴上,它的开始点是圆右边中间的位置,而我们进度的圆环是从圆的顶部的顶点开始,所以startAngle开始角度需要设置为-90度。

内环的绘制我们使用addArc的方法,起始角度为0,结束角度用360度乘以progress进度的值,而且要加上startAngle开始角度来计算。

最后返回画好的圆,我们可以在ContentView中引用它看看效果。

ZStack {

    //外环
    Circle()
        .stroke(Color(.systemGray6), lineWidth: thickness)

    //内环
    RingShape(progress: 0.3, thickness: thickness)
}
.frame(width: width, height: width, alignment: .center)
复制代码

5.png

基础样式-渐变色

接下来,我们给我们的进度圆环附上渐变色Gradient

科普一个知识点。

AngularGradient角梯度,AngularGradient角梯度是SwiftUI提供的一种绘制渐变色的方法,可以跟随不同角度变化,从起点到终点,颜色按顺时针做扇形渐变

AngularGradient(gradient: Gradient(colors: [.gradientPink, .gradientYellow]), center: .center, startAngle: .degrees(startAngle), endAngle: .degrees(360 * 0.3 + startAngle))
复制代码

这里,我们在AngularGradient角梯度的框架里,指定渲染颜色为渐变色,引用我们定义好的gradientPink粉色和gradientYellow黄色。

渲染梯度开始角度为startAngle定义的开始角度,结束角度为360 * 0.3 + startAngle,从开始角度开始。

然后我们使用.fil修饰符,将AngularGradient角梯度赋予RingShape内环。

6.png

非常不错!

动画效果

接下来,我们来实现下动画效果。

设置3个进度来展示进度:0%、50%、100%,当我们的进度从0%50%时,我们可以看到进度条内环从0~50度的全过程。

我们先定义好初始的进度,替换我们固定的进度值:

@State var progress = 0.0
复制代码

然后,我们完成下进度值的选择。

//进度调节
HStack {
    Group {

        Text("0%")
            .font(.system(.headline, design: .rounded))
            .onTapGesture {
                self.progress = 0.0
            }

        Text("50%")
            .font(.system(.headline, design: .rounded))
            .onTapGesture {
                self.progress = 0.5
            }

        Text("100%")
            .font(.system(.headline, design: .rounded))
            .onTapGesture {
                self.progress = 1.0
            }
    }
    .padding()
    .background(Color(.systemGray6)).clipShape(RoundedRectangle(cornerRadius: 15.0, style: .continuous))
    .padding()
}
.padding()
复制代码

我们设置了3个进度值调节,当我们点击0%的进度值时,进度progress赋值0,同理,我们完成了3个进度值选择。

我们将整个进度调节和内外环视图使用VStack垂直排布。

同时,我们在内外环的组合视图中增加Animation动画。

.animation(Animation.easeInOut(duration: 1.0),value: progress)
复制代码

7.png

嗯?好像出了点问题,我们发现内环的Animation动画好像不起效果,进度加载仍旧很生硬

这是因为我们在实现RingShape内环构建的过程中,它符合Shape协议,而恰巧是Shape协议它有一个默认的动画,也就是没有数据的动画。

因此在ContentView中,我们怎么加Animation动画效果都没有作用。

要解决这个问题也很简单,我们只需要在构建RingShape内环时,赋予progress新的值就可以了。

var animatableData: Double {

    get { progress }
    set { progress = newValue }

}
复制代码

8.png

这样,我们就实现了内环进度的Animation动画效果。

恭喜你,完成了本章的所有练习~

完整代码

import SwiftUI

struct ContentView: View {

    var thickness: CGFloat = 30.0
    var width: CGFloat = 250.0
    var startAngle = -90.0
    @State var progress = 0.0

    var body: some View {
        VStack {
            ZStack {

                // 外环
                Circle()
                    .stroke(Color(.systemGray6), lineWidth: thickness)

                // 内环
                RingShape(progress: progress, thickness: thickness)
                    .fill(AngularGradient(gradient: Gradient(colors: [.gradientPink, .gradientYellow]), center: .center, startAngle: .degrees(startAngle), endAngle: .degrees(360*0.3 + startAngle)))
            }
            .frame(width: width, height: width, alignment: .center)
            .animation(Animation.easeInOut(duration: 1.0),value: progress)

            //进度调节
            HStack {
                Group {

                    Text("0%")
                        .font(.system(.headline, design: .rounded))
                        .onTapGesture {
                            self.progress = 0.0
                        }

                    Text("50%")
                        .font(.system(.headline, design: .rounded))
                        .onTapGesture {
                            self.progress = 0.5
                        }

                    Text("100%")
                        .font(.system(.headline, design: .rounded))
                        .onTapGesture {
                            self.progress = 1.0
                        }
                }
                .padding()
                .background(Color(.systemGray6)).clipShape(RoundedRectangle(cornerRadius: 15.0, style: .continuous))
                .padding()
            }
            .padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

// 内环
struct RingShape: Shape {

    var progress: Double = 0.0
    var thickness: CGFloat = 30.0
    var startAngle: Double = -90.0

    var animatableData: Double {
        get { progress }
        set { progress = newValue }
    }

    func path(in rect: CGRect) -> Path {

        var path = Path()

        path.addArc(center: CGPoint(x: rect.width / 2.0, y: rect.height / 2.0), radius: min(rect.width, rect.height) / 2.0,startAngle: .degrees(startAngle),endAngle: .degrees(360 * progress + startAngle), clockwise: false)

        return path.strokedPath(.init(lineWidth: thickness, lineCap: .round))
    }
}
复制代码

快来动手试试吧!

如果本专栏对你有帮助,不妨点赞、评论、关注~

Acho que você gosta

Origin juejin.im/post/7095519626301276174
Recomendado
Clasificación