从另一个 SwiftUI 视图下方滑出一个视图

8

我正在尝试使用SwiftUI构建动画。

Start: [ A ][ B ][ D ]
End:   [ A ][ B ][    C    ][ D ]

动画的关键元素包括:
  • C 应该从 B 下方滑出(而不是从零宽度扩展)
  • 所有视图的宽度都由子视图定义,并且未知
  • 所有子视图的宽度在动画期间和之后不应更改(因此,最终状态下总视图宽度更大)
我用 SwiftUI 非常难以满足所有这些要求,但之前已经能够通过自动布局实现类似效果。
我的第一次尝试是使用具有 layoutPriorities 的 HStack 过渡。这与要求相差甚远,因为它会影响 C 在动画期间的宽度。
我的第二次尝试仍然使用了 HStack,但是使用了具有非对称移动动画的过渡。这已经非常接近了,但在动画期间 B 和 C 的移动并没有给人 C 直接位于 B 下方的效果。
我最新的尝试是放弃依赖于 HStack 用于两个动画视图,并改用 ZStack。使用这种设置,我可以通过组合偏移和填充来完美地进行动画。然而,请注意,只有在为 B 和 C 设置已知值的帧大小时才能得到正确的结果。
有没有人有任何想法可以实现这种效果,而不需要对 B 和 C 设置固定的帧大小?

WWDC的“介绍SwiftUI:构建你的第一个应用”会话中有一个示例,其中ZStack中有两个视图,其中一个滑入和滑出。也许它会对你有所帮助。 - koen
我和任何人一样都很新于SwiftUI,但我认为如果你想要自动调整大小,你需要保持HStack的参与。是否可能将C包含在一个组中,并进行翻译? - Drew McCormack
看起来你可能想要一个 C 的分组容器视图,它固定在可用单元格的大小上,并将其移动到 HStack 分配的区域之外,以动画方式显示。因此,你需要一种方法来使子视图移动到其父视图之外并进行裁剪。也许这是一个值得探究的方向。 - Drew McCormack
我使用.layoutPriority()和.offset()解决了一个类似的问题。我的视图由其子视图大小确定,在转换时会跳动,因为子视图被从视图层次结构中移除,导致宽度突然变为零。在ZStack的子视图上使用.layoutPriority(cIsVisible ? x : y)来选择哪个子视图决定父视图的大小,如果需要,可以引入虚拟子视图。然后根据需要在A、B、C和D上使用.offset(cIsVisible ? x : y)来修复任何位置错误。 - Dan Halliday
@DanHalliday 我已经尝试过了。我遇到的问题是找不出偏移量要使用什么值,因为我的视图大小是未知的。 - Mattie
2个回答

13

自从我最初回答这个问题以来,我一直在研究GeometryReader、View Preferences和Anchor Preferences。我已经准备了一个详细的解释,进一步阐述了这些内容。您可以在以下链接中阅读:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

enter image description here

一旦将 CCCCCCCC 视图的几何信息存入 textRect 变量中,剩下的就容易了。您只需要使用 .offset(x:) 修饰符和 clipped()。

import SwiftUI

struct RectPreferenceKey: PreferenceKey {
    static var defaultValue = CGRect()

    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }

    typealias Value = CGRect
}

struct ContentView : View {
    @State private var textRect = CGRect()
    @State private var slideOut = false

    var body: some View {

        return VStack {
            HStack(spacing: 0) {
                Text("AAAAAA")
                    .font(.largeTitle)
                    .background(Color.yellow)
                    .zIndex(4)


                Text("BBBB")
                    .font(.largeTitle)
                    .background(Color.red)
                    .zIndex(3)

                Text("I am a very long text")
                    .zIndex(2)
                    .font(.largeTitle)
                    .background(GeometryGetter())
                    .background(Color.green)
                    .offset(x: slideOut ? 0.0 : -textRect.width)
                    .clipped()
                    .onPreferenceChange(RectPreferenceKey.self) { self.textRect = $0 }

                Text("DDDDDDDDDDDDD").font(.largeTitle)
                    .zIndex(1)
                    .background(Color.blue)
                    .offset(x: slideOut ? 0.0 : -textRect.width)

            }.offset(x: slideOut ? 0.0 : +textRect.width / 2.0)

            Divider()
            Button(action: {
                withAnimation(.basic(duration: 1.5)) {
                    self.slideOut.toggle()
                }
            }, label: {
                Text("Animate Me")
            })
        }

    }
}

