天天看點

核心動畫(3)圖層幾何學圖層幾何學

核心動畫(3)圖層幾何學

  • 圖層幾何學
    • 布局
    • 錨點
    • 坐标系
      • 翻轉的幾何結構
      • Z坐标軸
    • 自動布局
    • 總結

圖層幾何學

不熟悉幾何學的人就不要來這裡了 --柏拉圖學院入口的簽名

在第二章裡面,我們介紹了圖層背後的圖檔,和一些控制圖層坐标和旋轉的屬性。在這一章中,我們将要看一看圖層内部是如何根據父圖層和兄弟圖層來控制位置和尺寸的。另外我們也會涉及如何管理圖層的幾何結構,以及它是如何被自動調整和自動布局影響的。

布局

UIView

有三個比較重要的布局屬性:

frame

bounds

center

CALayer

對應地叫做

frame

bounds

position

。為了能清楚區分,圖層用了“position”,視圖用了“center”,但是他們都代表同樣的值。

frame

代表了圖層的外部坐标(也就是在父圖層上占據的空間),

bounds

是内部坐标({0, 0}通常是圖層的左上角),

center

position

都代表了相對于父圖層

anchorPoint

所在的位置。

anchorPoint

的屬性将會在後續介紹到,現在把它想成圖層的中心點就好了。圖3.1顯示了這些屬性是如何互相依賴的。

核心動畫(3)圖層幾何學圖層幾何學

圖3.1

UIView

CALayer

的坐标系

視圖的

frame

bounds

center

屬性僅僅是存取方法,當操縱視圖的

frame

,實際上是在改變位于視圖下方

CALayer

frame

,不能夠獨立于圖層之外改變視圖的

frame

對于視圖或者圖層來說,

frame

并不是一個非常清晰的屬性,它其實是一個虛拟屬性,是根據

bounds

position

transform

計算而來,是以當其中任何一個值發生改變,frame都會變化。相反,改變frame的值同樣會影響到他們當中的值

記住當對圖層做變換的時候,比如旋轉或者縮放,

frame

實際上代表了覆寫在圖層旋轉之後的整個軸對齊的矩形區域,也就是說

frame

的寬高可能和

bounds

的寬高不再一緻了(圖3.2)

核心動畫(3)圖層幾何學圖層幾何學

圖3.2 旋轉一個視圖或者圖層之後的

frame

屬性

錨點

之前提到過,視圖的

center

屬性和圖層的

position

屬性都指定了

anchorPoint

相對于父圖層的位置。圖層的

anchorPoint

通過

position

來控制它的

frame

的位置,你可以認為

anchorPoint

是用來移動圖層的把柄。

預設來說,

anchorPoint

位于圖層的中點,是以圖層的将會以這個點為中心放置。

anchorPoint

屬性并沒有被

UIView

接口暴露出來,這也是視圖的position屬性被叫做“center”的原因。但是圖層的

anchorPoint

可以被移動,比如你可以把它置于圖層

frame

的左上角,于是圖層的内容将會向右下角的

position

方向移動(圖3.3),而不是居中了。

核心動畫(3)圖層幾何學圖層幾何學

圖3.3 改變

anchorPoint

的效果

和第二章提到的

contentsRect

contentsCenter

屬性類似,

anchorPoint

用機關坐标來描述,也就是圖層的相對坐标,圖層左上角是{0, 0},右下角是{1, 1},是以預設坐标是{0.5, 0.5}。

anchorPoint

可以通過指定x和y值小于0或者大于1,使它放置在圖層範圍之外。

注意在圖3.3中,當改變了

anchorPoint

position

屬性保持固定的值并沒有發生改變,但是

frame

卻移動了。

那在什麼場合需要改變

anchorPoint

呢?既然我們可以随意改變圖層位置,那改變

anchorPoint

不會造成困惑麼?為了舉例說明,我們來舉一個實用的例子,建立一個模拟鬧鐘的項目。

鐘面和鐘表由四張圖檔組成(圖3.4),為了簡單說明,我們還是用傳統的方式來裝載和加載圖檔,使用四個

UIImageView

執行個體(當然你也可以用正常的視圖,設定他們圖層的

contents

圖檔)。

核心動畫(3)圖層幾何學圖層幾何學

圖3.4 組成鐘面和鐘表的四張圖檔

鬧鐘的元件通過IB來排列(圖3.5),這些圖檔視圖嵌套在一個容器視圖之内,并且自動調整和自動布局都被禁用了。這是因為自動調整會影響到視圖的

frame

,而根據圖3.2的示範,當視圖旋轉的時候,

frame

是會發生改變的,這将會導緻一些布局上的失靈。

