同步不同坐标空间中超视图和单个视图的变换矩阵

Synchronize transform matrix of superview and individual views in different coordinate space

提问人:User 提问时间:12/27/2016 最后编辑:User 更新时间:4/30/2017 访问量:690

问:

给定以下视图层次结构:

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

任务:

和的交叉必须始终对齐,给定全局转换。更多详细信息,请参阅“要求”部分。superviewsubview

上下文:

上面的视图层次结构属于图表。为了提供最大的灵活性,它允许以 3 种不同的方式呈现图表点和相关内容:

  1. 在图表的基视图 () 方法中绘图。superviewdraw

  2. 向 添加子视图。 在缩放/平移时进行转换,并自动转换其子视图。subviewsubview

  3. 将子视图添加到 的同级。为简单起见,并且因为它与问题无关,因此未在视图层次结构中显示。这里只提一下,做一个概述。此方法与 2. 的区别在于,此处的视图没有转换,因此留给内容的实现来“手动”更新所有子项的转换。subview

最大的灵活性!但随之而来的是实施起来有点棘手的成本。具体来说,第 2 点。

目前,我基本上通过单独处理核心图形绘制的转换来工作缩放/平移,但这会导致冗余和容易出错,例如用于边界检查的重复代码等。superviewsubview

所以现在我正在尝试重构它,以使用一个全局矩阵来存储所有转换并从中派生所有内容。将全局矩阵应用于用于绘制的坐标是微不足道的,但根据下一节中列出的要求,推导 的矩阵就不重要了。superviewsubview

我在视图层次结构部分提到“十字”,因为这是我在我的 Playground 中使用的,用作一个图表点的简化表示(带有 x/y 指南)(您可以向下滚动查看图像和要点)。

要求:

  1. 可以缩放和平移内容。
  2. 十字架始终保持完美对齐。
  3. subview的子视图,即不能触及交叉线视图(例如,对它们应用转换)——唯一可以修改的就是 的转换。subview
  4. 缩放和平移变换仅存储在全局矩阵中。matrix
  5. matrix然后用于计算绘制的十字的坐标(平凡),以及(不是平凡 - 这个问题的原因)的变换矩阵。superviewsubview
    • 由于似乎不可能从全局矩阵中唯一地推导出 的矩阵,因此允许在变量中存储其他数据,然后与全局矩阵一起使用来计算 的矩阵。subviewsubview
  6. 在缩放/平移过程中,大小/原点可能会发生变化。这样做的原因是 y 轴的标签可以有不同的长度,并且图表需要根据标签占用的空间动态调整内容大小(在缩放和平移期间)。container
  7. 当然,当大小发生变化时,域-屏幕坐标的比例也必须相应变化,使得完整的原始可见域继续包含在中。例如,如果我在宽度为 500pt 的容器框架中显示一个带有域 [0, 10] 的 x 轴,即将域点转换为屏幕坐标的比率为 ,并将容器宽度缩小到 250,现在我的 [0, 10] 域必须适应这个新宽度,比率为 25。containercontainer500/10=50
  8. 它还必须适用于多个交叉(同时)和每个交叉的任意域位置。这应该通过解决 1-7 自动发生,但为了完整性而提及它。

我做了什么:

以下是我为更好地理解问题而做的分步游乐场:

第 1 步(有效):

构建开头描述的层次结构,只显示在(编程化)缩放和平移期间必须保持对齐的交叉。满足要求 1、2、3、4 和 5:

enter image description here 与游乐场的要点。

这里的特殊性:

  • 为了简单起见,我跳过了视图。 是 的直接子视图。containersubviewsuperview
  • subview具有相同的大小(当然在缩放之前),也是为了保持简单。superview
  • 我将锚点设置为原点 (0, 0),这似乎是与全局矩阵同步所必需的。subview
  • 必须记住用于锚点更改的翻译,以便将其与全局矩阵一起再次应用。否则,它将被覆盖。为此,我使用变量 .这属于我在要求 5 下的项目符号中想到的额外数据。subviewAnchorTranslation

好的,正如你所看到的,这里一切正常。是时候尝试下一步了。

第 2 步(有效):

第 1 步 Playground 的副本,并进行了修改:

  • 添加了视图,类似于开头描述的视图层次结构。container
  • 为了使 ,现在是 的子视图继续显示在同一位置,必须将其移动到顶部并离开 。subviewcontainer-container.origin
  • 现在,缩放和平移调用与更改容器帧位置/大小的调用随机交错。

