第二章里住练,我們介紹了圖層背后的圖片,和一些控制圖層坐標(biāo)和旋轉(zhuǎn)的屬性肯污,在本章里面我們將要看一看在圖層內(nèi)部是如何根據(jù)父圖層和兄弟圖層來控制位置和尺寸的性置。另外我們也會涉及如何管理圖層的幾何結(jié)構(gòu),以及它是如何被自動調(diào)整和自動布局影響混埠。
布局
UIView有三個比較重要的布局屬性:frame怠缸、bounds和center。CALayer對應(yīng)的叫frame钳宪、bounds和position揭北。為了能夠區(qū)分清楚,圖層用了”position“吏颖,視圖用了”center“搔体,但是他們都代表同樣的值。
frame代表了圖層的外部坐標(biāo)(也就是在父圖層上占據(jù)的空間)侦高,bounds是內(nèi)部坐標(biāo)({0嫉柴,0}通常是圖層的左上角)。center和position都代表了相對父圖層anchorPoint所在的位置奉呛。anchorPoint我們后續(xù)再做介紹计螺,現(xiàn)在把它想成是圖層的中心點就好了,如圖所示:
視圖的frame瞧壮、bounds和center屬性僅僅是存取方法登馒。當(dāng)操縱視圖的frame,實際上是在改變位于視圖下方CALayer的frame咆槽,不能夠獨立于圖層之外改變視圖的frame陈轿。
對于視圖或者圖層來說,frame并不是一個非常清晰的屬性。它其實是一個虛擬屬性麦射,是根據(jù)bounds蛾娶,position和transform計算而來,所以當(dāng)其中任何一個值發(fā)生變化潜秋,frame都會改變蛔琅。相反, 改變frame的值同樣會影響到他們當(dāng)中的值峻呛。
記住當(dāng)對圖層做變換的時候罗售,比如旋轉(zhuǎn)或者縮放,frame實際上代表了覆蓋在圖層旋轉(zhuǎn)之后的整個軸對齊的矩形區(qū)域钩述,也就是說frame的寬度可能和bounds的寬度不一致了寨躁。
如圖所示:在旋轉(zhuǎn)完成后一個視圖或者圖層的frame屬性
錨點
之前提到過,視圖的center屬性和圖層的position都指定了anchorPoint相對于父圖層的位置牙勘。圖層的auchorPoint通過position來控制它的frame的位置职恳。你可以認為anchorPoint是用來移動圖層的把柄。
默認來說谜悟,anchorPoint位于圖層的中點话肖,所以圖層將會以這個點為中心進行放置北秽。anchorPoint并沒有被UIView接口暴露出來葡幸,這也是視圖的position屬性被叫做center的原因。但是圖層的anchorPoint可以被移動贺氓,比如你可以把它置于圖層frame的左上角蔚叨,于是圖層的內(nèi)容將會向右下角的position方向移動,而不是劇中辙培。
圖示如下:
和第二章中用到的contentsRect和contentsCenter屬性相類似蔑水,anchorPoint用單位坐標(biāo)來描述,也就是圖層的相對坐標(biāo)**扬蕊。圖層的左上角是{0搀别,0},圖層的右下角是{1尾抑,1}歇父,因此默認的坐標(biāo)為{0.5,0.5}再愈;anchorPoint可以通過指定x和y的值小于0和大于1榜苫,使它放置在圖層范圍之外。
注意在上圖中翎冲,當(dāng)改變anchorPoint值垂睬,position屬性保持固定的值并沒有發(fā)生變化。但是frame卻移動了。
那在什么場合下需要改變anchorPoint的值呢驹饺?既然我們可以隨意改變圖層位置钳枕,那改變anchorPoint不會產(chǎn)生困惑嗎?為了舉例說明赏壹,我們來講一個實用的例子么伯,創(chuàng)建一個模擬鬧鐘的項目。
鐘面和鐘表有四張圖片組成卡儒。
為了簡單說明田柔,我們還是用傳統(tǒng)的方式來裝載和加載圖片,實用四個UIImageView實例骨望。(當(dāng)然你也可以使用普通的視圖硬爆,設(shè)置他們的contents屬性);
鬧鐘的組件通過IB來排列,這些圖片視圖嵌套在一個容器視圖之內(nèi)擎鸠,并且自動布局和自動調(diào)整都被禁止了缀磕。這是因為自動調(diào)整會影響到視圖的frame,當(dāng)視圖旋轉(zhuǎn)時劣光,frame是會發(fā)生改變的袜蚕,這將會導(dǎo)致一些布局上的失靈。
我們用NSTimer來更新鬧鐘绢涡,使用視圖的transform來進行鐘表的旋轉(zhuǎn)(如果你對這個屬性不熟悉牲剃,我們會再后面的章節(jié)進行詳細說明),
代碼如下:
@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];
//start timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
//set initial hand positions
[self tick];
}
- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar ];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate hour hand angle //calculate minute hand angle
CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle);
self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle);
self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle);
}
@end
效果如下:
你也許會認為可以在interface bulider當(dāng)中調(diào)整指針圖片的位置來解決雄可,但其實并不能達到目的凿傅,因為如果不放到鐘面中間的話,同樣不能正確旋轉(zhuǎn)数苫。
也許在圖片的末尾添加一個透明空間也是一個解決方案聪舒,當(dāng)這樣會讓圖片變大,也會消耗更多的內(nèi)存虐急,這樣并不優(yōu)雅箱残。
更好的解決方案是使用anchorPoint屬性,我們在-viewDidLoad方法中添加幾行代碼來給每個鐘指針的anchorPoint做一些平移止吁,
代碼如下:
- (void)viewDidLoad
{
[super viewDidLoad];
// adjust anchor points
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);
// start timer
}
效果如下:
坐標(biāo)系
和視圖一樣被辑,圖層在圖層樹當(dāng)中也是相對于父圖層按層級關(guān)系放置,一個圖層的position依賴于它父圖層的bounds赏殃。如果父圖層發(fā)生了移動敷待,它的所有子圖層也會跟著移動。
這樣對于放置圖層會更加方便仁热。因為你可以通過移動根視圖來將它的子視圖作為一個整體來移動榜揖,但是有時候你需要知道一個圖層的絕對位置勾哩,或者是相對于另一個圖層的位置,而不是它當(dāng)前父圖層的位置举哟。
CALayer給不同坐標(biāo)系之間的圖層轉(zhuǎn)換提供了一些工具方法:
- (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;
這些方法可以把定義在一個圖層坐標(biāo)系下的點或者矩形轉(zhuǎn)換成另一個圖層坐標(biāo)系下的點或者矩形。
翻轉(zhuǎn)的幾何結(jié)構(gòu)
常規(guī)說來妨猩,在ios上潜叛,一個圖層的position位于父圖層的左上角,但是在MaxOS系統(tǒng)上壶硅,通常是位于左下角威兜,CoreAnimation可以通過geometryFilpped屬性來適配這兩種情況,它決定了一個圖層的坐標(biāo)是否相對于父圖層垂直翻轉(zhuǎn)庐椒,是一個bool類型椒舵,在ios上通過設(shè)置它為yes意味著它的子視圖將會被垂直翻轉(zhuǎn),也就是將會沿著底部排版而不是通常的頂部(它的所有子圖層也同理约谈,除非把它們的geometryFlipped屬性也設(shè)置成yes)笔宿。
Z坐標(biāo)軸
和UIView嚴(yán)格的二維坐標(biāo)系不同,CALayer存在于一個三維空間中棱诱,除了我們已經(jīng)討論過的position和anchorPoint之外泼橘,CALayer還有另外兩個屬性zPosition和anchorPointZ,兩者都是在Z軸上描述圖層位置的浮點類型迈勋。
注意在這里并沒有更深的屬性來描述由寬和高做成的bounds了炬灭,圖層是一個完全扁平的對象,你可以把它想象成類似是一頁二維的堅硬的紙片粪躬,用膠水粘成一個空洞担败,就像三維結(jié)構(gòu)的折紙一樣。
zPosition在大多數(shù)情況下其實并不常用镰官。接下去的章節(jié)我們將會涉及到CATransform3D,你會知道如何在三維空間移動和旋轉(zhuǎn)圖層吗货,除了做變換之外泳唠,zPosition最實用的功能就是改變圖層的顯示順序。
通常宙搬,圖層是根據(jù)他們子圖層的subLayers出現(xiàn)的順序來繪制的笨腥,這就是所謂的畫家的算法——就像一個畫家在墻上作畫后被繪制上圖層將會遮蓋住之前的圖層**,但是通過增加圖層的zPosition勇垛,就可以把圖層向相機方向前置脖母,于是它就在所有其他圖層的前面了(或者至少是小于他的zPosition值的圖層的前面)。
這里所謂的“相機”實際上是相對于用戶的視角闲孤,這里和iPhone背后的內(nèi)置相機沒任何關(guān)系谆级。
如下圖所示首先出現(xiàn)在視圖層級綠色的視圖被繪制的紅色視圖后面。
我們希望在真實的應(yīng)用中也能顯示出繪圖的順序,同樣的肥照,如果我們提高的綠色視圖的zPosition脚仔,我們會發(fā)現(xiàn)順序就會反了。其實并不需要增加太多舆绎,視圖都非常的薄鲤脏,所以給zPosition提高一個像素就可以使綠色視圖前置。當(dāng)然0.1或者0.0001也能夠做到吕朵,但是最好不要這樣猎醇,因為浮點類型四舍五入的計算可能會造成一些不便的麻煩。
代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
CALayer *greenLayer = [CALayer layer];
greenLayer.frame = CGRectMake(100, 100, 200, 200);
greenLayer.backgroundColor = [UIColor greenColor].CGColor;
greenLayer.zPosition = 1;
[self.view.layer addSublayer:greenLayer];
CALayer *redLayer = [CALayer layer];
redLayer.frame = CGRectMake(200, 200, 200, 200);
redLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:redLayer];
}
效果如下:
Hit Testing
第一章的時候“圖層樹”證實了最好使用圖層相關(guān)視圖努溃,而不是創(chuàng)建獨立的圖層關(guān)系姑食,其中一個原因就是要處理額外復(fù)雜的觸摸事件。
CALayer并不關(guān)心任何響應(yīng)鏈?zhǔn)录┨常圆荒苤苯犹幚碛|摸事件或者手勢音半,但是他有一系列的方法幫你處理事件:-hitText;和-containsPoint;
-containsPoint:接收一個在本圖層坐標(biāo)系下的CGPoint,如果這個點在圖層的frame范圍之內(nèi)就返回yes贡蓖;在第一章的例子的另一個版本曹鸠,使用-containsPoint方法來判斷到底是白色圖層還是藍色圖層被觸摸了。這需要把觸摸坐標(biāo)轉(zhuǎn)換成每個圖層坐標(biāo)系下的坐標(biāo)斥铺,結(jié)果很不方便彻桃;
代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor grayColor];
_layerView = [[UIView alloc] initWithFrame: CGRectMake(100, 100, 200, 200)];
_layerView.backgroundColor = [UIColor whiteColor];
[self.view addSubview:_layerView];
/*
UIImage *image = [UIImage imageNamed:@"tesla.jpg"];
layerView.layer.contents = (__bridge id)image.CGImage;
layerView.layer.contentsGravity = kCAGravityResizeAspect;
// layerView.layer.contentsScale = image.scale;
layerView.layer.masksToBounds = YES;
layerView.layer.contentsScale = [UIScreen mainScreen].scale;
layerView.layer.contentsRect = CGRectMake(0, 0, 1.3, 1.3);
*/
//添加一個圖層
_layer = [CALayer layer];
_layer.frame = CGRectMake(50, 50, 100, 100);
_layer.backgroundColor = [UIColor blueColor].CGColor;
[_layerView.layer addSublayer:_layer];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
CGPoint point = [[touches anyObject] locationInView:self.view];
point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
if ([self.layerView.layer containsPoint:point]) {
NSLog(@"白色區(qū)域");
CGPoint point2 = [self.layer convertPoint:point fromLayer:self.layerView.layer];
if ([self.layer containsPoint:point2]) {
NSLog(@"藍色區(qū)域");
}
}else{
NSLog(@"空白區(qū)域");
}
}
-hitText:方法同樣接收一個CGPoint類型參數(shù),而不是bool類型晾蜘,他返回圖層本身邻眷,或者包含這個坐標(biāo)點的葉子節(jié)點圖層。這意味著不再需要像使用-containsPoint那樣剔交,人工的在每個子圖層變換或者測試點擊的坐標(biāo)肆饶。如果這個點在最外面圖層的外圍之外,則返回nil岖常,
代碼如下:
//hitTest
CGPoint point = [[touches anyObject] locationInView:self.view];
CALayer *layerHit = [self.view.layer hitTest:point];
if (layerHit == self.view.layer) {
NSLog(@"空白區(qū)域");
}else if (layerHit == self.layer){
NSLog(@"白色區(qū)域 + 藍色區(qū)域");
}else if (layerHit == self.layerView.layer){
NSLog(@"白色區(qū)域");
}
注意當(dāng)調(diào)用圖層的-hitTest方法時驯镊,測算的順序嚴(yán)格依賴于圖層樹中的圖層順序(和UIView處理事件類似),之前提到的zPosition屬性可以明顯改變屏幕上圖層的順序竭鞍,但不能改變事件傳遞的順序板惑。
這意味著如果改變了圖層的z軸順序,你會發(fā)現(xiàn)將不能檢測到最前方的的視圖點擊事件偎快,這是因為被另一個圖層遮蓋住了冯乘。雖然它 的zPosition較小,但是在圖層樹中的順序靠前晒夹,我們將在后面章節(jié)詳細介紹這個問題裆馒。
自動布局
你可能用過UIViewAutoresizingMask類型的一些常量姊氓,應(yīng)用于當(dāng)父視圖改變尺寸的時候,相應(yīng)UIView的frame也跟著更新的場景(通常用于橫豎屏的切換)领追。
在IOS6中他膳,蘋果介紹了自動排版機制,他和自動調(diào)整不同绒窑,并且更加復(fù)雜棕孙。
在MacOS平臺上,CALayer有一個叫做layoutManager的屬性可以通過CALayoutManager協(xié)議和CAConstraintLayoutManager類來實現(xiàn)自動排版機制些膨,但由于某些原因蟀俊,這在IOS上并不適用。
當(dāng)使用視圖的時候订雾,可以充分利用UIView類接口暴露出的UIViewAutoresizingMask和CAConstraintLayoutManager API肢预,但如果想隨意控制CALayer的布局,就需要手工操作洼哎,最簡單的方法就是用CALayerDelegate 如下函數(shù):
-(void)layoutSublayersOfLayer:(CALayer) layer;
當(dāng)圖層的bounds發(fā)生改變或者 圖層的-setNeedsLayout被調(diào)用時烫映,這個函數(shù)就是被執(zhí)行,這使得你可以手動的重新調(diào)整子圖層的大小噩峦,但是不能像UIView的autoresizingMask和contraints屬性做到自適應(yīng)屏幕旋轉(zhuǎn)锭沟。
這也是為什么最好使用視圖而不是單獨的圖層來構(gòu)建應(yīng)用程序的另一個重要原因之一。
總結(jié)
本章涉及了CALayer的集合結(jié)構(gòu)识补,包括他的frame族淮、bounds和position,介紹了三維空間內(nèi)圖層的概念凭涂。以及如何在獨立的圖層內(nèi)響應(yīng)事件祝辣,最后簡單的說明了在ios平臺,Core Animation對自動調(diào)整和自動布局支持的缺乏切油。