在 SwiftUI 中的 Shape 中绘制多个路径并为其设置动画

问题描述 投票:0回答:1

我正在尝试实现如下所示的动画。

首先,根据完成的真假,显示适当的形状(+或✓)。点击按钮后,我希望形状具有动画效果,如上图所示。据我了解:

  • 如果以 + 符号开始: (1.) 水平笔划不透明度变为零 (2.) 垂直笔划的顶部以逆时针方式从
    (midX, minY)
    移动到
    (minX, midY)
    ,形成 ✓ 符号的左侧笔划。垂直笔画的底部保持固定。 ✓ 符号的右笔划现在从
    (midX, maxY)
    处的垂直笔划底部开始动画,并延伸至右上角至
    (maxX, minY)
    以完成形状。
  • 如果以 ✓ 符号开始: (1.) ✓ 的右笔画从
    (maxX, minY)
    向后移动到底部
    (midX, maxY)
    ,两笔画相交。 (2.) ✓ 的左笔划动画回到
    (midX, minY)
    以形成 + 符号的垂直笔划。 (3.) 最后,+ 符号的水平笔划动画回到不透明度 1。
  • 点击按钮时形状动画应反转。如果用户快速点击切换状态的按钮,则应忽略动画持续时间,并且形状应仅在 + 和 ✓ 之间切换,直到用户从按钮上抬起触摸;在这种情况下,根据
    completed
    的最终状态,应显示的形状应继续以动画方式显示在视图中。

编辑:更新代码以使用不同的线段形状。但是,✓ 符号的右笔画不会为路径图绘制动画。此外,在形状之间切换时,动画无法正确反转。

如有任何帮助,我们将不胜感激。

struct LineSegment1: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        
        // MARK: + shape's horizontal line
        path.move(to: CGPoint(x: rect.minX, y: rect.midY))
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
        
        return path
    }
}

struct LineSegment2: Shape {
    var endPoint: CGPoint

    var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get { AnimatablePair(endPoint.x, endPoint.y) }
        set {
            endPoint.x = newValue.first
            endPoint.y = newValue.second
        }
    }

    // MARK: + shape's vertical line & ☑️ shape's left stroke
    func path(in rect: CGRect) -> Path {
        let start = CGPoint(x: rect.midX, y: rect.maxY)
        let end = CGPoint(x: endPoint.x * rect.width, y: endPoint.y * rect.height)
        var path = Path()
        path.move(to: start)
        path.addLine(to: end)
        return path
    }
}

struct LineSegment3: Shape {
    var animatableData: CGFloat {
        get { return progress }
        set { progress = newValue }
    }
    
    var progress: CGFloat = 0
    
    func path(in rect: CGRect) -> Path {
        var path = Path()
        // MARK: ☑️ shape's, right stroke
        path.move(to: CGPoint(x: rect.midX, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.minX + (rect.maxX * progress), y: rect.minY))
        return path
    }
}
struct ContentView: View {
    @State private var drawProgress: CGFloat = 0.0
    @State private var completed: Bool = false
    
    var body: some View {
        VStack {
            RoundedRectangle(cornerRadius: 5)
                .frame(width: 300, height: 300)
                .foregroundColor(.red)
                .overlay {
                    ZStack {
                        LineSegment1()
                            .stroke(.white, lineWidth: 4.0)
                            .opacity(completed ? 0 : 1)
                            .frame(width: 200, height: 200)
                        LineSegment2(endPoint: CGPoint(
                            x: completed ? 0.0 : 0.5,
                            y: completed ? 0.5 : 0.0))
                        .stroke(completed ? .yellow : .white, lineWidth: 4.0)
                        .frame(width: 200, height: 200)
                        LineSegment3(progress: drawProgress)
                            .stroke(.yellow, lineWidth: 4.0)
                            .opacity(completed ? 1 : 0)
                            .frame(width: 200, height: 200)
                    }
                }
            
            Button(completed ? "Incomplete" : "Complete") {
                withAnimation(.easeInOut(duration: 0.5)) {
                    drawProgress = 1.0
                    completed.toggle()
                }
            }
            .buttonStyle(.borderedProminent)
            Spacer()
        }
        .padding()
    }
}
ios swift swiftui shapes
1个回答
0
投票

