iOS——Core Animation 知識摘抄(二)

lihaiyin發表於2015-04-23

陰影

主要是shadowOpacity 、shadowColor、shadowOffset和shadowRadius四個屬性

shadowPath屬性

我們已經知道圖層陰影並不總是方的,而是從圖層內容的形狀繼承而來。這看上去不錯,但是實時計算陰影也是一個非常消耗資源的,尤其是圖層有多個子圖層,每個圖層還有一個有透明效果的寄宿圖的時候。

如果你事先知道你的陰影形狀會是什麼樣子的,你可以通過指定一個shadowPath來提高效能。shadowPath是一個CGPathRef型別(一個指向CGPath的指標)。CGPath是一個Core Graphics物件,用來指定任意的一個向量圖形。我們可以通過這個屬性單獨於圖層形狀之外指定陰影的形狀。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  //enable layer shadows
  self.layerView1.layer.shadowOpacity = 0.5f;
  self.layerView2.layer.shadowOpacity = 0.5f;
//create a square shadow CGMutablePathRef squarePath = CGPathCreateMutable(); CGPathAddRect(squarePath, NULL, self.layerView1.bounds); self.layerView1.layer.shadowPath = squarePath;
CGPathRelease(squarePath);
?//create a circular shadow CGMutablePathRef circlePath = CGPathCreateMutable(); CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds); self.layerView2.layer.shadowPath =
circlePath;
CGPathRelease(circlePath); }
@end

如果是一個舉行或是圓,用CGPath會相當簡單明瞭。但是如果是更加複雜一點的圖形,UIBezierPath類會更合適,它是一個由UIKit提供的在CGPath基礎上的Objective-C包裝類。

 

圖層蒙板

CALayer有一個屬性叫做mask可以解決這個問題。這個屬性本身就是個CALayer型別,有和其他圖層一樣的繪製和佈局屬性。它類似於一個子圖層,相對於父圖層(即擁有該屬性的圖層)佈局,但是它卻不是一個普通的子圖層。不同於那些繪製在父圖層中的子圖層,mask圖層定義了父圖層的部分可見區域。

mask圖層的Color屬性是無關緊要的,真正重要的是圖層的輪廓。mask屬性就像是一個餅乾切割機,mask圖層實心的部分會被保留下來,其他的則會被拋棄。(如圖4.12)

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  //create mask layer
  CALayer *maskLayer = [CALayer layer];
  maskLayer.frame = self.layerView.bounds;
  UIImage *maskImage = [UIImage imageNamed:@"Cone.png"];
  maskLayer.contents = (__bridge id)maskImage.CGImage;
  //apply mask to image layer?
  self.imageView.layer.mask = maskLayer;
}
@end

 

CALayer蒙板圖層真正厲害的地方在於蒙板圖不侷限於靜態圖。任何有圖層構成的都可以作為mask屬性,這意味著你的蒙板可以通過程式碼甚至是動畫實時生成。

 

組透明

iOS常見的做法是把一個空間的alpha值設定為0.5(50%)以使其看上去呈現為不可用狀態。對於獨立的檢視來說還不錯,但是當一個控制元件有子檢視的時候就有點奇怪了,圖4.20展示了一個內嵌了UILabel的自定義UIButton;左邊是一個不透明的按鈕,右邊是50%透明度的相同按鈕。我們可以注意到,裡面的標籤的輪廓跟按鈕的背景很不搭調。

 

理想狀況下,當你設定了一個圖層的透明度,你希望它包含的整個圖層樹像一個整體一樣的透明效果。你可以通過設定Info.plist檔案中的UIViewGroupOpacity為YES來達到這個效果,但是這個設定會影響到這個應用,整個app可能會受到不良影響。如果UIViewGroupOpacity並未設定,iOS 6和以前的版本會預設為NO(也許以後的版本會有一些改變)。

另一個方法就是,你可以設定CALayer的一個叫做shouldRasterize屬性(見清單4.7)來實現組透明的效果,如果它被設定為YES,在應用透明度之前,圖層及其子圖層都會被整合成一個整體的圖片,這樣就沒有透明度混合的問題了(如圖4.21)。

