Please enable Javascript to view the contents

视图旋转后判断触摸点是否在视图内

 ·  ☕ 4 分钟  ·  🍶 Brewmaster · 👀... 阅读

判断某个 point 是否在 View 内部是开发中非常常见的场景,比如最典型的 tap 手势判断 touch point 是否在某个 view 范围内:

图 1 - App 常见页面:欢迎弹窗

图 1 - App 常见页面:欢迎弹窗

在实际的开发中,我遇到了 view 旋转后需要判断点是否在范围内的问题。在这篇文章中我会从简单情况到我真实遇到的问题一步步推导,阐明我是如何解决该问题的

View 未发生旋转

假设 View 没有发生旋转,我们仅需要使用 CGRect 提供的 CGRectContainsPoint 函数就可以进行判断。例如前言中提及的这个欢迎弹窗的 tap 手势判断:

1
2
3
- (BOOL)shouldHandleTap:(CGPoint)touchPoint {
  return CGRectContainsPoint(self.contentView.frame, touchPoint);
}

View 发生旋转

当视图进行了旋转后,出现的一个问题就是 view.frame 不再能准确描述 view 的范围。我们查看 CGRect 的定义:

1
2
3
4
struct CGRect {
  CGPoint origin;
  CGSize size;
};

CGRect 是一个仅由矩形原点以及矩形长宽组成的结构体,这样子的描述方式是无法描述一个旋转过后的矩形的。事实上,旋转后再打印 view.frame 会得到一个面积更大的矩形(前提是旋转角度不是 90 的倍数)。因此不能再使用 self.contentView.frame 进行判断

使用 CGRectApplyAffineTransform 函数可以将参数中的 CGRect 进行变换,返回新的 CGRect,查阅函数的注释:

1
2
3
4
5
6
7
8
9
/* Transform `rect' by `t' and return the result. Since affine transforms do
   not preserve rectangles in general, this function returns the smallest
   rectangle which contains the transformed corner points of `rect'. If `t'
   consists solely of scales, flips and translations, then the returned
   rectangle coincides with the rectangle constructed from the four
   transformed corners. */

CG_EXTERN CGRect CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t)
  CG_AVAILABLE_STARTING(10.4, 2.0);

可知变换后无法保留矩形,实际上是将原矩形的四个角点进行变换,返回能容纳新的四个角点的最小矩形

Frame 与 Bounds

Reference: 图层几何学

图 2 - 变换前的 frame 与 bounds

图 2 - 变换前的 frame 与 bounds

图 3 - 旋转后的 frame 与 bounds

图 3 - 旋转后的 frame 与 bounds

两张图很好地说明了 frame 与 bounds 的区别:

  • frame: 视图的外部坐标(相对坐标),原点与父图层保持一致
  • bounds: 视图的内部坐标(绝对坐标),原点位于视图的左上角

当发生旋转后,frame 相当于调用了 CGRectApplyAffineTransform 函数,转换为能容纳旋转后视图的最小矩形,因此 origin, size 都会发生改变

而 bounds 由于是绝对坐标,相当于整个坐标系都一起发生了旋转,因此不会有变化:

图 4 - bounds 坐标系旋转

图 4 - bounds 坐标系旋转

convertPoint:toView:

convertPoint:toView: 方法是一个坐标系转化方法,能够将给定的 CGPoint 转化到目标 View 的坐标系中

因此我们先将 point 转化到 contentView 中,得到在 contentView.bounds 这个坐标系里点的坐标,再判断转换后的点是否在 contentView.bounds 中,就可以得到正确的结果

1
2
3
4
- (BOOL)shouldHandleTap:(CGPoint)touchPoint {
  CGPoint convertPoint = [self convertPoint:touchPoint toView:self.contentView;
  return CGRectContainsPoint(self.contentView.bounds, convertPoint);
}

View 发生旋转 && frame only

最后一种情况也是开发中我实际遇到的是无法获取对应的 View,仅能获取对应的 frame 以及视图的旋转角度,要判断 touch point 是否在视图范围内

由于无法获取对应的 view,也就没有办法得到 view.bounds,无法将 touchPoint 进行坐标系转化

重新分析我们的问题,假设我们的 touchPoint 在旋转后的矩形内:

图 5 - touchPoint 位于旋转后的矩形

图 5 - touchPoint 位于旋转后的矩形

视图发生旋转,本质上是矩形内所有点围绕中心点发生了旋转,因此我们的 touchPoint 如果逆角度进行旋转可以转回原来的矩形(也就是我们的 frame):

图 6 - touchPoint 转化

图 6 - touchPoint 转化

我们需要对 touchPoint 进行旋转,将 point 绕矩形的中心点反方向旋转对应的角度,再判断新的 point 是否在 frame 内

CGPoint 中没有提供绕点旋转的方法,需要我们自己进行计算

Reference: 计算几何之向量旋转

推广结论:对于任意两个不同点A和B,A绕B旋转θ角度后的坐标为:

(Δxcosθ- Δy * sinθ+ xB, Δycosθ + Δx * sinθ+ yB )

注:xB、yB为B点坐标。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/// 返回绕中心点旋转后的点
/// @param point 旋转前的点
/// @param anchorPoint 旋转中心点
/// @param angle 旋转角度
- (CGPoint)pointRotated:(CGPoint)point
            anchorPoint:(CGPoint)anchorPoint
                  angle:(CGFloat)angle {
  CGFloat x = (point.x - anchorPoint.x) * cos(angle) - (point.y - anchorPoint.y) * sin(angle) + anchorPoint.x;
  CGFloat y = (point.x - anchorPoint.x) * sin(angle) + (point.y - anchorPoint.y) * cos(angle) + anchorPoint.y;
  return CGPointMake(x, y);
}

- (BOOL)shouldHandleTap:(CGPoint)touchPoint
                  frame:(CGRect)frame
                  angle:(CGFloat)angle {
  CGPoint rotatedPoint = [self pointRotated:touchPoint
                                anchorPoint:CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
                                      angle:angle];
  return CGRectContainsPoint(frame, rotatedPoint);
}

在使用该方法时会发现旋转 90° 得到的点坐标有很小的误差,是因为我们使用的圆周率并不是真实值,查看 math.h 文件可以看到 M_PI 的定义:

1
#define M_PI        3.14159265358979323846264338327950288   /* pi             */

但这样的误差实际上非常小,可以忽略不计

Summary

最开始的时候,我考虑的做法是计算旋转过后矩形的四个角的坐标,再计算新的每条边的方程式,用类似高中数学里线性规划的方式去计算 point 是否在范围内:

screenshot: 线性规划常见题型和解法

图 7 - 线性规划例题

图 7 - 线性规划例题

之后想到矩形旋转比较复杂,可以尝试反过来考虑把点进行旋转。伟大的哲学家拉大缸“先生”教导我们:只有回归,才能揭露秘密

screenshot: 疾风华莱士

图 8 - 「只有回归,才能揭露秘密」

图 8 - 「只有回归,才能揭露秘密」

让我们伸出双手,摆出这个 pose,愉快地结束这篇博客~~

分享
您的鼓励是我最大的动力
alipay QR Code
wechat QR Code

KevinAshen
作者
Brewmaster
iOS Developer