Layout
在SwiftUI中,Layout
协议不是公共API的一部分,而是用于内部实现的。它用于定义如何布局视图的实际算法。开发者通常不会直接与Layout
协议进行交互,而是通过使用SwiftUI提供的布局系统来实现自定义布局。
SwiftUI的布局系统是基于声明式的,它使用View
协议来定义视图的外观和行为,而不是直接控制布局算法。在开发者创建自定义布局时,他们通常会通过实现View
协议中的body
属性来创建自己的视图,并使用SwiftUI提供的布局容器(例如VStack
、HStack
、ZStack
等)来组织和排列子视图。
虽然Layout
协议不是公开的,但是SwiftUI提供了一些用于自定义布局的公共API,例如GeometryProxy
和ViewModifier
。通过这些API,开发者可以在自定义布局中访问父视图的几何信息,并应用布局修饰符来控制视图的外观和行为。
总的来说,虽然Layout
协议在SwiftUI中起着重要作用,但是大多数开发者不需要直接与它交互。相反,他们可以使用SwiftUI提供的高级API来实现自定义布局和视图。
Layout 协议必须实现的两个方法
算出多少组每组哪些元素
扩展: 计算 [每组] 元素的最大的高度
上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| import SwiftUI
struct TagLayout: Layout { var alignment: Alignment = .trailing var spacing: CGFloat = 0 func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let maxWidth: CGFloat = proposal.width ?? 0 var height: CGFloat = 0 let rows = generateRows(maxWidth, proposal, subviews: subviews) for(index, row) in rows.enumerated() { if index == rows.count - 1 { height += row.maxHeight(proposal) } else { height += spacing + row.maxHeight(proposal) } } return .init(width: maxWidth, height: height) } func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { let maxWidth: CGFloat = bounds.width var origin: CGPoint = bounds.origin let rows = generateRows(maxWidth, proposal, subviews: subviews) for row in rows { let leading: CGFloat = bounds.maxX - maxWidth let trailing = bounds.maxX - row.reduce(CGFloat.zero, { partialResult, view in let width = view.sizeThatFits(proposal).width if view == row.last { return partialResult + width } return partialResult + width + spacing }) let center = (trailing + leading) * 0.5 origin.x = alignment == .leading ? leading : (alignment == .trailing ? trailing : center) for view in row { let viewSize = view.sizeThatFits(proposal) view.place(at: origin, proposal: proposal) origin.x += viewSize.width + spacing } origin.y += row.maxHeight(proposal) + spacing } } }
extension TagLayout { func generateRows(_ maxWidth: CGFloat, _ proposal: ProposedViewSize, subviews: Subviews) -> [[LayoutSubviews.Element]] {
var row: [LayoutSubviews.Element] = [] var rows: [[LayoutSubviews.Element]] = [] var origin = CGRect.zero.origin for view in subviews { let viewSize = view.sizeThatFits(proposal) if (origin.x + viewSize.width + spacing) > maxWidth { print("另起一行") rows.append(row) row.removeAll() origin.x = 0 row.append(view) origin.x += (viewSize.width + spacing) } else { row.append(view) origin.x += (viewSize.width + spacing) } } if !row.isEmpty { rows.append(row) row.removeAll() } return rows } }
extension [LayoutSubviews.Element] { func maxHeight(_ proposal: ProposedViewSize) -> CGFloat { return self.compactMap { view in return view.sizeThatFits(proposal).height }.max() ?? 0 } }
|
动画
修饰符: matchedGeometryEffect
在SwiftUI中,matchedGeometryEffect
修饰符用于在视图之间创建动画效果,以便使两个视图之间的几何形状(例如位置、大小、旋转等)匹配。这在创建复杂的用户界面时非常有用,尤其是在实现页面转换或视图交互时。
当你在两个视图之间使用matchedGeometryEffect
时,SwiftUI会自动计算两个视图之间的差异,并在它们之间创建平滑的动画以使它们匹配。这个修饰符通常与matchedGeometryEffectId
一起使用,以便SwiftUI知道如何匹配两个视图。
例如,你可以在两个不同的界面之间传递相同的标识符来创建动画效果,使得一个视图在界面转换时平滑地转换到另一个视图的位置和大小。
以下是一个简单的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct ContentView: View { @State private var isExpanded = false
var body: some View { VStack { if isExpanded { RoundedRectangle(cornerRadius: 25.0) .matchedGeometryEffect(id: "shape", in: namespace) .frame(width: 200, height: 200) } else { RoundedRectangle(cornerRadius: 10.0) .matchedGeometryEffect(id: "shape", in: namespace) .frame(width: 100, height: 100) } } .onTapGesture { withAnimation { isExpanded.toggle() } } } }
|
在这个示例中,当你点击视图时,它会在大小和位置之间平滑地过渡,因为我们在两个状态之间使用了matchedGeometryEffect
修饰符,并且它们都有相同的标识符。
Demo: TagView
平滑过度动画效果
TagView
运行效果