【问题标题】:SwiftUI - how to override nested offset/position animations?SwiftUI - 如何覆盖嵌套的偏移/位置动画?
【发布时间】:2021-04-09 04:16:11
【问题描述】:

考虑这个简单的例子:

struct TestView: View {
    @State private var enabled = false
    
    var body: some View {
        Circle()
            .foregroundColor(.red)
            .overlay(
                Circle()
                    .foregroundColor(.blue)
                    .frame(width: 50, height: 50)
                    .animation(.spring())
                
            )
            .frame(width: 100, height: 100)
            .offset(x: 0, y: enabled ? -50 : 50)
            .animation(.easeIn(duration: 1.0))
            .onTapGesture{
                enabled.toggle()
            }
            
    }
}

点击生成的圆圈时会产生以下动画:

嵌套圆通过其自己的计时函数(弹簧)动画到其新的全局位置,而外圈/父视图通过其 easeInOut 计时函数动画到新的全局位置。理想情况下,如果修饰符在父视图抽象级别(在本例中为offset,但也有类似position)工作,所有子节点都将使用父时间函数进行动画处理。

这可能是因为 SwiftUI 渲染引擎在布局过程中为视图层次结构中所有受影响的子项计算新的全局属性,并根据附加的最具体的动画修饰符对每个属性的更改进行动画处理(即使在这种情况下,子项在父项中的相对位置不会改变)。这使得当视图的子视图可能运行它们自己的复杂动画(父视图不知道并且真的不应该知道)。

我注意到的另一个怪癖是,在这个特定的示例中,在偏移量修饰符之前添加一个animation(nil) 修饰符会破坏外圆上的动画,尽管.easeInOut 仍然直接附加到偏移修改器。这违反了我对这些修改器如何链接的理解,其中(根据this source)动画修改器适用于它需要的所有视图,直到下一个嵌套动画修改器。

【问题讨论】:

  • 说实话,我不清楚你想达到什么目的。
  • @Asperi 影响父视图属性(偏移量、位置)的动画不应在子视图中触发动画。在上面的例子中,整个圆(外 + 内)应该一起移动,即使子视图(即内圆)正在执行其他动画。
  • 动画由修改后的属性触发,反之则不然。因此,如果您更改可动画属性,则当前上下文动画将被激活。如果您想要独立的动画,请将它们加入到特定的不同值中。
  • @Asperi 在这种情况下将动画修改器绑定到不同的值仍然不能解决问题 - 我已经尝试过了。问题是动画修改器绑定到改变一个属性的值仍将应用于它们修改到的视图的所有其他属性,即使该属性的更改是隐式的(即,由于父级更改,它是屏幕上的全局位置它是自己的偏移量,即使孩子的本地位置根本没有改变)。
  • @roozbubu,我发现了同样的问题,见stackoverflow.com/questions/65582121/…

标签: ios swift swiftui swiftui-animation


【解决方案1】:

这对我来说似乎是一个错误,可能值得向 Apple 报告。通过在 .offset 修饰符之前添加另一个修饰符,我确实找到了一个奇怪的解决方法:

struct TestView: View {
    @State private var enabled = false
    
    var body: some View {
        Circle()
            .foregroundColor(.red)
            .overlay(
                Circle()
                    .foregroundColor(.blue)
                    .frame(width: 50, height: 50)
                    .animation(.spring())
                
            )
            .frame(width: 100, height: 100)
            .scaleEffect(1) // <------- this doesn't do anything but somehow overrides the animation
            .offset(x: 0, y: enabled ? -50 : 50)
            .animation(.easeIn(duration: 1.0))
            .onTapGesture{
                enabled.toggle()
            }
            
    }
}

我已经用 scaleEffect、rotationEffect 和 rotation3DEffect 进行了尝试,它们都可以工作。并非所有修饰符都有效。我真的很好奇为什么这些特定的会这样做。

.animation(nil) 对我来说也是一个错误。

【讨论】:

  • 据我所知,.animation(nil) 案例不是 错误,而是 SwiftUI 的工作方式(至少现在如此)。
  • 这确实应该得到修复。非常感谢这一发现。
【解决方案2】:

我将尝试澄清您在问题中提到的一些要点:

理想情况下,如果修饰符在父视图抽象级别起作用,则所有子级都将使用父级计时函数进行动画处理

考虑以下示例:

Circle()
    .overlay(
        Circle()
            .frame(width: 50, height: 50)
            .foregroundColor(.blue)
    )
    .frame(width: 100, height: 100)
    .foregroundColor(.red)

如果你说的是真的,那么嵌套的Circle 应该是红色的。在 SwiftUI 中,父修饰符不会覆盖子修饰符。


在偏移量修改器打破外圈动画之前添加一个动画(nil)修改器

这与上面的示例完全相同。

通过调用:

Circle()
    .foregroundColor(.red)
    .overlay(
        Circle()
            .foregroundColor(.blue)
            .frame(width: 50, height: 50)
            .animation(.spring())
    )
    .frame(width: 100, height: 100)
    .animation(nil) // with `nil` animation
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

您将 animation 设置为 nil 仅用于父级。子视图有自己的animation 修饰符。

如果内部 Circle 没有 animation 修饰符,它会起作用,因此父修饰符也适用于这个特定的子视图:

Circle()
    .foregroundColor(.red)
    .overlay(
        Circle()
            .foregroundColor(.blue)
            .frame(width: 50, height: 50)
            // .animation(.spring())
    )
    .frame(width: 100, height: 100)
    .animation(nil) // now this will work for both parent and child view
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

这违反了我对这些修改器如何链接的理解,其中 [...] 动画修改器适用于它需要的所有视图,直到下一个嵌套动画修改器。

直到下一个嵌套动画修饰符是这里的关键。设置animation(nil) 有效直到它找到另一个animation 修饰符。


如何覆盖嵌套的偏移/位置动画?

最简单的选择就是删除嵌套的.animation(.spring()) 修饰符。

如果无法做到这一点,您可以从两个圆圈创建一个视图,例如使用scaleEffect 作为their answer 中建议的@bze12。

来自scaleEffect的文档:

相对于锚点,按给定的垂直和水平尺寸量缩放此视图的渲染输出。

这意味着下面的代码:

Circle()
    .foregroundColor(.red)
    .overlay(
        Circle()
            .foregroundColor(.blue)
            .frame(width: 50, height: 50)
            .animation(.spring())
    )
    .frame(width: 100, height: 100)
    .scaleEffect()
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

也可以读作:

scaledView
    .offset(x: 0, y: enabled ? -50 : 50)
    .animation(.easeIn(duration: 1.0))
    .onTapGesture {
        enabled.toggle()
    }

如您所见,现在只有一个 animation 修饰符,它适用于生成的缩放视图。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多