布局(三) – 视图修饰符

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

视图修饰符总是把已经存在的视图包装到另⼀层中去:修饰符会变为它所作⽤的视图的⽗视图。

Padding

.padding 修饰符使⽤它收到的内边距值来修改建议尺⼨,它会从建议尺⼨的对应边上减去这个边距值。修改后的尺⼨将被提供给 .padding 的⼦视图 (也就是这个修饰符所作⽤的视图)。当⼦视图将其尺⼨报告回来时,padding 再将边距值加回到对应的边上,然后把扩展后的尺⼨作为⾃⼰的尺⼨进⾏报告。

.padding 有两个有⽤的变体:

  • .padding(_ inset:) 让我们能使⽤ EdgeInset 值,在单次调⽤中就为不同的边指定不同的边距值。
  • .padding(_ edges:_ length:) 让我们能够为⼀组边 (⽐如分别代表左右两边和上下两边的 .horizontal 或者 vertical) 指定⼀个边距值。

没有指定具体值,⽽是使⽤不带任何参数的 .padding() 时,则会使⽤当前平台的默认边距值。

固定框架

固定框架 (fixed frame) 修饰符 .frame(width:height:alignment) 所拥有的布局⾏为⾮常简单:它把所指定的尺⼨原封不动地提供给⼦视图,同时不管⼦视图所报告的尺⼨是多少,总是把这个指定的尺⼨作为⾃⼰的⼤⼩进⾏汇报。

如果只指定宽度或者⾼度,⽽把另⼀个参数设定为 nil 或者完全省略掉它,那么固定框架将不会对该维度产⽣影响。向固定框架提供的建议尺⼨在该维度上将被直接转发给⼦视图,固定框架在这个维度上也会将⼦视图所报告的尺⼨作为⾃⼰的尺⼨。

灵活框架

灵活框架 (flexible frame) 的 API ⽀持⾮常多的参数:我们不仅可以为框架的宽和⾼提供最⼩、最⼤和理想值,还可以为它指定对⻬⽅式。

最⼤值和最⼩值的边界会被灵活框架使⽤两次:⼀次在它提供尺⼨给⼦视图时,另⼀次在决定框架⾃⼰要报告的尺⼨时。

灵活框架会把所获得的建议尺⼨,按照指定的最⼩值和最⼤值进⾏限制:如果传⼊的尺⼨没有落在最⼩和最⼤值之间,则强制使⽤最⼩和最⼤值作为建议尺⼨并提供给框架的⼦视图。

当⼦视图的尺⼨被计算出来后,灵活框架将基于它所收到的建议尺⼨,再次应⽤设定边界值,来确定⾃⼰的尺⼨。

在实践中,这意味着如果我们只指定了 minWidth,那么灵活框架的宽度⾄少会是 minWidth,⽽最多会是它的⼦视图的宽度。如果我们只指定了 maxWidth,那么灵活框架的宽度⾄少是它的⼦视图的宽度,⽽最多则为 maxWidth。⾼度也同理。

Text("Hello, World!")
    .frame(maxWidth: .infinity)
    .background(.quaternary)

灵活框架 maxWidth
灵活框架 maxWidth


.frame(maxWidth: .in!nity) 模式确保了灵活框架的宽度⾄少和被建议的宽度相同,如果⼦视图的宽度要⽐框架接受的建议宽度还宽的话,则使⽤⼦视图的宽度。通常,这个模式被⽤来创建占据整个可⽤宽度的视图。

此模式添加为 View 的扩展

extension View {
    func proposedWidthOrGreater() -> some View {
        frame(maxWidth: .infinity)
    }
}

另⼀种常⻅模式如下:

Text("Hello, World!")
    .frame(minWidth: 0, maxWidth: .infinity)
    .background(.teal)

这可以确保框架⽆视它的⼦视图的尺⼨,框架报告的宽度将总是它接受到的建议宽度。同样,我们可以为这个模式编写⼀个辅助函数:

extension View {
    func acceptProposedWidth() -> some View {
        frame(minWidth: 0, maxWidth: .infinity)
    }
}

示例
示例


假设视图渲染到 320⨉480 的屏幕上,布局渲染的步骤如下所示:

  1. 系统向 padding 提出 320⨉480 作为建议尺⼨。
  2. paddingbackground 建议 300⨉460。
  3. background 将同样的 300⨉460 建议给它的主要⼦视图 (也就是 frame)。
  4. frame 把同样的 300⨉460 提供给它的⼦视图 Text
  5. Text 汇报它的尺⼨为 76⨉17。
  6. frame 的宽度变为 max(0, min(.infinity, 300)) = 300。注意,这⾥的 0.infinity 值都是灵活尺⼨的参数⾥指定的值。
  7. background 把灵活框架的尺⼨ (300⨉17) 提供给次要⼦视图 (Color)。
  8. 颜⾊视图接受这个建议,并将它作为⾃⼰的尺⼨汇报。
  9. background 将它的主要⼦视图的尺⼨ (300⨉17) 进⾏汇报。
  10. padding 在每条边上加上 10,最终它的尺⼨为 320⨉37。