十字架继续保持同步。满足要求:全部来自第 1 步 + 要求 6。enter image description here Gist 与游乐场

第 3 步(不起作用):

到目前为止,我一直在使用从 0 开始的屏幕范围(可见 playground 结果的左侧)。这意味着它没有实现它包含范围的功能,即要求 7。为了满足这一点,在比率计算中必须包括原点。containercontainer

现在还必须缩放,以便在正确的位置安装/显示十字架。这增加了第二个变量(第一个是),我称之为 ,包含这种缩放,它必须包含在矩阵计算中。subviewcontainersubviewAnchorTranslationcontentScalingFactorsubview

在这里,我做了多次实验,都失败了。在当前状态下,从相同的帧开始,当帧发生变化时,其帧被调整+缩放。此外,现在在容器内,即它的原点现在是 的原点而不是 的原点,我必须设置更新它的锚点,使原点不在 (0,0) 而是 (-x,-y),是 x 和 y 的坐标 ' 原点,这样它继续相对于 的原点定位。每次更改其原点时更新此锚点似乎是合乎逻辑的,因为这会将相对位置从 的原点更改为 的原点。subviewcontainercontainersubviewcontainersuperviewcontainersubviewsuperviewcontainercontentsuperview

我为此上传了代码 - 在这种情况下,一个完整的 iOS 项目,而不仅仅是一个游乐场(我最初认为它正在工作,并想使用实际手势进行测试)。在我正在处理的实际项目中,转换效果更好,但我找不到区别。无论如何,它效果不佳,在某些时候总是有小的偏移量,并且点/交叉不同步。

enter image description here Github 项目

好的,我该如何解决这个问题,以便满足所有条件。十字架必须保持同步,连续缩放/平移并改变两者之间的帧。container

IOS 数学 矩阵 UIView 转换

评论

0赞 Zapko 1/14/2017
嘿,lxx,不清楚缩放应该做什么。它应该更改子视图大小还是域大小?
0赞 Zapko 1/14/2017
根据我从上下文中了解到的情况,平移应该会改变黑叉的位置,但缩放不应该。这是对的吗?
0赞 Zapko 1/14/2017
它的行为是否类似于您在 mac OS 上从系统屏幕缩放中获得的行为?除了缩放的内容显示在容器中。
0赞 User 1/14/2017
@Zapko缩放不会更改域大小。[0, 10] 域继续为 [0, 10]。当然,它会更改可用的屏幕空间,即子视图大小。
0赞 User 1/14/2017
不不不,平移和缩放应该会改变黑色十字和红色,并且两者都应该保持完全对齐。就这样。只需下载示例 gist 和项目,您就会看到问题;)

答:

0赞 SwiftArchitect 1/13/2017 #1

目前的答案允许任意转换子层次结构中的任何视图。它跟踪变换,只是转换变换后的点,从而回答了以下问题:

位于另一个视图坐标系中的子视图中的点的坐标是什么,无论该子视图已转换多少

为了将项与剪裁容器分离并提供通用答案,我建议在概念上将它们放在同一级别,并在视觉上以不同的顺序(†)放置它们:

Modified hierarchy

使用通用的超级视图

要应用从子项项的滚动、缩放或任何其他转换,请通过公共监督视图(在本例中称为协调器)。

该方法与此 Stack Overflow 答案非常相似,其中两个以不同的速度滚动。UIScrollView

请注意,红色发际线和黑色发际线是如何重叠的,无论层次结构中任何和所有视图(包括容器视图)的位置、滚动和转换如何。

Demo - all crosses match
↻ 回放动画


法典

坐标转换

为清楚起见,在视图的坐标系中使用任意点 (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()
}

(†)我已将 SuperviewSubview 分别重命名为 ParentChild

评论

0赞 User 1/13/2017
嗯,这如何解决问题?它们基本上已经解耦了,因为我没有将矩阵直接应用于 superview,而只是将选定元素(如黑叉)的核心图形应用于
0赞 User 1/13/2017
也就是说,在绘制中,我手动将矩阵应用于线点并绘制这些转换点。Superview 不受影响。
0赞 SwiftArchitect 1/13/2017
思考 - 我理解正确吗,你试图让父母中的十字架跟随一个给定的点,在孩子的观点中改变?
0赞 SwiftArchitect 1/13/2017
说明协调器用于将点从一个坐标系转换为另一个坐标系。
0赞 User 1/13/2017
你读过我帖子的前 10 行吗?转换是整个问题。层次结构无关紧要,因为上述转换不适用于监督视图本身。我和你的“协调员”也有同样的问题。请阅读它。