如何在SwiftUI中控制动画分数?

64jmpszr  于 2023-08-02  发布在  Swift
关注(0)|答案(1)|浏览(129)

当在两个状态之间动画视图时,如何实现不仅从state1到state2或返回的动画,而且设置特定的动画分数,就像在UIKit中使用UIViewPropertyAnimator的fractionComplete一样?
这里是代码,我可以动画之间的2个国家的水龙头。但是我需要follow slider value(或者从socket获取value,或者scrollView offset,无论CGFloat从0到1是什么)。基本上,需要传递一个动画片段,以便视图在时间轴上静态地表示它的状态。简而言之,需要设置动画进度

struct TransitionView: View {
    @Namespace var namespace
    @State var isExpanded: Bool = false
    
    @ViewBuilder
    var body: some View {
        VStack {
            switch isExpanded {
            case true:
                VStack {
                    Circle()
                        .strokeBorder(Color.green, lineWidth: 5)
                        .matchedGeometryEffect(id: "circle", in: namespace)
                        .frame(width: 70, height: 70)
                    Text(verbatim: "I am Circle")
                        .matchedGeometryEffect(id: "text", in: namespace)
                }
            case false:
                HStack {
                    Circle()
                        .strokeBorder(Color.red, lineWidth: 5)
                        .matchedGeometryEffect(id: "circle", in: namespace)
                        .frame(width: 30, height: 30)
                    Text(verbatim: "I am Circle")
                        .matchedGeometryEffect(id: "text", in: namespace)
                }
            }
        }
        .contentShape(Rectangle())
        .animation(.easeInOut, value: isExpanded)
        .onTapGesture {
            isExpanded.toggle()
        }
    }
}

字符串


的数据

qyzbxkaa

qyzbxkaa1#

您在示例中看到的实际上是从水平布局的circle+label到垂直布局的circle+label的过渡。但是这两组视图是相互独立的,因此对颜色等属性的更改不会被动画化。
使用matchedGeometryEffect有助于从一个布局到另一个布局的平滑过渡,但这更多的是由于运气。这是因为你没有找到来源。根据documentation,如果“isSource = true的组中当前插入的视图的数量不正好是一个”,则结果未定义。换句话说,您没有正确使用matchedGeometryEffect
为了更好地控制动画,我建议进行以下更改:

1.将两个布局合并为一个

更改视图结构,以便只有一个圆和一个标签。这很棘手,因为您要将布局从水平更改为垂直,但一种方法是将圆应用为具有计算偏移的叠加:

struct TransitionView: View {
    @State var isExpanded: Bool = false
    private let minCircleSize = CGFloat(30)
    private let maxCircleSize = CGFloat(70)
    private let spacing = CGFloat(10)

    var body: some View {
        Text(verbatim: "I am Circle")
            .padding(.top, isExpanded ? maxCircleSize + spacing : 0)
            .padding(.leading, isExpanded ? 0 : minCircleSize + spacing)
            .frame(minHeight: minCircleSize)
            .overlay {
                GeometryReader { proxy in
                    Circle()
                        .strokeBorder(isExpanded ? .green : .red, lineWidth: 5)
                        .frame(
                            width: isExpanded ? maxCircleSize : minCircleSize,
                            height: isExpanded ? maxCircleSize : minCircleSize
                        )
                        .offset(x: isExpanded ? proxy.size.width / 2 - (maxCircleSize / 2) : 0)
                }
            }
            .contentShape(Rectangle())
            .animation(.easeInOut, value: isExpanded)
            .onTapGesture {
                isExpanded.toggle()
            }
    }
}

字符串

2.将boolean替换为合适的变量

如果你希望转换能够表示一个中间状态,那么你需要用一个可以用来保存这个状态的变量来替换布尔值,比如CGFloat。如果我们坚持在进度达到一半时进行简单的颜色更改,则可以如下实现:

struct TransitionView: View {

    @State private var fractionComplete = 0.0
    private let minCircleSize = CGFloat(30)
    private let maxCircleSize = CGFloat(70)
    private let spacing = CGFloat(10)

    var body: some View {
        ZStack {
            Text(verbatim: "I am Circle")
                .padding(.top, fractionComplete * (maxCircleSize + spacing))
                .padding(.leading, (1 - fractionComplete) * (minCircleSize + spacing))
                .frame(minHeight: minCircleSize)
                .overlay {
                    GeometryReader { proxy in
                        Circle()
                            .strokeBorder(fractionComplete > 0.5 ? .green : .red, lineWidth: 5)
                            .frame(
                                width: minCircleSize + (fractionComplete * (maxCircleSize - minCircleSize)),
                                height: minCircleSize + (fractionComplete * (maxCircleSize - minCircleSize))
                            )
                            .offset(x: fractionComplete * (proxy.size.width / 2 - (maxCircleSize / 2)))
                    }
                }
                .contentShape(Rectangle())
                .animation(.easeInOut, value: fractionComplete)
                .onTapGesture {
                    fractionComplete = fractionComplete > 0.5 ? 0 : 1
                }
            // A slider
            VStack {
                Spacer()
                Spacer()
                Slider(
                    value: $fractionComplete,
                    in: 0...1
                )
                .padding()
                Spacer()
            }
        }
    }
}

3.复杂动画使用Animatable视图修饰符

动画通过确定开始位置和结束位置并在它们之间进行插值来工作。当更改是线性的时,这很有效,就像这里的圆大小和偏移一样。但是,如果中间状态涉及到更复杂的函数,例如遵循非线性路径,则可能需要实现自己的Animatable视图修饰符。
我不认为这适用于这里,因为你更感兴趣的是表现一个冻结的中间状态,而不是执行一个不规则的动画。但有关可设置动画的视图修改器的更多信息,请参见以下其他答案:
Change Reverse Animation Speed SwiftUI
swiftui Alignment Guide for two view

相关问题