理想尺⼨,就是如果某个维度上收到的建议尺⼨为 nil 时将采⽤的尺⼨。如果指定了 idealWidthidealHeight 参数,在灵活框架收到包含 nil 的建议尺⼨时 (通常来源于 fixedSize 修饰符),它会把指定的理想尺⼨提供给⼦视图,并且这个尺⼨也会被作为框架⾃身的尺⼨进⾏报告,⽽再不考虑其⼦视图的尺⼨。

宽高比

aspectRatio 修饰符在处理完全灵活的视图时⾮常有⽤。例如

Color.secondary
    .aspectRatio(4/3, contentMode: .fit)

宽高比 4 / 3 的矩形
宽高比 4 / 3 的矩形


aspectRatio 修饰符将会计算出⼀个能够适配建议尺⼨的宽⾼⽐为 4/3 的矩形,然后将它作为建议尺⼨提供给⼦视图。对于⽗视图,它总是把⼦视图的尺⼨汇报上去,⽽不去理会建议尺⼨或者设定的⽐例。

假设⼀个 200×200 的建议尺⼨。aspectRatio 通过计算,得到在这个建议尺⼨下满⾜ 4/3 宽⾼⽐的最⼤矩形,此处是 200⨉150。它把这个尺⼨建议给 Color,后者会直接接受建议。最后,aspectRatio 把 200⨉150 作为它⾃⼰的尺⼨进⾏汇报。

如果把 contentMode.fit 替换为 .fill

Color.secondary
    .aspectRatio(4/3, contentMode: .fill)

假设⼀个 200×200 的建议尺⼨,基于这个尺⼨,.aspectRatio 将会计算⼀个 4/3 宽⾼⽐,能够覆盖住整个建议尺⼨的最⼩矩形。本例中这个矩形的尺⼨约为 266.6⨉200。它把这个尺⼨建议给 Color,后者会直接接受建议。最后,aspectRatio 把 266.6⨉200 这个⼦视图的尺⼨作为它⾃⼰的尺⼨进⾏汇报。

.aspectRatio 最常⻅的使⽤⽅式是配合图像。

Image("header")
    .resizable()
    .aspectRatio(contentMode: .fit)

因为我们没有指定具体的宽⾼⽐,.aspectRatio 将会 (通过提供 nil⨉nil 尺⼨) 探测⼦视图的理想尺⼨,并依据这个尺⼨计算宽⾼⽐。

Image 的宽高比
Image 的宽高比


假设建议尺寸为 200×200,布局渲染的步骤:

  1. 200⨉200 的建议尺⼨被提供给 aspectRatio。
  2. aspectRatio 向图像建议 nil⨉nil。
  3. 图像报告⾃⼰的理想尺⼨ 100⨉30。
  4. aspectRatio 将⼀个宽⾼⽐为 100/30 的矩形适配到 200⨉200 中,得到 200⨉60,并将这个尺⼨提供给图⽚。
  5. 图⽚将⾃⼰的尺⼨报告为 200⨉60。
  6. aspectRatio 将⼦视图的尺⼨ 200⨉60 作为⾃⼰的尺⼨进⾏报告。

覆盖层和背景

覆盖层 (overlay) 和背景 (background) 是 SwiftUI 中最有⽤的⼏个修饰符中的成员了。在布局⽅⾯,两者的⼯作⽅式完全相同。唯⼀的区别在于 overlay 会把次要视图绘制在主要视图的前⾯,⽽ background 则是把次要视图绘制在主要视图的后⽅。

背景 background
背景 background


background ⾸先确定它的主要⼦视图 (上例中的 Text) ⼤⼩,然后将这个主要⼦视图的尺⼨作为建议尺⼨提供给次要⼦视图。这样,⻘绿⾊背景的⼤⼩就和 Text 视图⼤⼩⼀致了。我们也可以修改⼀下 Text,让它包含⼀些边距:

Padding + Background
Padding + Background


背景和覆盖层都不会影响其主要⼦视图的布局,两者所报告的尺⼨始终都是主要⼦视图的尺⼨。

固定尺寸

fixedSize() 修饰符不考虑它⾃⼰收到的建议尺⼨,转⽽会向它的⼦视图建议 nil 的宽度和⾼度。这样,⼦视图将使⽤⾃⼰的理想尺⼨。它具有⼀个重载⽅法 fixedSize(horizontal:vertical:),可以允许我们使⼦视图只在某⼀个维度上变为其理想尺⼨ (例如,将宽度建议为 nil,但不改变⾼度)。

extension View {
    func badge<Badge: View>(@ViewBuilder contents: () -> Badge) -> some View {
        self.overlay(alignment: .topTrailing) {
            contents()
                .padding(3)
                .background {
                    RoundedRectangle(cornerRadius: 5)
                        .fill(.teal)
                }
                .fixedSize()
        }
    }
}

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

请登录后发表评论

    暂无评论内容