為了啟用shouldRasterize屬性,我們設定了圖層的rasterizationScale屬性。預設情況下,所有圖層拉伸都是1.0, 所以如果你使用了shouldRasterize屬性,你就要確保你設定了rasterizationScale屬性去匹配螢幕,以防止出現Retina螢幕畫素化的問題。

當shouldRasterize和UIViewGroupOpacity一起的時候,效能問題就出現了(我們在第12章『速度』和第15章『圖層效能』將做出介紹),但是效能碰撞都本地化了(譯者注:這句話需要再翻譯)。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (UIButton *)customButton
{
  //create button
  CGRect frame = CGRectMake(0, 0, 150, 50);
  UIButton *button = [[UIButton alloc] initWithFrame:frame];
  button.backgroundColor = [UIColor whiteColor];
  button.layer.cornerRadius = 10;
  //add label
  frame = CGRectMake(20, 10, 110, 30);
  UILabel *label = [[UILabel alloc] initWithFrame:frame];
  label.text = @"Hello World";
  label.textAlignment = NSTextAlignmentCenter;
  [button addSubview:label];
  return button;
}
- (void)viewDidLoad
{
  [super viewDidLoad];
  //create opaque button
  UIButton *button1 = [self customButton];
  button1.center = CGPointMake(50, 150);
  [self.containerView addSubview:button1];
  //create translucent button
  UIButton *button2 = [self customButton];
  ?
  button2.center = CGPointMake(250, 150);
  button2.alpha = 0.5;
  [self.containerView addSubview:button2];
  //enable rasterization for the translucent button
  button2.layer.shouldRasterize = YES;
  button2.layer.rasterizationScale = [UIScreen mainScreen].scale;
}
@end

仿射變換

當對圖層應用變換矩陣,圖層矩形內的每一個點都被相應地做變換,從而形成一個新的四邊形的形狀。CGAffineTransform中的“仿射”的意思是無論變換矩陣用什麼值,圖層中平行的兩條線在變換之後任然保持平行,CGAffineTransform可以做出任意符合上述標註的變換,圖5.2顯示了一些仿射的和非仿射的變換:

UIView可以通過設定transform屬性做變換,但實際上它只是封裝了內部圖層的變換。

CALayer同樣也有一個transform屬性,但它的型別是CATransform3D,而不是CGAffineTransform,本章後續將會詳細解釋。CALayer對應於UIView的transform屬性叫做affineTransform,清單5.1的例子就是使用affineTransform對圖層做了45度順時針旋轉。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //rotate the layer 45 degrees
    CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
    self.layerView.layer.affineTransform = transform;
}
@end

注意我們使用的旋轉常量是M_PI_4,而不是你想象的45,因為iOS的變換函式使用弧度而不是角度作為單位。弧度用數學常量pi的倍數表示,一個pi代表180度,所以四分之一的pi就是45度。

C的數學函式庫(iOS會自動引入)提供了pi的一些簡便的換算,M_PI_4於是就是pi的四分之一,如果對換算不太清楚的話,可以用如下的巨集做換算:

#define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0) 
#define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI)
 

混合變換

當操縱一個變換的時候,初始生成一個什麼都不做的變換很重要--也就是建立一個CGAffineTransform型別的空值,矩陣論中稱作單位矩陣,Core Graphics同樣也提供了一個方便的常量:CGAffineTransformIdentity

最後,如果需要混合兩個已經存在的變換矩陣,就可以使用如下方法,在兩個變換的基礎上建立一個新的變換:CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