我們用

NSTimer

來更新鬧鐘,使用視圖的

transform

屬性來旋轉鐘表(如果你對這個屬性不太熟悉,不要着急,我們将會在第5章“變換”當中詳細說明),具體代碼見清單3.1

核心動畫(3)圖層幾何學圖層幾何學

圖3.5 在Interface Builder中布局鬧鐘視圖

清單3.1 Clock

@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
           

運作項目,看起來有點奇怪(圖3.6),因為鐘表的圖檔在圍繞着中心旋轉,這并不是我們期待的一個支點。

核心動畫(3)圖層幾何學圖層幾何學

圖3.6 鐘面,和不對齊的鐘指針

你也許會認為可以在Interface Builder當中調整指針圖檔的位置來解決,但其實并不能達到目的,因為如果不放在鐘面中間的話,同樣不能正确的旋轉。

也許在圖檔末尾添加一個透明空間也是個解決方案,但這樣會讓圖檔變大,也會消耗更多的記憶體,這樣并不優雅。

更好的方案是使用

anchorPoint

屬性,我們來在

-viewDidLoad

方法中添加幾行代碼來給每個鐘指針的

anchorPoint

做一些平移(清單3.2),圖3.7顯示了正确的結果。

清單3.2

- (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
} 
           
核心動畫(3)圖層幾何學圖層幾何學

圖3.7 鐘面,和正确對齊的鐘指針

坐标系

和視圖一樣,圖層在圖層樹當中也是相對于父圖層按層級關系放置,一個圖層的

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;
           

這些方法可以把定義在一個圖層坐标系下的點或者矩形轉換成另一個圖層坐标系下的點或者矩形

翻轉的幾何結構

正常說來,在iOS上,一個圖層的

position

位于父圖層的左上角,但是在Mac OS上,通常是位于左下角。Core Animation可以通過

geometryFlipped

屬性來适配這兩種情況,它決定了一個圖層的坐标是否相對于父圖層垂直翻轉,是一個

BOOL

類型。在iOS上通過設定它為

YES

意味着它的子圖層将會被垂直翻轉,也就是将會沿着底部排版而不是通常的頂部(它的所有子圖層也同理,除非把它們的

geometryFlipped

屬性也設為

YES

)。

Z坐标軸

UIView

嚴格的二維坐标系不同,

CALayer

存在于一個三維空間當中。除了我們已經讨論過的

position

anchorPoint

屬性之外,

CALayer

還有另外兩個屬性,

zPosition

anchorPointZ

,二者都是在Z軸上描述圖層位置的浮點類型。

注意這裡并沒有更深的屬性來描述由寬和高做成的

bounds

了,圖層是一個完全扁平的對象,你可以把它們想象成類似于一頁二維的堅硬的紙片,用膠水粘成一個空洞,就像三維結構的折紙一樣。

zPosition

屬性在大多數情況下其實并不常用。在第五章,我們将會涉及

CATransform3D

,你會知道如何在三維空間移動和旋轉圖層,除了做變換之外,

zPosition

最實用的功能就是改變圖層的顯示順序了。

通常,圖層是根據它們子圖層的

sublayers

出現的順序來類繪制的,這就是所謂的畫家的算法–就像一個畫家在牆上作畫–後被繪制上的圖層将會遮蓋住之前的圖層,但是通過增加圖層的

zPosition

,就可以把圖層向相機方向前置,于是它就在所有其他圖層的前面了(或者至少是小于它的

zPosition

值的圖層的前面)。

這裡所謂的“相機”實際上是相對于使用者是視角,這裡和iPhone背後的内置相機沒任何關系。

圖3.8顯示了在Interface Builder内的一對視圖,正如你所見,首先出現在視圖層級綠色的視圖被繪制在紅色視圖的後面。

核心動畫(3)圖層幾何學圖層幾何學

圖3.8 在視圖層級中綠色視圖被繪制在紅色視圖的後面

我們希望在真實的應用中也能顯示出繪圖的順序,同樣地,如果我們提高綠色視圖的

zPosition

(清單3.3),我們會發現順序就反了(圖3.9)。其實并不需要增加太多,視圖都非常地薄,是以給

zPosition

提高一個像素就可以讓綠色視圖前置,當然0.1或者0.0001也能夠做到,但是最好不要這樣,因為浮點類型四舍五入的計算可能會造成一些不便的麻煩。

清單3.3

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *greenView;
@property (nonatomic, weak) IBOutlet UIView *redView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //move the green view zPosition nearer to the camera
    self.greenView.layer.zPosition = 1.0f;
}
@end
           
核心動畫(3)圖層幾何學圖層幾何學

