提问人:User 提问时间:12/27/2016 最后编辑:User 更新时间:4/30/2017 访问量:690
同步不同坐标空间中超视图和单个视图的变换矩阵
Synchronize transform matrix of superview and individual views in different coordinate space
问:
给定以下视图层次结构:
root (e.g. view of a view controller)
|_superview: A view where we will draw a cross using core graphics
|_container: Clips subview
|_subview: A view where we will show a cross adding subviews, which has to align perfectly with the cross drawn in superview
|_horizontal line of cross
|_vertical line of cross
任务:
和的交叉必须始终对齐,给定全局转换。更多详细信息,请参阅“要求”部分。superview
subview
上下文:
上面的视图层次结构属于图表。为了提供最大的灵活性,它允许以 3 种不同的方式呈现图表点和相关内容:
在图表的基视图 () 方法中绘图。
superview
draw
向 添加子视图。 在缩放/平移时进行转换,并自动转换其子视图。
subview
subview
将子视图添加到 的同级。为简单起见,并且因为它与问题无关,因此未在视图层次结构中显示。这里只提一下,做一个概述。此方法与 2. 的区别在于,此处的视图没有转换,因此留给内容的实现来“手动”更新所有子项的转换。
subview
最大的灵活性!但随之而来的是实施起来有点棘手的成本。具体来说,第 2 点。
目前,我基本上通过单独处理核心图形绘制的转换来工作缩放/平移,但这会导致冗余和容易出错,例如用于边界检查的重复代码等。superview
subview
所以现在我正在尝试重构它,以使用一个全局矩阵来存储所有转换并从中派生所有内容。将全局矩阵应用于用于绘制的坐标是微不足道的,但根据下一节中列出的要求,推导 的矩阵就不重要了。superview
subview
我在视图层次结构部分提到“十字”,因为这是我在我的 Playground 中使用的,用作一个图表点的简化表示(带有 x/y 指南)(您可以向下滚动查看图像和要点)。
要求:
- 可以缩放和平移内容。
- 十字架始终保持完美对齐。
subview
的子视图,即不能触及交叉线视图(例如,对它们应用转换)——唯一可以修改的就是 的转换。subview
- 缩放和平移变换仅存储在全局矩阵中。
matrix
matrix
然后用于计算绘制的十字的坐标(平凡),以及(不是平凡 - 这个问题的原因)的变换矩阵。superview
subview
- 由于似乎不可能从全局矩阵中唯一地推导出 的矩阵,因此允许在变量中存储其他数据,然后与全局矩阵一起使用来计算 的矩阵。
subview
subview
- 由于似乎不可能从全局矩阵中唯一地推导出 的矩阵,因此允许在变量中存储其他数据,然后与全局矩阵一起使用来计算 的矩阵。
- 在缩放/平移过程中,大小/原点可能会发生变化。这样做的原因是 y 轴的标签可以有不同的长度,并且图表需要根据标签占用的空间动态调整内容大小(在缩放和平移期间)。
container
- 当然,当大小发生变化时,域-屏幕坐标的比例也必须相应变化,使得完整的原始可见域继续包含在中。例如,如果我在宽度为 500pt 的容器框架中显示一个带有域 [0, 10] 的 x 轴,即将域点转换为屏幕坐标的比率为 ,并将容器宽度缩小到 250,现在我的 [0, 10] 域必须适应这个新宽度,比率为 25。
container
container
500/10=50
- 它还必须适用于多个交叉(同时)和每个交叉的任意域位置。这应该通过解决 1-7 自动发生,但为了完整性而提及它。
我做了什么:
以下是我为更好地理解问题而做的分步游乐场:
第 1 步(有效):
构建开头描述的层次结构,只显示在(编程化)缩放和平移期间必须保持对齐的交叉。满足要求 1、2、3、4 和 5:
这里的特殊性:
- 为了简单起见,我跳过了视图。 是 的直接子视图。
container
subview
superview
subview
具有相同的大小(当然在缩放之前),也是为了保持简单。superview
- 我将锚点设置为原点 (0, 0),这似乎是与全局矩阵同步所必需的。
subview
- 必须记住用于锚点更改的翻译,以便将其与全局矩阵一起再次应用。否则,它将被覆盖。为此,我使用变量 .这属于我在要求 5 下的项目符号中想到的额外数据。
subviewAnchorTranslation
好的,正如你所看到的,这里一切正常。是时候尝试下一步了。
第 2 步(有效):
第 1 步 Playground 的副本,并进行了修改:
- 添加了视图,类似于开头描述的视图层次结构。
container
- 为了使 ,现在是 的子视图继续显示在同一位置,必须将其移动到顶部并离开 。
subview
container
-container.origin
- 现在,缩放和平移调用与更改容器帧位置/大小的调用随机交错。
十字架继续保持同步。满足要求:全部来自第 1 步 + 要求 6。 Gist 与游乐场
第 3 步(不起作用):
到目前为止,我一直在使用从 0 开始的屏幕范围(可见 playground 结果的左侧)。这意味着它没有实现它包含范围的功能,即要求 7。为了满足这一点,在比率计算中必须包括原点。container
container
现在还必须缩放,以便在正确的位置安装/显示十字架。这增加了第二个变量(第一个是),我称之为 ,包含这种缩放,它必须包含在矩阵计算中。subview
container
subviewAnchorTranslation
contentScalingFactor
subview
在这里,我做了多次实验,都失败了。在当前状态下,从相同的帧开始,当帧发生变化时,其帧被调整+缩放。此外,现在在容器内,即它的原点现在是 的原点而不是 的原点,我必须设置更新它的锚点,使原点不在 (0,0) 而是 (-x,-y),是 x 和 y 的坐标 ' 原点,这样它继续相对于 的原点定位。每次更改其原点时更新此锚点似乎是合乎逻辑的,因为这会将相对位置从 的原点更改为 的原点。subview
container
container
subview
container
superview
container
subview
superview
container
content
superview
我为此上传了代码 - 在这种情况下,一个完整的 iOS 项目,而不仅仅是一个游乐场(我最初认为它正在工作,并想使用实际手势进行测试)。在我正在处理的实际项目中,转换效果更好,但我找不到区别。无论如何,它效果不佳,在某些时候总是有小的偏移量,并且点/交叉不同步。
好的,我该如何解决这个问题,以便满足所有条件。十字架必须保持同步,连续缩放/平移并改变两者之间的帧。container
答:
目前的答案允许任意转换子层次结构中的任何视图。它不跟踪变换,只是转换变换后的点,从而回答了以下问题:
位于另一个视图坐标系中的子视图中的点的坐标是什么,无论该子视图已转换多少。
为了将父项与剪裁容器分离并提供通用答案,我建议在概念上将它们放在同一级别,并在视觉上以不同的顺序(†)放置它们:
使用通用的超级视图
要应用从子项到父项的滚动、缩放或任何其他转换,请通过公共监督视图(在本例中称为协调器)。
该方法与此 Stack Overflow 答案非常相似,其中两个以不同的速度滚动。UIScrollView
请注意,红色发际线和黑色发际线是如何重叠的,无论子
层次结构中任何和所有视图(包括容器
视图)的位置、滚动和转换如何。
法典
坐标转换
为清楚起见,在子视图的坐标系中使用任意点 (50,50)(有效绘制该点的位置),并在父视图系统中将其转换为如下所示:
func coordinate() {
let transfer = theChild.convert(CGPoint(x:50, y:50), to: coordinator)
let final = coordinator.convert(transfer, to: theParent)
theParent.transformed = final
theParent.setNeedsDisplay()
}
缩放和翻译容器
func zoom(center: CGPoint, delta: CGPoint) {
theContainer.transform = theContainer.transform.scaledBy(x: delta.x, y: delta.y)
coordinate()
}
func translate(delta: CGPoint) {
theContainer.transform = theContainer.transform.translatedBy(x: delta.x, y: delta.y)
coordinate()
}
(†)我已将 Superview 和 Subview 分别重命名为 Parent 和 Child。
评论