如何在SwiftUI中将一个视图底部对齐另一个视图,且该视图超出屏幕边缘并被裁剪?

4
我有一个视图,包含一个背景图/动画和前景的控制按钮。我想把按钮.overlay在背景上,在底部Leading、中心和Trailing对齐 - 请参见下面的截图(不显示实际背景)。
但是,背景必须是一个动画。我在AVPlayer周围使用一个包装视图来处理这个问题。播放的视频是纵向的,但被填充为横向,因此宽度比屏幕更宽。我需要它填充屏幕的垂直空间。那意味着我必须使用.frame(maxWidth: .infinity, maxHeight: .infinity).scaledToFill()
这样就可以完美地垂直对齐-灰色顶部中央标记和底部中心按钮被渲染在正确的位置上- 然而,它会影响水平对齐。底部右侧按钮相对于视频对齐,而不是跟屏幕对齐,因此对齐超出了屏幕的范围(请参见第二张图片)。
我需要背景填满整个屏幕,并使覆盖层适当对齐。视频采用肖像录制,但AVPlayer让它们以黑色填充的横向方式呈现,因此除非可以调整这一点,否则我无法更改视频的宽高比。
对我最有益的事情是学习如何相对于屏幕而不是父级在叠加堆栈中align components。是否有一种方法可以实现这一点?如果没有,是否有解决我的问题的解决方案(使按钮水平对齐)?
代码
以下代码不是真正的代码,只是生成演示的代码。提供此代码是因为评论中有位绅士礼貌地要求它。图像是最终的真相来源。实际的代码要大得多,具有随机(和基于应用程序状态的)选择要播放的AVPlayer mp4视频的机制。我认为这不应该很重要(封装和其他东西),但如果它很重要,请告诉我为什么它会影响代码结构,并在评论中添加更多细节。
private func bigBgImage() -> some View {
    self.backgroundChooser.render()
        .resizable()
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .scaledToFill()
}

在这里,backgroundChooser.render() 被视为黑盒子。你可以放入一个虚拟的 Image

期望的布局

期望的叠加布局

通过在 GeometryReader 中使用虚拟图像来实现此演示。这是解决 bigBgImage 具有 maxWidth: .infinity 的覆盖范围的变通方法。由于 GeometryReader 会影响动画对齐,因此我不希望在最终代码中使用它。尽管如此,这是片段代码:

GeometryReader { _ in
    self.bigBgImage()
}
    .overlay(alignment: .top) { self.title() }
    .overlay(alignment: .bottom) { self.resetButton() }
    .overlay(alignment: .bottomTrailing) { self.toggleButton() }

布局有误

溢出问题破坏了水平对齐

self.bigBgImage()
    .overlay(alignment: .top) { self.title() }
    .overlay(alignment: .bottom) { self.resetButton() }
    .overlay(alignment: .bottomTrailing) { self.toggleButton() }

backgroundChooser 的机制过于复杂,无法在一个有意义的SO问题中予以说明。您只需随意添加任何图像即可。


图片很好看,但你的视图代码更棒。 - xTwisteDx
我不明白为什么你需要以如此咸的方式(参见您的个人简介)来询问它。这些图片已经足够描述行为了 - 实际上,它们比我可以可靠编码的描述更好,这就是为什么我首先要问这个问题的原因。我会添加粗略的代码片段,但它们并不是真相 - 图片才是(无论您喜欢与否)。 - DrunkCoder
你想要的是在屏幕底部有三个按钮,分别位于左侧、中间和右侧吗? - Steven-Carrot
@tail 是的,三个操作按钮。对于这个问题,数量并不重要 - 问题在于在存在 maxWidth: .infinity 裁剪超出边缘的基础视图的情况下,前导/尾随对齐。请参见 @Asperi 的答案以使按钮正确对齐(使用带有 backgroundoverlay 的虚拟视图)。这会破坏背景的对齐方式,我只能通过解决方法来解决 - 请参见我的答案。 - DrunkCoder
3个回答

3
在这种情况下,我们应该将控件与内容分开。一种可能的方法是拥有一个独立的屏幕层(始终绑定到屏幕几何形状),然后将内容移动到后台,将控件移动到覆盖层中,例如:
Color.clear    // << always tight to screen area
//.ignoresSafeArea() // << optional, if needed

  .background(self.bigBgImage()) // now independent, but not clipped

  .overlay(alignment: .top) { self.title() }          // << all on screen
  .overlay(alignment: .bottom) { self.resetButton() }
  .overlay(alignment: .bottomTrailing) { self.toggleButton() }

谢谢你,我不知道.background。但是这也会影响对齐。视频是以纵向模式录制的,但是AVPlayerViewController通过在左右两侧填充黑色边缘将其变成了横向模式。这使得视频与左侧对齐并在右侧裁剪。有没有办法让它在两个边缘上均匀裁剪,以使其居中? - DrunkCoder
好的,这并没有回答问题,因为指定了背景是一个必须剪辑到屏幕边界的视频。我相信,在移动应用程序中,具有覆盖操作按钮的背景视图是如此普遍的设计模式,以至于任何熟悉特定UI框架的人都可以在很短时间内解决设计问题(我昨天开始使用SwiftUI,只花了10分钟就能找到相关信息)。问题的关键是剪辑、覆盖和对齐的交互 - 这是您没有解决的。 - DrunkCoder

3
我发现问题出在您的.frame.scaleToFill()代码之后图像

这里我有一个不同的解决方案。(代码在图片下面) enter image description here

请使用屏幕最大宽度和高度,而不是无限制的叠加。
struct DemoView: View {
var body: some View {
    ZStack {
        bigBigImage
            .overlay(alignment: .top) {
                Button("Top") {
                }
                .padding()
                .background(.black)
                .cornerRadius(10)
            }
            .overlay(alignment: .bottom) {
                Button("Center") {
                }
                .padding()
                .background(.black)
                .cornerRadius(10)
            }
            .overlay(alignment: .bottomTrailing) {
                Button("Leading") {
                }
                .padding()
                .background(.black)
                .cornerRadius(10)
                .padding(.trailing)
            }
     }
   }
    var bigBigImage: some View {
     Image("Swift")
        .resizable()
        .scaledToFill() //here
        .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) //here
        .edgesIgnoringSafeArea(.all)
    }
}

1
谢谢提供另一个选项!我不确定这是否会使视频太小(因为它是强制横屏),但我会尝试一下。我已经稍微重新调整了视频控制器,所以现在无法检查它。 - DrunkCoder

2

所以我找到了一个解决方法,需要手动设置背景的 x 偏移量。

Asperi 的部分回答 的基础上,我们添加一个 GeometryReader 并将 background 的偏移量设置为宽度的一半:

Color.clear
     .background() {
         GeometryReader { geo in
             self.bigBgImage()
                 .offset(x: -geo.size.width / 2)
         }
     }
     .overlay(alignment: .top) { self.title() }
     .overlay(alignment: .bottom) { self.resetButton() }
     .overlay(alignment: .bottomTrailing) { self.toggleButton() }

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