我們來用這些函式組合一個更加複雜的變換,先縮小50%,再旋轉30度,最後向右移動200個畫素(清單5.2)。圖5.4顯示了圖層變換最後的結果。
- (void)viewDidLoad
{
    [super viewDidLoad]; 
//create a new transform CGAffineTransform transform = CGAffineTransformIdentity;
//scale by 50% transform = CGAffineTransformScale(transform, 0.5, 0.5);
//rotate by 30 degrees transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//translate by 200 points transform = CGAffineTransformTranslate(transform, 200, 0); //apply transform to layer self.layerView.layer.affineTransform = transform; }

圖5.4中有些需要注意的地方:圖片向右邊發生了平移,但並沒有指定距離那麼遠(200畫素),另外它還有點向下發生了平移。原因在於當你按順序做了變換,上一個變換的結果將會影響之後的變換,所以200畫素的向右平移同樣也被旋轉了30度,縮小了50%,所以它實際上是斜向移動了100畫素。

這意味著變換的順序會影響最終的結果,也就是說旋轉之後的平移和平移之後的旋轉結果可能不同。

3D變換

繞Y軸旋轉圖層

@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //rotate the layer 45 degrees along the Y axis
    CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    self.layerView.layer.transform = transform;
}
@end

透視投影

CATransform3D的透視效果通過一個矩陣中一個很簡單的元素來控制:m34。m34(圖5.9)用於按比例縮放X和Y的值來計算到底要離視角多遠。

m34的預設值是0,我們可以通過設定m34為-1.0 / d來應用透視效果,d代表了想象中視角相機和螢幕之間的距離,以畫素為單位,那應該如何計算這個距離呢?實際上並不需要,大概估算一個就好了。

因為視角相機實際上並不存在,所以可以根據螢幕上的顯示效果自由決定它的防止的位置。通常500-1000就已經很好了,但對於特定的圖層有時候更小後者更大的值會看起來更舒服,減少距離的值會增強透視效果,所以一個非常微小的值會讓它看起來更加失真,然而一個非常大的值會讓它基本失去透視效果,對檢視應用透視的程式碼見清單5.5,結果見圖5.10。

@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a new transform
    CATransform3D transform = CATransform3DIdentity;
    //apply perspective
    transform.m34 = - 1.0 / 500.0;
    //rotate by 45 degrees along the Y axis
    transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
    //apply to layer
    self.layerView.layer.transform = transform;
}
@end

  

消亡點

Core Animation定義了這個點位於變換圖層的anchorPoint(通常位於圖層中心,但也有例外,見第三章)。這就是說,當圖層發生變換時,這個點永遠位於圖層變換之前anchorPoint的位置。

當改變一個圖層的position,你也改變了它的消亡點,做3D變換的時候要時刻記住這一點,當你檢視通過調整m34來讓它更加有3D效果,應該首先把它放置於螢幕中央,然後通過平移來把它移動到指定位置(而不是直接改變它的position),這樣所有的3D圖層都共享一個消亡點。

sublayerTransform屬性

如果有多個檢視或者圖層,每個都做3D變換,那就需要分別設定相同的m34值,並且確保在變換之前都在螢幕中央共享同一個position,如果用一個函式封裝這些操作的確會更加方便,但仍然有限制(例如,你不能在Interface Builder中擺放檢視),這裡有一個更好的方法。

CALayer有一個屬性叫做sublayerTransform。它也是CATransform3D型別,但和對一個圖層的變換不同,它影響到所有的子圖層。這意味著你可以一次性對包含這些圖層的容器做變換,於是所有的子圖層都自動繼承了這個變換方法。

相較而言,通過在一個地方設定透視變換會很方便,同時它會帶來另一個顯著的優勢:消亡點被設定在容器圖層的中點,從而不需要再對子圖層分別設定了。這意味著你可以隨意使用position和frame來放置子圖層,而不需要把它們放置在螢幕中點,然後為了保證統一的消亡點用變換來做平移。

我們來用一個demo舉例說明。這裡用Interface Builder並排放置兩個檢視(圖5.12),然後通過設定它們容器檢視的透視變換,我們可以保證它們有相同的透視和消亡點,程式碼見清單5.6,結果見圖5.13。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, weak) IBOutlet UIView *layerView1;
@property (nonatomic, weak) IBOutlet UIView *layerView2;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //apply perspective transform to container
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = - 1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //rotate layerView1 by 45 degrees along the Y axis
    CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
    self.layerView1.layer.transform = transform1;
    //rotate layerView2 by 45 degrees along the Y axis
    CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
    self.layerView2.layer.transform = transform2;
}

