Core Animation 第三章 圖層幾何

往期回顧:
序章
第一章
第二章
項目中使用的代碼

布局


UIView中比較重要的布局屬性為frame, bounds, centerCALayer中對應的成為frame, bounds, position
其中frame表示的是圖層的外部坐標,即圖層在父視圖上占據的空間。center(position)代表的是相對于父視圖anchorPoint所在的位置,anchorPoint會在后面講到,默認的話就是視圖的中心點。

View和Layer的坐標關系.png

UIViewframe, bounds, center僅僅是存取方法,實際訪問和改變的是位于試圖下方的layer。并且 frame 是通過 bounds, positiontransform 所計算出來的。

住當對圖層做變換的時候,比如旋轉或者縮放,實際上代表了覆蓋在圖層旋轉之后的整個軸對齊的矩形區域,也就是說的寬高可能和 bounds 的寬高不再一致了。

旋轉后視圖的frame與bounds.png

錨點(anchorPoint)


前面說過 center(position)實際是anchorPoint相對于父視圖的位置。也就是說可以通過修改anchorPoint來控制frame的位置。

修改anchorPoint后的frame對比.png

anchorPoint使用的是單位坐標,圖層左上角為{0, 0},右下角為{1, 1},默認值為{0.5, 0.5}。當然,anchorPoint也可以位于圖層之外,也就是取值可以小于0或者大于1;

下面我們來做一個時鐘:


我們可以啟用一個NSTimer,然后每隔一秒獲取一次本地時間,并且對應的使用transform來旋轉指針(transform屬性會在后面講到)。

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *hourImageView;
@property (weak, nonatomic) IBOutlet UIImageView *minuteImageView;
@property (weak, nonatomic) IBOutlet UIImageView *secondImageView;
@property (weak, nonatomic) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timeTick) userInfo:nil repeats:YES];
    //初始化指針的位置
    [self timeTick];
}

- (void)timeTick {
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierChinese];
    NSUInteger units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;
    NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
    CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
    CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
    CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
    self.hourImageView.transform = CGAffineTransformMakeRotation(hourAngle);
    self.minuteImageView.transform = CGAffineTransformMakeRotation(minuteAngle);
    self.secondImageView.transform = CGAffineTransformMakeRotation(secondAngle);
}
@end
沒有修改anchorPoint的時鐘.png

可以看到旋轉的支點以及指針的位置都十分奇怪,因為我們圖層默認的anchorPoint是{0.5, 0.5},但是現實中時鐘的指針卻很少有以中心為支點旋轉的,所以我們可以修改一下anchorPoint來調整我們指針的位置和旋轉的支點。在viewDidLoad中添加如下代碼

    self.hourImageView.layer.anchorPoint = CGPointMake(0.5, 0.7);
    self.minuteImageView.layer.anchorPoint = CGPointMake(0.5, 0.7);
    self.secondImageView.layer.anchorPoint = CGPointMake(0.5, 0.7);
調整anchorPoint后的時鐘.png

坐標系


和視圖一樣,圖層在父視圖中也是按照層級關系放置的,如果父圖層移動了,那么所有的子視圖也是跟著一起移動。這樣就可以將若干圖層作為一個整體一起移動,但是有時候你需要知道一個圖層的絕對位置,或者是相對于另一個圖層的位置,而不是它當前父圖層的位置,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;

翻轉的幾何結構

我們都知道圖層的原點是左上角,但是在macOS中視圖原點卻是左下角,Core Animation可以通過 geometryFlipped屬性來適配這兩種情況,他決定了一個圖層以及其子圖層是否垂直翻轉。

Z坐標軸

CALayerUIView的一大主要區別就是CALayer是存在于三維空間中的,除了positionanchorPoint之外,CALayer還有zPositionzAnchorPoint這兩個屬性,二者都是在Z軸上描述圖層位置的浮點類型。這里并沒有引申出z軸方向上的bounds等屬性,因為圖層并沒有厚度。寫一個簡單的例子,下面有紅色和藍色兩個圖層,紅色圖層在藍色圖層上方,我們稍微改變一下藍色圖層的zPosition,可以看到藍色圖層就會跑到紅色圖層上方

@interface ZAxleController ()
@property (weak, nonatomic) CALayer *blueLayer;
@property (weak, nonatomic) CALayer *redLayer;
@end

@implementation ZAxleController

