我正在尝试实现如下所示的动画。
首先,根据完成的真假,显示适当的形状(+或✓)。点击按钮后,我希望形状具有动画效果,如上图所示。据我了解:
(midX, minY)
移动到 (minX, midY)
,形成 ✓ 符号的左侧笔划。垂直笔画的底部保持固定。 ✓ 符号的右笔划现在从 (midX, maxY)
处的垂直笔划底部开始动画,并延伸至右上角至 (maxX, minY)
以完成形状。(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()
}
}
我认为有一种更简单的方法来实现这个动画。
您只需要 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)
}
}
}
编辑 更新为对 y 方向上的复选标记段使用与 x 方向上相同的偏移量,以便复选标记在完全显示时居中。还使动画持续时间取决于上次点击时间,以便快速点击会导致快速变化。