1. 程式人生 > >圖層幾何學 -- iOS Core Animation 系列二

圖層幾何學 -- iOS Core Animation 系列二

《圖層樹和寄宿圖 -- iOS Core Animation 系列一》介紹了圖層的基礎知識和一些屬性方法。這篇主要內容是學習下圖層在父圖層上怎麼控制位置和尺寸的。

1.佈局

首先看一張例圖:

clipboard.png

對於圖上的frameboundscenterpostion的概念我就不贅述了。如果有不明白的自行搜尋下了解一下。

frame代表了圖層的外部座標(也就是在父圖層上佔據的空間),bounds是內部座標({0, 0}通常是圖層的左上角),centerposition都代表了相對於父圖層anchorPoint所在的位置

檢視的frameboundscenter屬性僅僅是存取方法,當操縱檢視的frame

時,實際上是在改變檢視對應的CALayerframe, 不能獨立於圖層之外改變檢視的frame.

如果對圖層做了變換,比如旋轉縮放等。frame的值實際指的是圖層旋轉之後整個軸對齊的矩形區域。此時frame的寬高可能和bounds的寬高不一致:

clipboard.png

2.錨點

預設來說,anchorPoint位於圖層的中點。這個屬性沒有被UIView直接暴露出來。但是圖層的anchorPoint可以被移動。我們可以把anchorPoint置於圖層frame的左上角。將會出現下圖右側的情況:

clipboard.png

注意上圖,改變anchorPointposition的值並沒變。

和系列一中提到的contentsRect類似,anchorPoint

單位座標來表示(預設情況是{0.5, 0.5})。可以通過指定x和y值小於0或者大於1,使它放置在圖層範圍之外。

2.1 示例

為了學習這個anchorPoint屬性,下面建立一個鬧鐘的示例demo。資原始檔我是從原文上截圖下來的

clipboard.png建立4個UIImageView並設定好約束(都是居中顯示)。

clipboard.png我們用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);
}

執行專案如下圖:

clipboard.png

除了指標圖片的位置,其他的都正常。可能這時候我們最先想到的方法,是調整對應圖片的位置來解決。但是這樣的話,你可以試試,並不能解決問題。不用賣關子了。這時候就是要用到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存在於一個三維空間中,它還提供了zPostionanchorPointz屬性。zPosition屬性大多數不常用,除了三維動畫之外,它最實用的功能是可以改變圖層的顯示順序。

3.2 zPosition演示程式碼

我們演示下改變zPosition會怎麼改變檢視的顯示順序。首先我在SB中設定兩個檢視,如下圖:

clipboard.png

如果我們不做任何操作,執行後,兩個檢視顯示的順序就是我們現在設定的這樣。但是假如我們對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;
}

現在的顯示效果如下:

clipboard.png

雖說圖層基本沒有厚度,但是我們也儘量不要設定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.layerViewzPosition,會有不同的結果。有興趣的可以自己測試一下。

-- 系列二完 --