图形几何学

57 阅读6分钟

布局

UIView有三个比较重要的布局属性:frameboundscenterCALayer对应地叫做frameboundsposition。为了能清楚区分,图层用了position,视图用了center,但是他们都代表同样的值。

frame代表了图层的外部坐标(也就是在父图层上占据的空间),bounds是内部坐标({0,0}通常是图层的左上角),centerposition都代表了相对于父图层anchorPoint所在的位置。

视图的frameboundscenter属性仅仅是存取方法,当操纵视图的frame,实际上是在改变位于视图下方的CALayerframe,不能够独立于图层之外改变视图的frame

对于视图或者图层来说,frame并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据boundspositiontransform计算而来,所以当其中任何一个值发生改变,frame都会变化,相反,改变frame的值同样会影响到他们当中的值。

记住当对图层做变换的时候,比如旋转或者缩放,frame实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高不再一致了:

锚点

上面我们也说了,视图的center属性和图层的position属性都指定了anchorPoint是用来移动图层的把柄。 默认来说,anchorPoint位于图层的中点,所以图层将会以这个点为中心放置。anchorPoint属性并没有被UIView接口暴露出来,这也是视图的position属性被叫做“center”的原因。但是图层的anchorPoint可以被移动,比如你可以把它置于图层frame的左上角,于是图层的内容将会向右下角的position方向移动,而不是居中了:

和上篇提到的contentsRectcontentsCenter属性类似,anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0,0},右下角是{1,1},因此默认坐标是{0.5,0.5}。anchorPoint可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。

坐标系

和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,如果父图层发生了移动,它的所有子图层也会跟着移动。 这样对于放置图层会更加方便,因为你可以通过移动根图层来将它的子图层作为一个整体来移动,但是有时候你需要知道一个图层的绝对位置,或者是相对于另一个图层的位置,而不是它当前父图层的位置。 CALayer给不同坐标系之间的图层转换提供了一些工具类方法:

- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer;
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形。

翻转的几何结构

常规说来,在iOS上,一个图层的position位于父图层的左上角,但是在Mac OS 上,通常是位于左下角。Core Animation可以通过geometryFlipped属性来适配这两种情况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个BOOL类型。在iOS上通过设置它为YES意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是通常的顶部(它的所有子图层也同理,除非把它们的geometryFlipped属性也设置为YES)。

Z坐标轴

UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。除了我们已经讨论过的positionanchorPoint属性之外,CALayer还有另外两个属性,zPositionanchorPointz,二者都是在Z轴上描述图层位置的浮点类型。 注意这里并没有更深的属性来描述由宽和高做成的bounds了,图层是一个完全扁平的对象,你可以把它们想象成类似于一页二维的坚硬的纸片,用胶水粘成一个空洞,就像三维结构的折纸一样。 zPosition属性在大多数情况下其实并不常用。这里先不介绍,在使用CATransform3D的时候再表,除了做变换之外,zPosition最实用的功能就是改变图层的显示顺序了。 通常,图层是根据它们子图层的sublayers出现的顺序来绘制的。

正如上图所示,首先出现在视图层级绿色的视图被绘制在红色视图的后面。 我们希望在真实的应用中也能显示出绘图的顺序,同样的,如果我们提高绿色视图的 zPosition我们就会发现顺序反了

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *greenView;
@property (nonatomic, weak) IBOutlet UIView *redView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //move the green view zPosition nearer to the camera
    self.greenView.layer.zPosition = 1.0f;
}
@end

其实并不需要增加太多,视图都非常地薄,所以给zPosition提高一个像素就可以让绿色视图前置,当然0.1或者0.0001也能够做到。

Hit Testing

CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:-hitTest:-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。 使用containsPoint判断被点击的图层:使用-containsPoint:这个方法

#import "ViewController.h"

@interface ViewController ()<CALayerDelegate>
@property (nonatomic, strong) CALayer *blueLayer;
@property (nonatomic, strong) UIView *view1;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.view.backgroundColor = [UIColor grayColor];
    _view1 = [[UIView alloc]init];
    _view1.frame = CGRectMake(80,200, 200, 200);
    _view1.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:_view1];
    
    _blueLayer = [CALayer layer];
    _blueLayer.frame = CGRectMake(50, 50, 100, 100);
    _blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    _blueLayer.delegate = self;
    _blueLayer.contentsScale = [UIScreen mainScreen].scale;
    [_view1.layer addSublayer:_blueLayer];
    [_blueLayer display];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
   //get touch position relative to main view
    CGPoint point = [[touches anyObject]locationInView:self.view];
    //convert point to the white layer's coordinates
    point = [self.view1.layer convertPoint:point fromLayer:self.view.layer];
    //get layer using containsPoint:
    if([self.view1.layer containsPoint:point]){
         //convert point to blueLayer’s coordinates
        point = [self.blueLayer convertPoint:point fromLayer:self.view1.layer];
        if([self.blueLayer containsPoint:point]){
            [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
                                        message:nil
                                       delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil] show];
        }else{
            [[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
                                        message:nil
                                       delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil] show];
        }
    }
}

-hitTest:方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子节点图层。这意味着不再需要像使用-containsPoint:那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。具体使用方法如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //get touch position
    CGPoint point = [[touches anyObject]locationInView:self.view];
    //get touched layer
    CALayer *layer = [self.view1.layer hitTest:point];
    //get layer using hitest
    if(layer == self.blueLayer){
        [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }else if(layer == self.view1.layer){
        [[[UIAlertView alloc] initWithTitle:@"Inside White Layer"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }else{
        [[[UIAlertView alloc] initWithTitle:@"Inside gray Layer"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
    
}

注意当调用图层的-hitTest:方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理类似)。之前提到的 zPosition属性可以明显改变屏幕上图层的顺序,但是不能改变事件传递的顺序。这意味着如果改变了图层的z轴顺序,你会发现将不能够检测到最前方的视图点击事件,这是因为被另一个图层遮盖住了,虽然它的zPosition值较小,但是在图层树中的顺序靠前。