布局(五) – 对齐

温馨提示:本文最后更新于2024-10-23 18:30:43,某些文章具有时效性,若有错误或已失效,请在下方留言

默认情况下,⼏乎所有的视图都会把⼦视图居中对⻬。

Text("Hello")
  .frame(width: 100, height: 100)

frame 修饰器拥有⼀个默认值为 .centeralignment 参数。这 100⨉100 的区域中,⽂本将被居中对⻬。为了简单起⻅,我们假设 Text 报告的尺⼨是 50⨉20。当放置这个⼦视图时,frame 修饰器将执⾏以下步骤:

  1. 它向⼦视图询问⽔平中⼼。由于⼦视图的宽度是 50,所以它将⾃⼰的⽔平中⼼报告为 25 (在⼦视图的本地视图坐标系中)。这个值被叫做⼦视图的⽔平中⼼对⻬参考线
  2. 它向⼦视图询问垂直中⼼。这个⼦视图的⾼度为 20,所以它将⾃⼰的垂直中⼼报告为 10。这是⼦视图的垂直中⼼对⻬参考线
  3. .frame 计算⾃⼰的⽔平和垂直中⼼,结果为 (50, 50)。
  4. 现在,frame 可以将⼦视图中⼼对⻬了,通过计算两个中⼼点的差值 (50-25,50-10) 并将⼦视图的原点放在 frame 坐标空间的 (25, 40) 就可以做到这⼀点。

水平垂直居中对齐
水平垂直居中对齐


想要理解 SwiftUI 的对⻬系统,最重要的⼀点是理解对⻬总是⽗视图和⼦视图之间“协商”决定的结果。⽗视图不会独⾃觉决定⼦视图的放置位置,⽽是会向⼦视图协商相关的对⻬参考线,然后根据⼦视图⾃⼰的⼤⼩或者其他⼦视图来决定它的位置。

Text("Hello")
  .frame(width: 100, height: 100, alignment: .bottomTrailing)

算法和上⾯的 center 对⻬完全相同:

  1. 它向⼦视图询问⼦视图的尾端对⻬参考线。⼦视图回应 50,这个数值是⼦视图在⾃⼰的视图坐标系中的尾边的位置。
  2. 它向⼦视图询问其底部的对⻬参考线。⼦视图回应20。
  3. frame 计算得到它⾃⼰的尾边 (100) 和底边 (100) 的对⻬参考线。
  4. frame 将⼦视图的原点放置在(50, 80)。同样地,这也是通过求解 (100-50,100-20) 的差所得到的。

水平尾部垂直底部对齐
水平尾部垂直底部对齐


frame 视图修饰器,对⻬可以在两个⽅向上⽣效。frame 的对⻬参数类型是 Alignment,它是⼀个组合了 HorizontalAlignment (⽔平对⻬) 和 VerticalAlignment (垂直对⻬) 的组合结构体。其他能够在两个⽅向上进⾏对⻬的类型包括 .overlay.backgroundZStackVStack 只拥有⽔平对⻬,⽽ HStack 只拥有垂直对⻬。注意,Alignment 类型并不是对⻬参考线,它只是决定了在执⾏对⻬时,要使⽤哪⼀个对⻬参考线。

每个视图都⾃动定义了所有的内置对⻬参考线。例如,下⾯是三⾏的 Text 视图在垂直⽅向上最重要的⼏条对⻬参考线:

Text 视图垂直方向对齐参考线
Text 视图垂直方向对齐参考线


当计算不包含任何⽂字的 Text 视图的 firstTextBaselinelastTextBaseline 时,这个参考线将使⽤视图的⾼度 (此时这两个参考线等效于 .bottom)。

ZStack 必须向所有⼦视图咨询它们的对⻬参考线,并相对于彼此进⾏对⻬。 ZStack ⾸先必须 (基于其⼦视图的尺⼨以及它们的对⻬参考线) 计算⾃⼰的⼤⼩,然后才能使⽤对⻬⽅式来放置⼦视图:

ZStack 的对齐
ZStack 的对齐


因为 ZStack ⾥对⻬的默认值是 .center,所以它将两个⼦视图按照以下步骤进⾏中⼼对⻬:

  1. 确定 ZStack ⾃⼰的尺⼨:
    1. 向第⼀个⼦视图 (带有蓝⾊矩形的 frame 视图) 询问它的尺⼨和中⼼对⻬参考线。它回应尺⼨为 50⨉50,对⻬点为 (25, 25)。
    2. 向第⼆个⼦视图 (Text,我们假设它的尺⼨是 100⨉20) 询问尺⼨和中⼼对⻬参考线。它回应尺⼨为 100⨉20,对⻬点为 (50, 10)。
    3. 计算 Text 相对于矩形的原点位置,通过将两个对⻬点相减,可以得到结果:(25, 25) – (50, 10) = (-25, 15)。
    4. 确定每个⼦视图的框架:框架指的是⼦视图的原点和尺⼨的组合。
    5. 计算两个⼦视图框架的并集,这个并集的原点为 (-25, 0),尺⼨为 100×50。该尺⼨将被作为 ZStack ⾃⼰的尺⼨。
  2. 放置 ZStack 的⼦视图:
    1. 基于步骤 1 中计算出的尺⼨,ZStack 算出⾃⼰的中⼼为 (50, 25)。
    2. 计算矩形⼦视图的原点,⽤ ZStack 的中⼼点减去矩形的对⻬点:(50, 25) – (25, 25) = (25, 0)。
    3. 计算 Text 的原点,⽤ ZStack 的中⼼点减去⽂本的对⻬点:(50, 25) – (50, 10) = (0, 15)。