- (void)viewDidLoad {
    [super viewDidLoad];
    CALayer *blueLayer = [CALayer layer];
    blueLayer.frame = CGRectMake(100, 50, 100, 100);
    blueLayer.backgroundColor = [UIColor colorWithRed:0.00 green:0.55 blue:1.00 alpha:1.00].CGColor;
    [self.view.layer addSublayer:blueLayer];
    self.blueLayer = blueLayer;
    
    CALayer *redLayer = [CALayer layer];
    redLayer.frame = CGRectMake(150, 100, 100, 100);
    redLayer.backgroundColor = [UIColor colorWithRed:1.00 green:0.45 blue:0.43 alpha:1.00].CGColor;
    [self.view.layer addSublayer:redLayer];
    self.redLayer = redLayer;
}
紅色圖層在藍色圖層上方.png

更改一下藍色圖層的zPosition(這里改變了0.1,理論上哪怕只是改變0.0000001效果也是一樣的,但是由于編譯器的原因,精度過小可能會出現問題,所以最好還是不要寫的太小)。

- (IBAction)zChangeClick:(id)sender {
    self.blueLayer.zPosition += 0.1;
}
改變了藍色圖層的zPosition.png

Hit Testing


CALayer 并不關心任何響應鏈事件,所以不能直接處理觸摸事件或者手勢。但是它有一系列的方法幫你處理事件:-containsPoint:-hitTest:
-containsPoint:傳入一個點,如果這個點在當前圖層的frame范圍內,就返回YES,比如下面的例子,我們在-touchesBegan: withEvent:中判斷點擊是否位于藍色圖層內部。

@interface HitTestingController ()
@property (weak, nonatomic) IBOutlet UIView *layerView;
@property (weak, nonatomic) CALayer *blueLayer;
@end

@implementation HitTestingController

- (void)viewDidLoad {
    [super viewDidLoad];
    CALayer *blueLayer = [CALayer layer];
    blueLayer.frame = CGRectMake(50, 50, 100, 100);
    blueLayer.backgroundColor = [UIColor colorWithRed:0.00 green:0.55 blue:1.00 alpha:1.00].CGColor;
    [self.layerView.layer addSublayer:blueLayer];
    self.blueLayer = blueLayer;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    CGPoint point = [touches.anyObject locationInView:self.view];
//    轉換為layerView中的坐標
    point = [self.blueLayer convertPoint:point fromLayer:self.view.layer];
    NSString *message = @"";
    if ([self.blueLayer containsPoint:point]) {
        message = @"點擊位于藍色視圖中";
    } else {
        message = @"點擊位于藍色視圖外";
    }
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:alertController animated:YES completion:nil];
    
}
@end
藍色圖層被點擊.png

-hitTest:同樣接收一個CGPoint類型的參數,返回圖層本身或者包含這個節點的子圖層,如果這個點在圖層之外則返回nil。所以上面的代碼也可以寫成這個樣子。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //-hitTest:
    CGPoint point = [touches.anyObject locationInView:self.view];
    CALayer *layer = [self.layerView.layer hitTest:point];
    NSString *message = @"";
    if (layer == self.blueLayer) {
        message = @"點擊位于藍色視圖中";
    } else {
        message = @"點擊位于藍色視圖外";
    }
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"確定" style:UIAlertActionStyleDefault handler:nil]];
    [self presentViewController:alertController animated:YES completion:nil];
}
  • 需要注意的是雖然我們可以通過zPosition改變我們看到的圖層的位置,但實際上響應鏈的順序并不會改變。

自動布局

iOS6中蘋果引入了自動布局,在使用UIView的時候你可以使用NSLayoutConstraint的API使用自動布局,但如果想要隨意控制CALayer的布局就需要手動操作。最簡單的方法就是使用CALayerDelegate的方法:

- (void)layoutSublayersOfLayer:(CALayer *)layer;

當圖層的bounds發生變化或者調用-setNeedsLayout的時候,這個方法就會被執行,你可以在這里手動的調整子圖層的大小與位置。

總結


本章涉及了CALayer 的幾何結構,包括它的 frame ,positionbounds ,介紹了三維空間內圖層的概念,以及如何在獨立的圖層內響應事件,最后簡單說明了在iOS平臺,Core Animation對自動調整和自動布局支持的缺乏。在第四章“視覺效果”當中,我們接著介紹一些圖層外表的特性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容