我认为有一种更简单的方法来实现这个动画。

您只需要 3 行,其中不透明度、方向、长度和颜色以状态变量为条件,并以动画方式进行更改。线的长度应由边界框确定。

在您的说明中,您说第 2 段的底座应位于

(midX, maxY)
,但我认为它稍微偏向中心左侧,并且复选标记的较长部分位于 90 度。无论如何,我在下面的版本中将其配置为可配置,因此您可以尝试调整它。

因此,这是让它发挥作用的尝试:

struct PlusOrChecked: View {
    let completed: Bool
    let smallTickLineFraction = CGFloat(0.55)

    struct Line: Shape {
        func path(in rect: CGRect) -> Path {
            var path = Path()
            path.move(to: CGPoint(x: rect.midX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
            return path
        }
    }

    private func xOffsetForCheckmark(height: CGFloat) -> CGFloat {
        height * smallTickLineFraction * sin(Double.pi / 4) - (height / 2)
    }

    private func yOffsetForCheckmark(height: CGFloat) -> CGFloat {
        let y = ((smallTickLineFraction * smallTickLineFraction) / 2).squareRoot()
        return -y * height / 2
    }

    private func lengthForSegment3(height: CGFloat) -> CGFloat {
        let x = 1 - ((smallTickLineFraction * smallTickLineFraction) / 2).squareRoot()
        return (x * x * 2).squareRoot() * height
    }

    var body: some View {
        GeometryReader { proxy in
            Color.clear
                .overlay {
                    Line()
                        .stroke(style: .init(lineWidth: proxy.size.height / 10, lineCap: .round))
                        .rotationEffect(.degrees(90))
                        .opacity(completed ? 0 : 1)
                        .foregroundColor(.white)
                }
                .overlay(alignment: .bottom) {
                    Line()
                        .stroke(style: .init(lineWidth: proxy.size.height / 10, lineCap: .round))
                        .frame(height: proxy.size.height * (completed ? smallTickLineFraction : 1))
                        .rotationEffect(.degrees(completed ? -45 : 0), anchor: .bottom)
                        .offset(
                            x: completed ? xOffsetForCheckmark(height: proxy.size.height) : 0,
                            y: completed ? yOffsetForCheckmark(height: proxy.size.height) : 0
                        )
                        .foregroundColor(completed ? .blue : .white)
                }
                .overlay(alignment: .bottom) {
                    Line()
                        .stroke(style: .init(lineWidth: proxy.size.height / 10, lineCap: .round))
                        .frame(height: completed ? lengthForSegment3(height: proxy.size.height) : 0)
                        .rotationEffect(.degrees(45), anchor: .bottom)
                        .offset(
                            x: completed ? xOffsetForCheckmark(height: proxy.size.height) : 0,
                            y: completed ? yOffsetForCheckmark(height: proxy.size.height) : 0
                        )
                        .foregroundColor(.blue)
                }
        }
    }
}

struct ContentView: View {
    @State private var completed = false
    @State private var timeOfLastTap = Date.now

    var body: some View {
        VStack(spacing: 30) {
            PlusOrChecked(completed: completed)
                .frame(width: 100, height: 100)
                .padding(5) // best if >= 5% of frame height
                .background(Color(white: 0.2))

            Button(completed ? "Incomplete" : "Complete") {
                withAnimation(.easeInOut(duration: min(0.5, -timeOfLastTap.timeIntervalSinceNow))) {
                    completed.toggle()
                }
                timeOfLastTap = Date.now
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

AnimatedLines


编辑 更新为对 y 方向上的复选标记段使用与 x 方向上相同的偏移量,以便复选标记在完全显示时居中。还使动画持续时间取决于上次点击时间,以便快速点击会导致快速变化。

© www.soinside.com 2019 - 2024. All rights reserved.