iOS核心動畫高級技巧二(圖層幾何學(xué))

目錄
  • 圖層幾何學(xué)
    • 布局
    • 錨點
    • 坐標(biāo)系
    • Hit Testing
    • 自動布局
一 圖層幾何學(xué)

在這一章中除盏,我們將要看一看圖層內(nèi)部是如何根據(jù)父圖層和兄弟圖層來控制位置和尺寸的味混。另外我們也會涉及如何管理圖層的幾何結(jié)構(gòu),以及它是如何被自動調(diào)整和自動布局影響的谬莹。

1.1 布局

UIView有三個比較重要的布局屬性:frameboundscenter井誉,CALayer對應(yīng)地叫做frame颗圣,boundsposition。為了能清楚區(qū)分蔽午,圖層用了position,視圖用了center骄恶,但是他們都代表同樣的值叠蝇。

frame代表了圖層的外部坐標(biāo)(也就是在父圖層上占據(jù)的空間)悔捶,bounds是內(nèi)部坐標(biāo)({0, 0}通常是圖層的左上角)蜕该,centerposition都代表了相對于父圖層anchorPoint所在的位置堂淡。anchorPoint的屬性將會在后續(xù)介紹到扒腕,現(xiàn)在把它想成圖層的中心點就好了皆的。下圖顯示了這些屬性是如何相互依賴的费薄。

UIView和CALayer的坐標(biāo)系.png

視圖的frameboundscenter屬性僅僅是存取方法凳厢,當(dāng)操縱視圖的frame,實際上是在改變位于視圖下方CALayerframe泡孩,不能夠獨立于圖層之外改變視圖的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屬性.png
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的效果.png

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)建一個模擬鬧鐘的項目别渔。

組成鐘面和鐘表的四張圖.png
  • 代碼實現(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);
}
  • 運行效果如下
Aug-24-2019 09-49-38.gif

運行項目附迷,看起來有點奇怪(圖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);
  • 運行效果如下
Aug-24-2019 09-50-16.gif
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)討論過的positionanchorPoint屬性之外,CALayer還有另外兩個屬性候齿,zPositionanchorPointZ熙暴,二者都是在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];
}
  • 運行效果如下
在視圖層級中綠色視圖被繪制在紅色視圖的后面.png

首先出現(xiàn)在視圖層級綠色的視圖被繪制在紅色視圖的后面拢肆。

我們希望在真實的應(yīng)用中也能顯示出繪圖的順序,同樣地抵皱,如果我們提高綠色視圖的zPosition善榛,我們會發(fā)現(xiàn)順序就反了辩蛋。其實并不需要增加太多呻畸,視圖都非常地薄,所以給zPosition提高一個像素就可以讓綠色視圖前置悼院,當(dāng)然0.1或者0.0001也能夠做到伤为,但是最好不要這樣,因為浮點類型四舍五入的計算可能會造成一些不便的麻煩据途。

  • 代碼如下
greenView.layer.zPosition = 1.0;
  • 運行效果如下
綠色視圖被繪制在紅色視圖的前面.png
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];
        }
    }
}
  • 運行效果如下
image.png
image.png

-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é)果如下
image.png
image.png

注意當(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類接口暴露出來的UIViewAutoresizingMaskNSLayoutConstraintAPI躺孝,但如果想隨意控制CALayer的布局,就需要手工操作底桂。最簡單的方法就是使用CALayerDelegate如下函數(shù):

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

當(dāng)圖層的bounds發(fā)生改變植袍,或者圖層的-setNeedsLayout方法被調(diào)用的時候,這個函數(shù)將會被執(zhí)行籽懦。這使得你可以手動地重新擺放或者重新調(diào)整子圖層的大小于个,但是不能像UIView的autoresizingMaskconstraints屬性做到自適應(yīng)屏幕旋轉(zhuǎn)。

這也是為什么最好使用視圖而不是單獨的圖層來構(gòu)建應(yīng)用程序的另一個重要原因之一暮顺。


本文摘自 iOS核心動畫高級技巧-圖層幾何學(xué)


項目鏈接地址 - AnimationHighSkill_2


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末厅篓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拖云,更是在濱河造成了極大的恐慌贷笛,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宙项,死亡現(xiàn)場離奇詭異乏苦,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)尤筐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門汇荐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盆繁,你說我怎么就攤上這事掀淘。” “怎么了油昂?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵革娄,是天一觀的道長倾贰。 經(jīng)常有香客問我,道長拦惋,這世上最難降的妖魔是什么匆浙? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮厕妖,結(jié)果婚禮上首尼,老公的妹妹穿的比我還像新娘。我一直安慰自己言秸,他們只是感情好软能,可當(dāng)我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著举畸,像睡著了一般查排。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俱恶,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天雹嗦,我揣著相機(jī)與錄音范舀,去河邊找鬼合是。 笑死,一個胖子當(dāng)著我的面吹牛锭环,可吹牛的內(nèi)容都是我干的聪全。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辅辩,長吁一口氣:“原來是場噩夢啊……” “哼难礼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起玫锋,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蛾茉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后撩鹿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谦炬,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年节沦,在試婚紗的時候發(fā)現(xiàn)自己被綠了键思。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡甫贯,死狀恐怖吼鳞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情叫搁,我是刑警寧澤赔桌,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布供炎,位于F島的核電站,受9級特大地震影響疾党,放射性物質(zhì)發(fā)生泄漏碱茁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一仿贬、第九天 我趴在偏房一處隱蔽的房頂上張望纽竣。 院中可真熱鬧,春花似錦茧泪、人聲如沸蜓氨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽穴吹。三九已至,卻和暖如春嗜侮,著一層夾襖步出監(jiān)牢的瞬間港令,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工锈颗, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留顷霹,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓击吱,卻偏偏與公主長得像淋淀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子覆醇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容