背面(相當於水平翻轉,但其實是。。。)

但這並不是一個很好的特性,因為如果圖層包含文字或者其他控制元件,那使用者看到這些內容的映象圖片當然會感到困惑。另外也有可能造成資源的浪費:想象用這些圖層形成一個不透明的固態立方體,既然永遠都看不見這些圖層的背面,那為什麼浪費GPU來繪製它們呢?

CALayer有一個叫做doubleSided的屬性來控制圖層的背面是否要被繪製。這是一個BOOL型別,預設為YES,如果設定為NO,那麼當圖層正面從相機視角消失的時候,它將不會被繪製。

固體物件

我們把一個有顏色的UILabel放置在檢視內部,是為了清楚的辨別它們之間的關係,並且UIButton被放置在第三個面檢視裡面,後面會做簡單的解釋。

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
    //get the face view and add it to the container
    UIView *face = self.faces[index];
    [self.containerView addSubview:face];
    //center the face view within the container
    CGSize containerSize = self.containerView.bounds.size;
    face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
    // apply the transform
    face.layer.transform = transform;
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up the container sublayer transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    self.containerView.layer.sublayerTransform = perspective;
    //add cube face 1
    CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
    [self addFace:0 withTransform:transform];
    //add cube face 2
    transform = CATransform3DMakeTranslation(100, 0, 0);
    transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
    [self addFace:1 withTransform:transform];
    //add cube face 3
    transform = CATransform3DMakeTranslation(0, -100, 0);
    transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
    [self addFace:2 withTransform:transform];
    //add cube face 4
    transform = CATransform3DMakeTranslation(0, 100, 0);
    transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
    [self addFace:3 withTransform:transform];
    //add cube face 5
    transform = CATransform3DMakeTranslation(-100, 0, 0);
    transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
    [self addFace:4 withTransform:transform];
    //add cube face 6
    transform = CATransform3DMakeTranslation(0, 0, -100);
    transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
    [self addFace:5 withTransform:transform];
}
@end

從這個角度看立方體並不是很明顯;看起來只是一個方塊,為了更好地欣賞它,我們將更換一個不同的視角。

旋轉這個立方體將會顯得很笨重,因為我們要單獨對每個面做旋轉。另一個簡單的方案是通過調整容器檢視的sublayerTransform去旋轉照相機。

新增如下幾行去旋轉containerView圖層的perspective變換矩陣:

perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); 
perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
這就對相機(或者相對相機的整個場景,你也可以這麼認為)繞Y軸旋轉45度,並且繞X軸旋轉45度。現在從另一個角度去觀察立方體,就能看出它的真實面貌(圖5.21)。

光亮和陰影

如果需要動態地建立光線效果,你可以根據每個檢視的方向應用不同的alpha值做出半透明的陰影圖層,但為了計算陰影圖層的不透明度,你需要得到每個面的正太向量(垂直於表面的向量),然後根據一個想象的光源計算出兩個向量叉乘結果。叉乘代表了光源和圖層之間的角度,從而決定了它有多大程度上的光亮。

清單5.10實現了這樣一個結果,我們用GLKit框架來做向量的計算(你需要引入GLKit庫來執行程式碼),每個面的CATransform3D都被轉換成GLKMatrix4,然後通過GLKMatrix4GetMatrix3函式得出一個3×3的旋轉矩陣。這個旋轉矩陣指定了圖層的方向,然後可以用它來得到正太向量的值。

結果如圖5.22所示,試著調整LIGHT_DIRECTION和AMBIENT_LIGHT的值來切換光線效果