struct GeometryGetter: View {
    var body: some View {
        GeometryReader { geometry in
            return Rectangle()
                .fill(Color.clear)
                .preference(key: RectPreferenceKey.self, value:geometry.frame(in: .global))
        }
    }
}

如果您想让所有内容保持居中,只需将以下代码添加到HStack容器中:HStack { ... }.offset(x: slideOut ? 0.0 : +textRect.width / 2.0) - kontiki
1
好的,我已经修改了代码,使其百分之百安全。我猜你不喜欢DispatchQueue.main.async调用(说实话,我也不喜欢)。我有另一个小技巧。使用Preferences代替。虽然他们的文档基本上不存在,但我设法让它们工作了。 - kontiki
1
有另一个有趣的工具,但到目前为止我还没有弄清楚它。锚点偏好设置似乎是几何属性的宝藏,但并没有被记录下来。 - kontiki
2
我更新了我的答案,因为我解决了C视图比A和B的总和大的问题。结果我们只需要在.offset()后面添加.clipped()即可。 - kontiki
感谢您提供的精彩演示。我想指出这种方法存在一个严重的限制,阻止它在一般情况下的应用 - 当第三个子视图被隐藏时,它仍然会对其父视图的大小产生影响。另一方面,我还没有找到一种使用offset()转换来实现动画的方法(我怀疑这是否可行)。我知道答案是在SwiftUI发布后不久给出的。三年后似乎在这方面没有太多变化? - rayx
显示剩余3条评论

4
很难确定你想要什么或者哪里出了问题。如果你能展示你所谓的“错误”动画或者分享你的代码,那么我们更容易帮助你。
无论如何,这是我的建议。我认为它在某种程度上做到了你所描述的,虽然肯定不完美:

Animated GIF of my solution

观察:

  • 动画依赖于假设(A)和(B)加在一起比(C)宽。否则,在动画开始时,(C)的部分将出现在A的左侧。

  • 同样,动画依赖于视图之间没有间隔。否则,当(C)比(B)宽时,(C)将出现在(B)的左侧。

    可能可以通过在层次结构中放置一个不透明的底层视图来解决这两个问题,使其位于(A)、(B)和(D)下方,但位于(C)上方。但我还没有想清楚。

  • HStack似乎比(C)滑入得更快一点,这就是为什么会出现白色区域的原因。我没有成功地消除这个问题。我尝试给HStack、转换、withAnimation调用和VStack添加相同的animation(.basic())修饰符,但这并没有帮助。

代码:

import SwiftUI

struct ContentView: View {
  @State var thirdViewIsVisible: Bool = false

  var body: some View {
    VStack(alignment: .leading, spacing: 20) {
      HStack(spacing: 0) {
        Text("Lorem ").background(Color.yellow)
          .zIndex(1)
        Text("ipsum ").background(Color.red)
          .zIndex(1)
        if thirdViewIsVisible {
          Text("dolor sit ").background(Color.green)
            .zIndex(0)
            .transition(.move(edge: .leading))
        }
        Text("amet.").background(Color.blue)
          .zIndex(1)
      }
        .border(Color.red, width: 1)
      Button(action: { withAnimation { self.thirdViewIsVisible.toggle() } }) {
        Text("Animate \(thirdViewIsVisible ? "out" : "in")")
      }
    }
      .padding()
      .border(Color.green, width: 1)
  }
}

谢谢。我的问题表述不够清楚,抱歉。你所做的基本上就是我的第二次尝试。如果你给B添加一些透明度并减慢动画速度,你会发现为什么HStack间距看起来有点奇怪。C的起始位置也不完美。或许可以以某种方式控制它? - Mattie
1
你真的应该在你的问题中添加一个动画GIF,以显示你所说的“HStack间距很有趣”是什么意思。 - Ole Begemann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接