更通⽤地描述,每个容器视图在对⻬它的⼦视图时,采⽤如下⽅式:

  1. 确定⾃身尺⼨:
    1. 确定它的⼦视图的尺⼨。这具体取决于特定视图的类型,我们在本章的第⼀部分已经讨论过这个问题了。
    2. 基于容器的对⻬⽅式,向⼦视图们询问它们的对⻬参考线。
    3. 使⽤任意⼀个特定的⼦视图作为参考,计算所有⼦视图的框架。
    4. 计算这些⼦视图框架的并集,将所得到的结果尺⼨作为容器的尺⼨。
  2. 放置⼦视图:
    1. 依据容器视图⾃⼰的尺⼨计算出它⾃⼰的对⻬参考线。
    2. 通过从容器视图的对⻬参考线中减去⼦视图的对⻬参考线,计算出每个⼦视图的原点。

修改对齐参考线

SwiftUI 允许我们通过为特定的某个对⻬⽅式提供显式的对⻬参考线,来改变视图原先的隐式对⻬参考线。

let image = Image(systemName: "pencil.circle.fill")
    .alignmentGuide(.firstTextBaseline) { dimension in
        dimension.height / 2
    }

提供显式对⻬参考线本身并不会做任何事。只有当这个对⻬参考线在实际被使⽤来放置⼦视图时,它才会⽣效。⽐如:

修改对齐参考线
修改对齐参考线


举例来说,如果我们想要把⼀个⻆标视图的中⼼覆盖到另⼀个视图的顶部尾⻆时,可以使⽤下⾯的代码:

extension View {
    func badge<B: View>(@ViewBuilder _ badge: () -> B) -> some View {
        overlay(alignment: .topTrailing) {
            badge()
                .alignmentGuide(.top) {
                    $0.height / 2
                }
                .alignmentGuide(.trailing) {
                    $0.height / 2
                }
        }
    }
}

badge 扩展的使用
badge 扩展的使用


自定义对齐标识

实现的对齐效果
实现的对齐效果


最下⽅稍⼤⼀点的菜单按钮需要和上⾯⼏个单独的带⽂字的菜单按钮的圆形部分在竖直⽅向上居中对⻬。下⾯是这个菜单⼤致的结构:

struct CircleButton: View {
    let symbol: String
    let radius: Int
    let imageWidth: CGFloat
    
    init(symbol: String, radius: Int = 25, imageWidth: CGFloat = 26) {
        self.symbol = symbol
        self.radius = radius
        self.imageWidth = imageWidth
    }
    
    var body: some View {
        ZStack {
            Circle()
                .fill(.gray.opacity(0.4))
                .frame(width: CGFloat(radius) * 2, height: CGFloat(radius) * 2)
            
            Image(systemName: symbol)
                .resizable()
                .scaledToFit()
                .frame(width: imageWidth, height: imageWidth)
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            HStack {
                Text("Inbox")
                CircleButton(symbol: "tray.and.arrow.down")
            }
            
            HStack {
                Text("Sent")
                CircleButton(symbol: "tray.and.arrow.up")
            }
            
            CircleButton(symbol: "line.3.horizontal", radius: 30)         
        }
    }
}

当前样式

当前样式
当前样式


实现⾃定义对⻬的第⼀步,是需要创建⼀个遵守 AlignmentID 协议的类型:

struct MenuAlignment: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
        context.width / 2
    }
}

AlignmentID 协议的唯⼀要求,是⼀个静态的 defaultValue(in:) ⽅法。在这个⽅法⾥,我们需要为特定的对⻬选择⼀个默认值。

现在我们可以在 HorizontalAlignment 结构体中为我们的⾃定义对⻬添加⼀个静态常量,就像 SwiftUI 在定义内置对⻬⽅式时做的⼀样:

extension HorizontalAlignment {
    static let menu = HorizontalAlignment(MenuAlignment.self)
}

现在,.menu 的对⻬⽅式将由我们的 MenuAlignment 结构体进⾏标识。我们可以将它⽤在菜单的 VStack ⾥,并为 CircleButton 的 .menu 对⻬⽅式指定显式的对⻬参考线:

VStack(alignment: .menu) {
    HStack {
        Text("Inbox")
        CircleButton(symbol: "tray.and.arrow.down")
            .alignmentGuide(.menu) {
                $0.width / 2
            }
    }
    
    HStack {
        Text("Sent")
        CircleButton(symbol: "tray.and.arrow.up")
            .alignmentGuide(.menu) {
                $0.width / 2
            }
    }
    
    CircleButton(symbol: "line.3.horizontal", radius: 30)
}

对齐后效果
对齐后效果

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容