#import "ViewController.h" 
#import 
#import
#define LIGHT_DIRECTION 0, 1, -0.5 
#define AMBIENT_LIGHT 0.5
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces;
@end
@implementation ViewController
- (void)applyLightingToFace:(CALayer *)face
{
    //add lighting layer
    CALayer *layer = [CALayer layer];
    layer.frame = face.bounds;
    [face addSublayer:layer];
    //convert the face transform to matrix
    //(GLKMatrix4 has the same structure as CATransform3D)
    CATransform3D transform = face.transform;
    GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform;
    GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
    //get face normal
    GLKVector3 normal = GLKVector3Make(0, 0, 1);
    normal = GLKMatrix3MultiplyVector3(matrix3, normal);
    normal = GLKVector3Normalize(normal);
    //get dot product with light direction
    GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION));
    float dotProduct = GLKVector3DotProduct(light, normal);
    //set lighting layer opacity
    CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT;
    UIColor *color = [UIColor colorWithWhite:0 alpha:shadow];
    layer.backgroundColor = color.CGColor;
}
- (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform
{
    //get the face view and add it to the container
    UIView *face = self.faces[index];
    [self.containerView addSubview:face];
    //center the face view within the container
    CGSize containerSize = self.containerView.bounds.size;
    face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
    // apply the transform
    face.layer.transform = transform;
    //apply lighting
    [self applyLightingToFace:face.layer];
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    //set up the container sublayer transform
    CATransform3D perspective = CATransform3DIdentity;
    perspective.m34 = -1.0 / 500.0;
    perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0);
    perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
    self.containerView.layer.sublayerTransform = perspective;
    //add cube face 1
    CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100);
    [self addFace:0 withTransform:transform];
    //add cube face 2
    transform = CATransform3DMakeTranslation(100, 0, 0);
    transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0);
    [self addFace:1 withTransform:transform];
    //add cube face 3
    transform = CATransform3DMakeTranslation(0, -100, 0);
    transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0);
    [self addFace:2 withTransform:transform];
    //add cube face 4
    transform = CATransform3DMakeTranslation(0, 100, 0);
    transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0);
    [self addFace:3 withTransform:transform];
    //add cube face 5
    transform = CATransform3DMakeTranslation(-100, 0, 0);
    transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0);
    [self addFace:4 withTransform:transform];
    //add cube face 6
    transform = CATransform3DMakeTranslation(0, 0, -100);
    transform = CATransform3DRotate(transform, M_PI, 0, 1, 0);
    [self addFace:5 withTransform:transform];
}
@end

點選事件

你應該能注意到現在可以在第三個表面的頂部看見按鈕了,點選它,什麼都沒發生,為什麼呢?

這並不是因為iOS在3D場景下正確地處理響應事件,實際上是可以做到的。問題在於檢視順序。在第三章中我們簡要提到過,點選事件的處理由檢視在父檢視中的順序決定的,並不是3D空間中的Z軸順序。當給立方體新增檢視的時候,我們實際上是按照一個順序新增,所以按照檢視/圖層順序來說,4,5,6在3的前面。

即使我們看不見4,5,6的表面(因為被1,2,3遮住了),iOS在事件響應上仍然保持之前的順序。當試圖點選表面3上的按鈕,表面4,5,6截斷了點選事件(取決於點選的位置),這就和普通的2D佈局在按鈕上覆蓋物體一樣。

你也許認為把doubleSided設定成NO可以解決這個問題,因為它不再渲染檢視後面的內容,但實際上並不起作用。因為背對相機而隱藏的檢視仍然會響應點選事件(這和通過設定hidden屬性或者設定alpha為0而隱藏的檢視不同,那兩種方式將不會響應事件)。所以即使禁止了雙面渲染仍然不能解決這個問題(雖然由於效能問題,還是需要把它設定成NO)。

這裡有幾種正確的方案:把除了表面3的其他檢視userInteractionEnabled屬性都設定成NO來禁止事件傳遞。或者簡單通過程式碼把檢視3覆蓋在檢視6上。無論怎樣都可以點選按鈕了(圖5.23)。

相關文章