圖3.9 綠色視圖被繪制在紅色視圖的前面

##Hit Testing

第一章“圖層樹”證明了最好使用圖層相關視圖,而不是建立獨立的圖層關系。其中一個原因就是要處理額外複雜的觸摸事件。

CALayer

對響應鍊一無所知,是以它不能直接處理觸摸事件或者手勢。但是它有一系列的方法幫你處理事件:

-containsPoint:

-hitTest:

-containsPoint:

接受一個在本圖層坐标系下的

CGPoint

,如果這個點在圖層

frame

範圍内就傳回

YES

。如清單3.4所示第一章的項目的另一個合适的版本,也就是使用

-containsPoint:

方法來判斷到底是白色還是藍色的圖層被觸摸了

(圖3.10)。這需要把觸摸坐标轉換成每個圖層坐标系下的坐标,結果很不友善。

清單3.4 使用containsPoint判斷被點選的圖層

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) CALayer *blueLayer;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    //create sublayer
    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 *)touches withEvent:(UIEvent *)event
{
    //get touch position relative to main view
    CGPoint point = [[touches anyObject] locationInView:self.view];
    //convert point to the white layer's coordinates
    point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
    //get layer using containsPoint:
    if ([self.layerView.layer containsPoint:point]) {
        //convert point to blueLayer’s coordinates
        point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
        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 White Layer"
                                        message:nil 
                                       delegate:nil
                              cancelButtonTitle:@"OK"
                              otherButtonTitles:nil] show];
        }
    }
}

@end
           
核心動畫(3)圖層幾何學圖層幾何學

圖3.10 點選圖層被正确辨別

-hitTest:

方法同樣接受一個

CGPoint

類型參數,而不是

BOOL

類型,它傳回圖層本身,或者包含這個坐标點的葉子節點圖層。這意味着不再需要像使用

-containsPoint:

那樣,人工地在每個子圖層變換或者測試點選的坐标。如果這個點在最外面圖層的範圍之外,則傳回nil。具體使用

-hitTest:

方法被點選圖層的代碼如清單3.5所示。

清單3.5 使用hitTest判斷被點選的圖層

- (void)touchesBegan:(NSSet *)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 White Layer"
                                    message:nil
                                   delegate:nil
                          cancelButtonTitle:@"OK"
                          otherButtonTitles:nil] show];
    }
}
           

注意當調用圖層的

-hitTest:

方法時,測算的順序嚴格依賴于圖層樹當中的圖層順序(和UIView處理事件類似)。之前提到的

zPosition

屬性可以明顯改變螢幕上圖層的順序,但不能改變觸摸事件被處理的順序。

這意味着如果改變了圖層的z軸順序,你會發現将不能夠檢測到最前方的視圖點選事件,這是因為被另一個圖層遮蓋住了,雖然它的

zPosition

值較小,但是在圖層樹中的順序靠前。我們将在第五章詳細讨論這個問題。

自動布局

你可能用過

UIViewAutoresizingMask

類型的一些常量,應用于當父視圖改變尺寸的時候,相應

UIView

frame

也跟着更新的場景(通常用于橫豎屏切換)。

在iOS6中,蘋果介紹了自動布局機制,它和自動調整不同,并且更加複雜,它通過指定組合形成線性方程組和不等式的限制,定義視圖的位置和大小。

在Mac OS平台,

CALayer

有一個叫做

layoutManager

的屬性可以通過

CALayoutManager

協定和

CAConstraintLayoutManager

類來實作自動排版的機制。但由于某些原因,這在iOS上并不适用。

當使用視圖的時候,可以充分利用

UIView

類接口暴露出來的

UIViewAutoresizingMask

NSLayoutConstraint

API,但如果想随意控制

CALayer

的布局,就需要手工操作。最簡單的方法就是使用

CALayerDelegate

如下函數:

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

當圖層的

bounds

發生改變,或者圖層的

-setNeedsLayout

方法被調用的時候,這個函數将會被執行。這使得你可以手動地重新擺放或者重新調整子圖層的大小,但是不能像

UIView

autoresizingMask

constraints

屬性做到自适應螢幕旋轉。

這也是為什麼最好使用視圖而不是單獨的圖層來建構應用程式的另一個重要原因之一。

總結

本章涉及了

CALayer

的幾何結構,包括它的

frame

position

bounds

,介紹了三維空間内圖層的概念,以及如何在獨立的圖層内響應事件,最後簡單說明了在iOS平台中,Core Animation對自動調整和自動布局支援的缺乏。

在第四章“視覺效果”當中,我們接着介紹一些圖層外表的特性。

繼續閱讀