目錄
- 圖層幾何學(xué)
- 布局
- 錨點
- 坐標(biāo)系
- Hit Testing
- 自動布局
一 圖層幾何學(xué)
在這一章中除盏,我們將要看一看圖層內(nèi)部是如何根據(jù)父圖層和兄弟圖層來控制位置和尺寸的味混。另外我們也會涉及如何管理圖層的幾何結(jié)構(gòu),以及它是如何被自動調(diào)整和自動布局影響的谬莹。
1.1 布局
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的寬高不再一致了
1.2 錨點
視圖的center
屬性和圖層的position
屬性都指定了anchorPoint
相對于父圖層的位置漏隐。圖層的anchorPoint
通過position
來控制它的frame
的位置朋譬,你可以認(rèn)為anchorPoint是用來移動圖層的把柄
。
默認(rèn)來說兴垦,anchorPoint
位于圖層的中點徙赢,所以圖層的將會以這個點為中心放置。anchorPoint
屬性并沒有被UIView接口暴露出來探越,這也是視圖的position
屬性被叫做center
的原因狡赐。但是圖層的anchorPoint
可以被移動,比如你可以把它置于圖層frame的左上角钦幔,于是圖層的內(nèi)容將會向右下角的position方向移動(圖3.3)枕屉,而不是居中了。
anchorPoint
用單位坐標(biāo)來描述鲤氢,也就是圖層的相對坐標(biāo)
搀擂,圖層左上角是{0, 0},右下角是{1, 1}卷玉,因此默認(rèn)坐標(biāo)是{0.5, 0.5}
哨颂。anchorPoint可以通過指定x和y值小于0或者大于1,使它放置在圖層范圍之外相种。
上圖中當(dāng)改變了anchorPoint
威恼,position
屬性保持固定的值并沒有發(fā)生改變,但是frame
卻移動了寝并。
那在什么場合需要改變anchorPoint
呢箫措?既然我們可以隨意改變圖層位置,那改變anchorPoint
不會造成困惑么衬潦?為了舉例說明蒂破,我們來舉一個實用的例子,創(chuàng)建一個模擬鬧鐘的項目别渔。
- 代碼實現(xiàn)
@interface ViewController ()
/** hour */
@property(nonatomic, strong)UIImageView *hourImgView;
/** minute */
@property(nonatomic, strong)UIImageView *minuteImgView;
/** second */
@property(nonatomic, strong)UIImageView *secondImgView;
/** timer */
@property(nonatomic, strong)NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self drawUI];
[self setupTimer];
}
- (void)drawUI {
UIImageView *clockImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
clockImgView.image = [UIImage imageNamed:@"clock"];
clockImgView.contentMode = UIViewContentModeScaleAspectFit;
clockImgView.center = self.view.center;
[self.view addSubview:clockImgView];
UIImageView *secondImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 5, 150)];
secondImgView.image = [UIImage imageNamed:@"second"];
secondImgView.contentMode = UIViewContentModeScaleAspectFit;
secondImgView.center = self.view.center;
[self.view addSubview:self.secondImgView = secondImgView];
UIImageView *minuteImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 5, 120)];
minuteImgView.image = [UIImage imageNamed:@"minute"];
minuteImgView.contentMode = UIViewContentModeScaleAspectFill;
minuteImgView.center = self.view.center;
[self.view addSubview:self.minuteImgView = minuteImgView];
UIImageView *hourkImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 5, 100)];
hourkImgView.image = [UIImage imageNamed:@"hour"];
hourkImgView.contentMode = UIViewContentModeScaleAspectFill;
hourkImgView.center = self.view.center;
[self.view addSubview:self.hourImgView = hourkImgView];
}
#pragma mark - timer
- (void)setupTimer {
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[self updateTimer];
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
[self tick];
}
- (void)updateTimer {
[self tick];
}
- (void)stopTimer {
}
#pragma mark - tick
- (void)tick {
// convert time to houres 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;
CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;
CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;
// rotate hands
self.hourImgView.transform = CGAffineTransformMakeRotation(hoursAngle);
self.minuteImgView.transform = CGAffineTransformMakeRotation(minsAngle);
self.secondImgView.transform = CGAffineTransformMakeRotation(secsAngle);
}
- 運行效果如下
運行項目附迷,看起來有點奇怪(圖3.6),因為鐘表的圖片在圍繞著中心旋轉(zhuǎn)哎媚,這并不是我們期待的一個支點喇伯。
更好的方案是使用anchorPoint屬性,我們給每個鐘指針的anchorPoint做一些平移拨与。
self.hourImgView.layer.anchorPoint = CGPointMake(0.5, 0.9);
self.minuteImgView.layer.anchorPoint = CGPointMake(0.5, 0.9);
self.secondImgView.layer.anchorPoint = CGPointMake(0.5, 0.9);
- 運行效果如下
1.3 坐標(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
位于父圖層的左上角何荚,但是在Mac OS上囱淋,通常是位于左下角。Core Animation可以通過geometryFlipped
屬性來適配這兩種情況餐塘,它決定了一個圖層的坐標(biāo)是否相對于父圖層垂直翻轉(zhuǎn)妥衣,是一個BOOL類型。在iOS上通過設(shè)置它為YES意味著它的子圖層將會被垂直翻轉(zhuǎn)戒傻,也就是將會沿著底部排版而不是通常的頂部(它的所有子圖層也同理称鳞,除非把它們的geometryFlipped屬性也設(shè)為YES)。
Z坐標(biāo)軸
和UIView嚴(yán)格的二維坐標(biāo)系不同稠鼻,CALayer存在于一個三維空間當(dāng)中冈止。除了我們已經(jīng)討論過的position
和anchorPoint
屬性之外,CALayer還有另外兩個屬性候齿,zPosition
和anchorPointZ
熙暴,二者都是在Z軸上描述圖層位置的浮點類型。
注意這里并沒有更深的屬性來描述由寬和高做成的bounds了慌盯,圖層是一個完全扁平的對象周霉,你可以把它們想象成類似于一頁二維的堅硬的紙片,用膠水粘成一個空洞亚皂,就像三維結(jié)構(gòu)的折紙一樣俱箱。
zPosition
屬性在大多數(shù)情況下其實并不常用。在第五章灭必,我們將會涉及CATransform3D狞谱,你會知道如何在三維空間移動和旋轉(zhuǎn)圖層,除了做變換之外禁漓,zPosition最實用的功能就是改變圖層的顯示順序了
跟衅。
通常,圖層是根據(jù)它們子圖層的sublayers出現(xiàn)的順序
來類繪制的播歼,這就是所謂的畫家的算法--就像一個畫家在墻上作畫--后被繪制上的圖層將會遮蓋住之前的圖層伶跷,但是通過增加
圖層的zPosition
,就可以把圖層向相機(jī)方向前置,于是它就在所有其他圖層的前面了(或者至少是小于它的zPosition值的圖層的前面)叭莫。
這里所謂的“相機(jī)”實際上是相對于用戶是視角
蹈集,這里和iPhone背后的內(nèi)置相機(jī)沒任何關(guān)系。
- 實例代碼如下
- (void)zPosition {
UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 100, 100)];
greenView.backgroundColor = [UIColor greenColor];
[self.view addSubview:greenView];
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(150, 250, 100, 100)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
}
- 運行效果如下
首先出現(xiàn)在視圖層級綠色的視圖被繪制在紅色視圖的后面拢肆。
我們希望在真實的應(yīng)用中也能顯示出繪圖的順序,同樣地抵皱,如果我們提高綠色視圖的zPosition
善榛,我們會發(fā)現(xiàn)順序就反了辩蛋。其實并不需要增加太多呻畸,視圖都非常地薄,所以給zPosition提高一個像素就可以讓綠色視圖前置悼院,當(dāng)然0.1或者0.0001也能夠做到伤为,但是最好不要這樣,因為浮點類型四舍五入的計算可能會造成一些不便的麻煩据途。
- 代碼如下
greenView.layer.zPosition = 1.0;
- 運行效果如下
1.4 Hit Testing
前面說了圖層樹
證實了最好使用圖層相關(guān)視圖绞愚,而不是創(chuàng)建獨立的圖層關(guān)系。其中一個原因就是要處理額外復(fù)雜的觸摸事件
颖医。
CALayer并不關(guān)心任何響應(yīng)鏈?zhǔn)录获茫圆荒苤苯犹幚碛|摸事件或者手勢。但是它有一系列的方法幫你處理事件:
-containsPoint:
-hitTest:
containsPoint:
-containsPoint:
接受一個在本圖層坐標(biāo)系下的CGPoint
熔萧,如果這個點在圖層frame范圍內(nèi)就返回YES
糖驴。
- 實例代碼如下
- (void)drawBlueView {
self.layerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
self.layerView.backgroundColor = [UIColor grayColor];
self.layerView.center = self.view.center;
[self.view addSubview:self.layerView];
self.blueLayer = [CALayer layer];
self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:self.blueLayer];
}
- (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 layers coordinates
point = [self.view.layer convertPoint:point toLayer:self.layerView.layer];
// get layer using containsPoint
if ([self.layerView.layer containsPoint:point]) {
// convert point to blueLayer's coordinates
point = [self.layerView.layer convertPoint:point toLayer:self.blueLayer];
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 gray Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
}
- 運行效果如下
-hitTest:
-hitTest:
方法同樣接受一個CGPoint
類型參數(shù),而不是BOOL類型佛致,它返回圖層本身贮缕,或者包含這個坐標(biāo)點的葉子節(jié)點圖層。這意味著不再需要像使用-containsPoint:
那樣俺榆,人工地在每個子圖層變換或者測試點擊的坐標(biāo)感昼。如果這個點在最外面圖層的范圍之外,則返回nil罐脊。具體使用-hitTest:方法被點擊圖層的代碼如下所示
/// hitTest:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// get touch position
CGPoint point = [[touches anyObject] locationInView:self.view];
// get touched layer
CALayer *layer = [self.layerView.layer hitTest:point];
// get layer using hitTest
if (layer == self.blueLayer) {
[[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
} else if (layer == self.layerView.layer) {
[[[UIAlertView alloc] initWithTitle:@"Inside gray Layer"
message:nil
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
- 運行結(jié)果如下
注意當(dāng)調(diào)用圖層的-hitTest:
方法時定嗓,測算的順序嚴(yán)格依賴于圖層樹當(dāng)中的圖層順序
(和UIView處理事件
類似)。之前提到的zPosition
屬性可以明顯改變屏幕上圖層的順序萍桌,但不能改變事件傳遞的順序
蜕乡。
這意味著如果改變了圖層的z軸順序,你會發(fā)現(xiàn)將不能夠檢測到最前方的視圖點擊事件梗夸,這是因為被另一個圖層遮蓋住了层玲,雖然它的zPosition值較小,但是在圖層樹中的順序靠前。我們將在后面詳細(xì)討論這個問題辛块。
1.5 自動布局
你可能用過UIViewAutoresizingMask
類型的一些常量畔派,應(yīng)用于當(dāng)父視圖改變尺寸的時候,相應(yīng)UIView的frame也跟著更新的場景(通常用于橫豎屏切換
)润绵。
在iOS6中线椰,蘋果介紹了自動排版機(jī)制,它和自動調(diào)整不同尘盼,并且更加復(fù)雜憨愉。
在Mac OS平臺,CALayer有一個叫做layoutManager
的屬性可以通過CALayoutManager
協(xié)議和CAConstraintLayoutManager
類來實現(xiàn)自動排版的機(jī)制卿捎。但由于某些原因配紫,這在iOS上并不適用。
當(dāng)使用視圖的時候午阵,可以充分利用UIView類接口暴露出來的UIViewAutoresizingMask
和NSLayoutConstraintAPI
躺孝,但如果想隨意控制CALayer的布局,就需要手工操作底桂。最簡單的方法就是使用CALayerDelegate
如下函數(shù):
- (void)layoutSublayersOfLayer:(CALayer *)layer;
當(dāng)圖層的bounds發(fā)生改變植袍,或者圖層的-setNeedsLayout
方法被調(diào)用的時候,這個函數(shù)將會被執(zhí)行籽懦。這使得你可以手動地重新擺放或者重新調(diào)整子圖層的大小于个,但是不能像UIView的autoresizingMask
和constraints
屬性做到自適應(yīng)屏幕旋轉(zhuǎn)。
這也是為什么最好使用視圖而不是單獨的圖層來構(gòu)建應(yīng)用程序的另一個重要原因之一暮顺。