SwiftUI 布局协议 - Part2

浏览:
字体:
发布时间:2023-01-09 14:22:03
来源:Swift社区

前言

在 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 并不局限于传递 

>更多相关文章
24小时热门资讯
24小时回复排行
资讯 | QQ | 安全 | 编程 | 数据库 | 系统 | 网络 | 考试 | 站长 | 关于东联 | 安全雇佣 | 搞笑视频大全 | 微信学院 | 视频课程 |
关于我们 | 联系我们 | 广告服务 | 免责申明 | 作品发布 | 网站地图 | 官方微博 | 技术培训
Copyright © 2007 - 2024 Vm888.Com. All Rights Reserved
粤公网安备 44060402001498号 粤ICP备19097316号 请遵循相关法律法规
');})();