SwiftUI 布局协议 - Part2
前言
在 Part 1 我们探索了布局协议的基础知识,为理解布局是如何工作的打下了坚实的基础。现在,是时候深入研究那些更少提及的功能了,以及如何使用它们来为我们带来便利。
Part 1 - 基础:
- 什么是布局协议
- 视图层次结构的族动态
- 我们的第一个布局实现
- 容器对齐
- 自定义值:LayoutValueKey
- 默认间距
- 布局属性和Spacer()
- 布局缓存
- 高明的伪装者
- 使用AnyLayout 切换布局
- 结语
Part 2 - 高级布局:
- 前言
- 自定义动画
- 双向自定义值
- 避免布局循环和崩溃
- 递归布局
- 布局组合
- 插入两个布局
- 使用绑定参数
- 一个有用的调试工具
- 最后的思考
自定义动画
让我们从写一个圆形布局的视图容器开始吧。我们将它叫做 WheelLayout:
struct ContentView: View {
let colors: [Color] = [.yellow, .orange, .red, .pink, .purple, .blue, .cyan, .green]
var body: some View {
WheelLayout(radius: 130.0, rotation: .zero) {
ForEach(0..<8) { idx in
RoundedRectangle(cornerRadius: 8)
.fill(colors[idx%colors.count].opacity(0.7))
.frame(width: 70, height: 70)
.overlay { Text("/(idx+1)") }
}
}
}
}
struct WheelLayout: Layout {
var radius: CGFloat
var rotation: Angle
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxSize = subviews.map { $0.sizeThatFits(proposal) }.reduce(CGSize.zero) {
return CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height))
}
return CGSize(width: (maxSize.width / 2 + radius) * 2,
height: (maxSize.height / 2 + radius) * 2)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ())
{
let angleStep = (Angle.degrees(360).radians / Double(subviews.count))
for (index, subview) in subviews.enumerated() {
let angle = angleStep * CGFloat(index) + rotation.radians
// Find a vector with an appropriate size and rotation.
var point = CGPoint(x: 0, y: -radius).applying(CGAffineTransform(rotationAngle: angle))
// Shift the vector to the middle of the region.
point.x += bounds.midX
point.y += bounds.midY
// Place the subview.
subview.place(at: point, anchor: .center, proposal: .unspecified)
}
}
}
当布局发生改变时,SwfitUI 提供了内置动画支持。所以如果我们将轮子的旋转值更改为90度,我们将会看见它是如何逐渐的移动到新的位置上:
WheelLayout(radius: radius, rotation: angle) {
// ...
}
Button("Rotate") {
withAnimation(.easeInOut(duration: 2.0)) {
angle = (angle == .zero ? .degrees(90) : .zero)
}
}
这非常好我本可以在此结束动画部分。但是,你已经知道我们的博客不满足于肤浅的表面,所以让我们深层次的看看到底发生了什么。
当我们改变角度时,SwiftUI 会计算好每个视图最初和最终的位置,然后在动画期间内修改它们的位置,从A点到B点成一条直线。起初它似乎没有这样做,但是检查下面这个动画,集中注意观察单个视图,看看它们是如何都跟随直虚线移动的?
你有想过如果动画的角度是从0到360会发生什么吗?给你一分钟... 对!...什么都不会发生。开始的位置和结束的位置是一样的,因此就 SwiftUI 而言,没有动画。
如果这就是你要找的东西,那就太好了,但由于我们将视图围绕一个圆圈放置,如果视图沿着那个假想的圆圈移动不是更有意义吗?好吧,事实证明,这样做非常容易!
我们问题的答案是很幸运的,这个布局协议采用 Animatable 协议!如果你不知道或者忘记这是什么,我建议你查看 SwiftUI 布局协议 - Part 1 的 Animating Shape Paths 部分 。
简单的说,通过添加 animatableData 属性到我们的布局,我们要求 SwiftUI 动画的每一帧重新计算布局。但是,在每个布局传递中,角度都会收到一个内插值。现在 SwiftUI 不会为我们插入位置。相反,它会插入角度值。我们的布局代码将会完成剩下的工作。
struct Wheel: Layout {
// ...
var animatableData: CGFloat {
get { rotation.radians }
set { rotation = .radians(newValue) }
}
// ...
}
添加一个 animatableData 属性足够让我们的视图正确的跟随圆圈。但是,既然我们已经到了这步。。。为什么我们不让半径也可以动画呢?
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get { AnimatablePair(rotation.radians, radius) }
set {
rotation = Angle.radians(newValue.first)
radius = newValue.second
}
}
双向自定义值
在文章的第一部分我们了解到如何使用 LayoutValues 将信息附加到视图,以便它们的代理可以在 placeSubviews 和 sizeThatFits 方法中暴露这些信息。我们的想法是信息从视图流向布局,一会儿将看见这一点是如何被逆转。
本节所解释的想法应谨慎使用,以避免布局循环和 CPU 峰值。在下一部分我将会解释原因和如何避免它。但是不用担心,这并不复杂,你只需要遵循一些准则。
让我们回到轮子的这个例子,假设我们想要视图旋转起来,让它们指向中心。
布局协议只能决定视图位置和它们的建议尺寸,但是不能应用样式、旋转或者其他的效果。如果我们想要这些效果,那么布局应该有一种传达回视图的方式。这时候布局值就变得重要起来,到目前为止,我们已经使用它们传递信息给布局,但只要加上一点创意,我们就可以反向使用它们。
我之前提到过的 LayoutValues 并不局限于传递
- 01-11全球最受赞誉公司揭晓:苹果连续九年第一
- 12-09罗伯特·莫里斯:让黑客真正变黑
- 12-09谁闯入了中国网络?揭秘美国绝密黑客小组TA
- 12-09警示:iOS6 惊现“闪退”BUG
- 03-08消息称微软开发内部AI推理模型,或将成为Op
- 03-08美国法院驳回马斯克请求,未阻止OpenAI转型
- 03-08饿了么成立即时配送算法专家委员会 持续全局
- 03-08长安汽车:预计今年底长安飞行汽车将完成试
- 03-08谷歌推出虚拟试穿、AR美妆新功能