布局
UIView
中比較重要的布局屬性為frame
, bounds
, center
。CALayer
中對應的成為frame
, bounds
, position
。
其中frame
表示的是圖層的外部坐標,即圖層在父視圖上占據的空間。center
(position
)代表的是相對于父視圖anchorPoint
所在的位置,anchorPoint
會在后面講到,默認的話就是視圖的中心點。
UIView
的frame
,bounds
,center
僅僅是存取方法,實際訪問和改變的是位于試圖下方的layer
。并且frame
是通過bounds
,position
和transform
所計算出來的。
住當對圖層做變換的時候,比如旋轉或者縮放,實際上代表了覆蓋在圖層旋轉之后的整個軸對齊的矩形區域,也就是說的寬高可能和 bounds 的寬高不再一致了。
錨點(anchorPoint)
前面說過 center
(position
)實際是anchorPoint
相對于父視圖的位置。也就是說可以通過修改anchorPoint
來控制frame
的位置。
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
是{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);
坐標系
和視圖一樣,圖層在父視圖中也是按照層級關系放置的,如果父圖層移動了,那么所有的子視圖也是跟著一起移動。這樣就可以將若干圖層作為一個整體一起移動,但是有時候你需要知道一個圖層的絕對位置,或者是相對于另一個圖層的位置,而不是它當前父圖層的位置,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坐標軸
CALayer
與UIView
的一大主要區別就是CALayer
是存在于三維空間中的,除了position
和anchorPoint
之外,CALayer
還有zPosition
和zAnchorPoint
這兩個屬性,二者都是在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;
}
更改一下藍色圖層的zPosition(這里改變了0.1,理論上哪怕只是改變0.0000001效果也是一樣的,但是由于編譯器的原因,精度過小可能會出現問題,所以最好還是不要寫的太小)。
- (IBAction)zChangeClick:(id)sender {
self.blueLayer.zPosition += 0.1;
}
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
-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
,position
和 bounds
,介紹了三維空間內圖層的概念,以及如何在獨立的圖層內響應事件,最后簡單說明了在iOS平臺,Core Animation
對自動調整和自動布局支持的缺乏。在第四章“視覺效果”當中,我們接著介紹一些圖層外表的特性。