圖層幾何學 -- iOS Core Animation 系列二
《圖層樹和寄宿圖 -- iOS Core Animation 系列一》介紹了圖層的基礎知識和一些屬性方法。這篇主要內容是學習下圖層在父圖層上怎麼控制位置和尺寸的。
1.佈局
首先看一張例圖:
對於圖上的frame
、bounds
、center
、postion
的概念我就不贅述了。如果有不明白的自行搜尋下了解一下。
frame
代表了圖層的外部座標(也就是在父圖層上佔據的空間),bounds
是內部座標({0, 0}
通常是圖層的左上角),center
和position
都代表了相對於父圖層anchorPoint
所在的位置
檢視的frame
、bounds
、center
屬性僅僅是存取方法,當操縱檢視的frame
CALayer
的frame
, 不能獨立於圖層之外改變檢視的frame
.
如果對圖層做了變換,比如旋轉縮放等。frame
的值實際指的是圖層旋轉之後整個軸對齊的矩形區域。此時frame
的寬高可能和bounds
的寬高不一致:
2.錨點
預設來說,anchorPoint
位於圖層的中點。這個屬性沒有被UIView
直接暴露出來。但是圖層的anchorPoint
可以被移動。我們可以把anchorPoint
置於圖層frame
的左上角。將會出現下圖右側的情況:
注意上圖,改變anchorPoint
後position
的值並沒變。
和系列一中提到的contentsRect
類似,anchorPoint
{0.5, 0.5}
)。可以通過指定x和y值小於0或者大於1,使它放置在圖層範圍之外。
2.1 示例
為了學習這個anchorPoint
屬性,下面建立一個鬧鐘的示例demo。資原始檔我是從原文上截圖下來的
建立4個UIImageView
並設定好約束(都是居中顯示)。
我們用NSTimer來更新鬧鐘,使用檢視的transform屬性來旋轉鐘錶。程式碼如下:
@interface ViewController () @property (nonatomic, weak) IBOutlet UIImageView *hourHand; @property (nonatomic, weak) IBOutlet UIImageView *minuteHand; @property (nonatomic, weak) IBOutlet UIImageView *secondHand; @property (nonatomic, weak) NSTimer *timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; [self tick]; } - (void)tick { //獲取對應的hours mins seconds NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; NSUInteger units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond; NSDateComponents *components = [calendar components:units fromDate:[NSDate date]]; CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0; CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0; CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0; //旋轉對應的檢視 self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle); self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle); self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle); }
執行專案如下圖:
除了指標圖片的位置,其他的都正常。可能這時候我們最先想到的方法,是調整對應圖片的位置來解決。但是這樣的話,你可以試試,並不能解決問題。不用賣關子了。這時候就是要用到anchorPoint
的時候。處理程式碼如下:
// 在viewdidload中新增
self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
執行完美。
3. 座標系
眾所周知,一個圖層的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;
常規來說,一個圖層的postion
位於父圖層的左上角,但在 Mac OS 中,通常位於左下角。
3.1 z座標軸
和UIView
的二維座標不同,CALayer
存在於一個三維空間中,它還提供了zPostion
和anchorPointz
屬性。zPosition
屬性大多數不常用,除了三維動畫之外,它最實用的功能是可以改變圖層的顯示順序。
3.2 zPosition演示程式碼
我們演示下改變zPosition
會怎麼改變檢視的顯示順序。首先我在SB中設定兩個檢視,如下圖:
如果我們不做任何操作,執行後,兩個檢視顯示的順序就是我們現在設定的這樣。但是假如我們對yellowView
設定zPosition
,哪怕很小的值,都會發現顯示的順序反了。
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *cyanView;
@property (weak, nonatomic) IBOutlet UIView *yellowView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.yellowView.layer.zPosition = 1.f;
}
現在的顯示效果如下:
雖說圖層基本沒有厚度,但是我們也儘量不要設定zPosition = 0.01f
之類的。因為浮點型別的四捨五入可能導致難以察覺的麻煩。
4. Hit Testing
雖說CALayer
不關心響應鏈事件,但是它提供了一些方法讓我們處理事件-containsPoint:
和-hitTest:
。
4.1 -containsPoint:
-containsPoint:
接受一個在本圖層座標系下的CGPoint
,如果這個點在圖層frame
範圍內就返回YES
.我們可以使用這個方法判斷是哪個圖層被觸摸了。
4.1.1 containsPoint 示例
程式碼如下:
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *layerView;
@property (nonatomic, strong) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(20.f, 20.f, 100.f, 100.f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.layerView.layer addSublayer:self.blueLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 獲取觸控點
CGPoint point = [[touches anyObject] locationInView:self.view];
// 轉換觸控點在layerView的圖層的位置
point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
// 判斷是否包含在layerview裡面
if ([self.layerView.layer containsPoint:point]) {
point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
if ([self.blueLayer containsPoint:point]) {
NSLog(@"點選藍色圖層");
} else {
NSLog(@"點選了白色圖層");
}
}
}
執行點選可以在控制檯看到NSLog
的輸出資訊。
4.2. -hitTest:
-hitTest:
方法同樣接受一個CGPoint
引數,但是返回的是圖層本身,而不是BOOL
型別。這使我們不用像-containsPoint:
一樣每個子圖層去測試點選的座標。如果這個點是在最外面的圖層,則返回nil
。
4.2.1 hitTest示例
把上面-containsPoint:
示例的程式碼下面的部分修改一下即可:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 獲取點選點
CGPoint point = [[touches anyObject] locationInView:self.view];
// 獲取這個點所在的圖層
CALayer *layer = [self.layerView.layer hitTest:point];
if (layer == self.blueLayer) {
NSLog(@"點選藍色圖層");
} else if (layer == self.layerView.layer) {
NSLog(@"點選了白色圖層");
}
}
嘗試修改self.layerView
的zPosition
,會有不同的結果。有興趣的可以自己測試一下。