# iOS基礎 # UIView、CALayer使用說明書

前言

? iOS開發中 UI是很重要也是最直觀可見的一部分,而所有的控件都是繼承自UIView的,UIView既可以實現顯示的功能,又可以實現響應用戶操作的功能。我們還知道每個UIView中都存在一個東西叫CALayer,實現了內容繪制等功能。本文總結整理UIView和CALayer的一些基本使用。

UIView

UIView表示屏幕上的一塊矩形區域,負責渲染區域的內容,并且響應該區域內發生事件。

UIView繼承自UIResponder, 事件響應部分見:iOS事件響應鏈

UIView動畫方面擴展見: iOS動畫原理與實現

1、基礎類別

1、繼承自UIResponder

2、layerClass 設置rootLayer 為自定義layer

3、userInteractionEnabled

4、tag 用于標記view,可以通過viewWithTag 查找對應view

5、layer  獲取rootLayer

6、canBecomeFocused  是否能成為焦點

2、幾何類別

frame、bounds、center

frame 復合屬性 由bounds表示大小、center表示位置 后續會具體解釋
bounds 視圖在其自己的坐標系中的位置與尺寸,但是無法確定自己在父視圖中的位置
center 定義了當前視圖在父視圖中的位置

注意:

bounds屬性與center屬性是完全獨立的,前者規定尺寸,后者定義位置
bounds中位置的修改不會影響自身在父視圖中的位置,但是會影響自己的subView的位置

transform

用于給UIView做一些形變(平移、縮放、旋轉)

移動:
// 平移
//每次移動都是相對于上次位置
 _redView.transform = CGAffineTransformTranslate(_redView.transform, 100, 0);
//每次移動都是相對于最開始的位置
 _redView.transform = CGAffineTransformMakeTranslation(200, 0);
 
 縮放:
//每次縮放都是相對于上次
 _redView.transform = CGAffineTransformScale(_redView.transform, 10, 10)
//每次縮放都是相對于最開始
 _redView.transform = CGAffineTransformMakeScale(10, 10);
 
 旋轉:
 // 每次旋轉都是相對于最初的角度
_redView.transform = CGAffineTransformMakeRotation(M_PI_4);
//每次旋轉都是相對于現在的角度
_redView.transform = CGAffineTransformRotate(_redView.transform, M_PI_4);

contentScaleFactor

這個屬性代表了從邏輯坐標系轉化成當前的設備坐標系的轉化比例,在[UIScreen mainScreen]中有個屬性叫做scale 和這個是一樣的

邏輯坐標系即我們數學上經常用的坐標體系,是對現實事物的一種抽象。

比如說我們要在app上顯示一個正方形,我們會確定它的坐標(100,100) 和 寬高(100,100).在這里,坐標和寬高的數值都是對這個正方形的一種抽象。在實際顯示的過程中,坐標的具體位置和寬高的實際長度則由具體硬件的物理屬性和它規定的坐標體系進行表達。在邏輯坐標系中,以points作為測量單位,即通常在數學的坐標系中用點來表示最小的測量單位。

在我們進行編程時,frame、center中設置的表達坐標位置所使用的CGFloat參數就是以point為單位的。

設備坐標系是設備實際的坐標系.在實際屏幕中,是以像素(Pixel)作為基本的測量單位.

由于兩個坐標系的單位不統一,這時需要進行坐標系的轉換.

iOS中當我們使用Quartz,UIKit,CoreAnimation等框架時,所有的坐標系統采用Point來衡量.系統在實際渲染到設置時會幫助我們處理Point到Pixel的轉換.

scale屬性反映了從邏輯坐標到設備屏幕坐標的轉換。在非視網膜屏幕上,比例因子值為1.0,即邏輯坐標系中的一個點等于設備中一個像素(1×1),在視網膜屏幕中,比例因子值為2.0,即邏輯坐標系中的一個點等于設備中四個像素(2×2)。同理,在6plus這種scale為3.0的設備上,1point等于9pixels。

因此,當我們在繪圖中做出一條線寬為1的線時,在非視網膜屏幕和視網膜屏幕上的情況是不同的。

非視網膜屏幕和視網膜屏幕上一個線寬時的顯示情況:

image

在非視網膜屏幕中,當我們把線寬為1的線畫在(3,0)上時,線為一個像素點的寬度(虛線部分),由于事實上不能讓一個像素點顯示半個像素,所以iOS的反鋸齒技術讓1個線寬的線顯示出了2個像素寬度的一條線(淺色部分),并且顏色變淺。只有對線進行0.5的偏移才能顯示真正的線寬為1的線。

偏移了0.5(point)才能顯示一個像素寬度的線:

image

在視網膜屏幕中,如果想要畫出寬度為一個像素的線,不僅需要先0.5point的線寬,還要進行0.25point的偏移,才能繪出一個像素點寬度的線。

exclusiveTouch

ExclusiveTouch的作用是:可以達到同一界面上多個控件接受事件時的排他性,從而避免bug。
也就是說避免在一個界面上同時點擊多個UIButton導致同時響應多個方法。
當這個UIView成為第一響應者時,在手指離開屏幕前其他view不會響應任何touch事件。

如果你不想讓2個button同時點擊,只需要把它們的exclusiveTouch都設定為YES

multipleTouchEnabled

是否支持多點觸控

convertPoint:toView:、convertRect:toView、convertPoint:fromView:、convertRect:fromView:

坐標轉換

1、convertPoint:toView:

[self.view addSubview:myView];
[myView convertPoint:CGPointMake(10, 10) toView:self.view];

把子view中的坐標點轉換到父容器坐標系中。

[myView convertPoint:CGPointMake(10, 10) fromView:self.view];

把父坐標系中的坐標點 轉換到子容器坐標系中

autoresizesSubviews、autoresizingMask

自動尺寸調整行為
當您改變視圖的邊框矩形時,其內嵌子視圖的位置和尺寸往往也需要改變,以適應原始視圖的新尺寸。如果視圖的autoresizesSubviews屬性聲明被設置為YES,則其子視圖會根據autoresizingMask屬性的值自動進行尺寸調整。簡單配置一下視圖的自動尺寸調整掩碼常常就能使應用程序得到合適的行為;否則,應用程序就必須通過重載layoutSubviews方法來提供自己的實現。
設置視圖的自動尺寸調整行為的方法是通過位OR操作符將期望的自動尺寸調整常量連結起來,并將結果賦值給視圖的autoresizingMask屬性。表2-1列舉了自動尺寸調整常量,并描述這些常量如何影響給定視圖的尺寸和位置。舉例來說,如果要使一個視圖和其父視圖左下角的相對位置保持不變,可以加入UIViewAutoresizingFlexibleRightMargin 和UIViewAutoresizingFlexibleTopMargin常量,并將結果賦值給autoresizingMask屬性。當同一個軸向有 多個部分被設置為可變時,尺寸調整的裕量會被平均分配到各個部分上。
     
UIViewAutoresizingNone
這個常量如果被設置,視圖將不進行自動尺寸調整。
UIViewAutoresizingFlexibleHeight
這個常量如果被設置,視圖的高度將和父視圖的高度一起成比例變化。否則,視圖的高度將保持不變。
UIViewAutoresizingFlexibleWidth
這個常量如果被設置,視圖的寬度將和父視圖的寬度一起成比例變化。否則,視圖的寬度將保持不變。
UIViewAutoresizingFlexibleLeftMargin
這個常量如果被設置,視圖的左邊界將隨著父視圖寬度的變化而按比例進行調整。否則,視圖和其父視圖的左邊界的相對位置將保持不變。
UIViewAutoresizingFlexibleRightMargin
這個常量如果被設置,視圖的右邊界將隨著父視圖寬度的變化而按比例進行調整。否則,視圖和其父視圖的右邊界的相對位置將保持不變。
UIViewAutoresizingFlexibleBottomMargin
這個常量如果被設置,視圖的底邊界將隨著父視圖高度的變化而按比例進行調整。否則,視圖和其父視圖的底邊界的相對位置將保持不變。
UIViewAutoresizingFlexibleTopMargin
這個常量如果被設置,視圖的上邊界將隨著父視圖高度的變化而按比例進行調整。否則,視圖和其父視圖的上邊界的相對位置將保持不變。


如 果您通過Interface Builder配置視圖,則可以用Size查看器的Autosizing控制來設置每個視圖的自動尺寸調整行為。上圖中的靈活寬度及高度常量和 Interface Builder中位于同樣位置的彈簧具有同樣的行為,但是空白常量的行為則是正好相反。換句話說,如果要將靈活右空白的自動尺寸調整行為應用到 Interface Builder的某個視圖,必須使相應方向空間的Autosizing控制為空,而不是放置一個支柱。幸運的是,Interface Builder通過動畫顯示了您的修改對視圖自動尺寸調整行為的影響。
如果視圖的autoresizesSubviews屬性被設置為 NO,則該視圖的直接子視圖的所有自動尺寸調整行為將被忽略。類似地,如果一個子視圖的自動尺寸調整掩碼被設置為 UIViewAutoresizingNone,則該子視圖的尺寸將不會被調整,因而其直接子視圖的尺寸也不會被調整。
請注意:為了使自動尺寸調整的行為正確,視圖的transform屬性必須設置為恒等變換;其它變換下的尺寸自動調整行為是未定義的。
自動尺寸調整行為可以適合一些布局的要求,但是如果您希望更多地控制視圖的布局,可以在適當的視圖類中重載layoutSubviews方法。

sizeToFit、sizeThatFits:(CGSize)size

- (CGSize)sizeThatFits:(CGSize)size;     // return 'best' size to fit given size. does not actually resize view. Default is return existing view size
- (void)sizeToFit;                       // calls sizeThatFits: with current view bounds and changes bounds size

根據文檔解釋,我們可以知道 sizeThatFits 會返回一個最合適的size,但是并不更新View的size,sizeToFit 調用 sizeThatFits: 并更新size, 自動調用drawRect:方法

sizeToFit不應該在子類中被重寫,應該重寫sizeThatFits
sizeThatFits傳入的參數是receiver當前的size,返回一個適合的size

3、層次類別

方法

//從父容器移除自己
- (void)removeFromSuperview;

//添加一個view
- (void)addSubview:(UIView *)view;

//插入指定位置
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index

//調整A\B兩個View的位置
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2

//把view提到其他subViews的上面
- (void)bringSubviewToFront:(UIView *)view;

//把view放到其他subViews的下面
- (void)sendSubviewToBack:(UIView *)view;

//根據tag查找對應的view
- (nullable __kindof UIView *)viewWithTag:(NSInteger)tag;

//UIView添加subView的生命周期

- (void)didAddSubview:(UIView *)subview;
- (void)willRemoveSubview:(UIView *)subview;
- (void)willMoveToSuperview:(nullable UIView *)newSuperview;
- (void)didMoveToSuperview;
- (void)willMoveToWindow:(nullable UIWindow *)newWindow;
- (void)didMoveToWindow;

4、渲染類別

屬性:

clipsToBounds:是否遮蓋越界部分subView的顯示,默認NO

opaque : view的不透明度  默認YES

clearsContextBeforeDrawing
重繪的時候清除原有內容
當view沒有設置背景色的時候,或者說opaque為透明的時候不生效。

contentMode: 填充模式

contentStretch:內容拉伸

maskView:view上的遮罩層,不存在和view的層級關系

方法:

//重寫此方法,執行重繪任務
- (void)drawRect:(CGRect)rect;

//標記為需要重繪,異步調用drawRect,標上一個需要被重新繪圖的標記,在下一個draw周期自動重繪,iphone device的刷新頻率是60hz,也就是1/60秒后重繪 
- (void)setNeedsDisplay;

//標記為需要局部重繪
- (void)setNeedsDisplayInRect:(CGRect)rect;

UI更新、渲染機制

1、layoutSubviews方法

在UIView里面有一個方法layoutSubviews,這個方法具體作用是什么呢?

layoutSubviews是對subviews重新布局。比如,我們想更新子視圖的位置的時候,可以通過調用layoutSubviews方法,既可以實現對子視圖重新布局。layoutSubviews默認是不做任何事情的,用到的時候,需要在子類進行重寫。

蘋果官方文檔建議不要直接調用此方法。如果你想強制更新布局,你可以調用setNeedsLayout方法;如果你想立即數顯你的views,你需要調用layoutIfNeeded方法。

layoutSubviews以下情況會被調用

以下幾種情況layoutSubviews會被調用:

1、init初始化不會觸發layoutSubviews。

2、addSubview會觸發layoutSubviews。 注意:當view的fram的值為0的時候,`addSubview`也不會調用`layoutSubviews`的。

3、設置view的Frame會觸發layoutSubviews,當然前提是frame的值設置前后發生了變化。

4、滾動一個UIScrollView會觸發layoutSubviews。

5、旋轉Screen會觸發父UIView上的layoutSubviews事件。

6、改變一個UIView大小的時候也會觸發父UIView上的layoutSubviews事件。

7、直接調用setLayoutSubviews。

2、 setNeedsDisplay 、 setNeedsLayout 和 drawRect

首先 setNeedsDisplay 、 setNeedsLayout兩個方法都是異步執行的。而 setNeedsDisplay 會調用自動調用drawRect方法,這樣可以拿到 UIGraphicsGetCurrentContext,就可以畫畫了。而setNeedsLayout會默認調用 layoutSubViews,就可以處理子視圖中的一些數據。

綜上所訴,setNeedsDisplay方便繪圖,而setNeedsLayout方便處理布局。

drawRect方法以下情況會被調用

 1、如果在UIView初始化時沒有設置rect大小,將直接導致drawRect不被自動調用。drawRect調用是在Controller->loadView, Controller->viewDidLoad 兩方法之后掉用的.所以不用擔心在控制器中,這些View的drawRect就開始畫了.這樣可以在控制器中設置一些值給View(如果這些View draw的時候需要用到某些變量值).
 
2、該方法在調用sizeToFit后被調用,所以可以先調用sizeToFit計算出size。然后系統自動調用drawRect:方法。

3、通過設置contentMode屬性值為UIViewContentModeRedraw。那么將在每次設置或更改frame的時候自動調用drawRect:。

4、直接調用setNeedsDisplay,或者setNeedsDisplayInRect:觸發drawRect:,但是有個前提條件是rect不能為0。

以上1,2推薦;而3,4不提倡

drawRect方法使用注意點

1、若使用UIView繪圖,只能在drawRect:方法中獲取相應的contextRef并繪圖。如果在其他方法中獲取將獲取到一個invalidate的ref并且不能用于畫圖。drawRect:方法不能手動顯示調用,必須通過調用setNeedsDisplay 或者 setNeedsDisplayInRect,讓系統自動調該方法。

2、若使用calayer繪圖,只能在drawInContext: 中(類似于drawRect)繪制,或者在delegate中的相應方法繪制。同樣也是調用setNeedDisplay等間接調用以上方法

3、若要實時畫圖,不能使用gestureRecognizer,只能使用touchbegan等方法來掉用setNeedsDisplay實時刷新屏幕

3、其他更新方法

setNeedsLayout:告知頁面需要更新,但是不會立刻開始更新,做標記等待運行循環。執行后會立刻調用layoutSubviews。

layoutIfNeeded:告知頁面布局立刻更新。所以一般都會和setNeedsLayout一起使用。如果希望立刻生成新的frame需要調用此方法,利用這點一般布局動畫可以在更新布局后直接使用這個方法讓動畫生效。

layoutSubviews:可以重寫布局,默認沒有做任何事情,需要子類進行重寫

setNeedsUpdateConstraints:告知需要更新約束,但是不會立刻開始

updateConstraintsIfNeeded:告知立刻更新約束

updateConstraints:系統更新約束

2、CALayer

- init()

(1)默認為無色,不會顯示。要想讓繪制的圖形顯示出來,還需要設置圖形的顏色。注意不能直接使用UI框架中的類

(2)在自定義layer中的 -(void)drawInContext:方法不會自己調用,只能自己通過setNeedDisplay方法調用,在view中畫東西DrawRect:方法在view第一次顯示的時候會自動調用。

- init(layer: Any)

這個初始值設定項CoreAnimation用來創建陰影的副本層,

如用作表示層。子類可以重寫這個方法來將自己的實例變量復制到演示層(子類應該調用超類之后)。

調用這個方法在其他任何情況下將導致未定義的行為。

presentationLayer、modelLayer

CALayer中存在三個tree,他們分別是:

Model Tree
Presentation Tree
Render Tree

Model Tree代表CALayer的真實屬性,Presentation Tree對應動畫過程中的屬性。無論動畫進行中還是已經結束,Model Tree都不會發生變化,變化的是Presentation Tree。而動畫結束后,Presentation Tree就被重置回到了初始狀態。為了讓其保持旋轉狀態,需要在加兩句代碼:

layer.fillMode=kCAFillModeForwards;
layer.removedOnCompletion=NO;

zPosition

決定層級,zPosition的數值相當于層在垂直屏幕的Z軸 上的位移值。在沒有經過任何Transform的2D環境下,zPosition僅僅會決定誰覆蓋誰,具體差值是沒有意義的,但是經過3D Transform,他們之間的差值,也就是距離,會顯現出來。

我們寫個測試:

CGRect frame = CGRectInset(self.view.bounds, 50, 50);
CALayer *layer = [CALayer layer];
layer.frame = frame;
[self.view.layer addSublayer:layer];
//第一個橢圓 藍色
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.contentsScale = [UIScreen mainScreen].scale;
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, NULL, layer.bounds);
shapeLayer.path = path;
shapeLayer.fillColor = [UIColor blueColor].CGColor;
shapeLayer.zPosition = 40;
[layer addSublayer:shapeLayer];

//第二個橢圓 綠色
CAShapeLayer *shapeLayer2 = [CAShapeLayer layer];
shapeLayer2.contentsScale = [UIScreen mainScreen].scale;
CGMutablePathRef path2 = CGPathCreateMutable();
CGPathAddEllipseInRect(path2, NULL, layer.bounds);
shapeLayer2.path = path2;
shapeLayer2.fillColor = [UIColor greenColor].CGColor;
shapeLayer2.zPosition = 0;
[layer addSublayer:shapeLayer2];

//背景矩形
CALayer *backLayer = [CALayer layer];
backLayer.contentsScale = [UIScreen mainScreen].scale;
backLayer.backgroundColor = [UIColor grayColor].CGColor;
backLayer.frame = layer.bounds;
backLayer.zPosition = -40;
[layer addSublayer:backLayer];
    
//Identity transform
CATransform3D transform = CATransform3DIdentity;
//Perspective 3D
transform.m34 = -1.0 / 700;
//旋轉
transform = CATransform3DRotate(transform, M_PI / 3, 0, 1, 0);
//設置CALayer的sublayerTransform,注意3D Transform是設置在父CALayer的sublayerTransform屬性上的,而不
//是transform屬性。因為Transform需要設置給每個子Layer,而如果設置transform屬性的話,會把整個Layer當做一
//個整體去變換
layer.sublayerTransform = transform;

上面代碼分別做三次測試:

1、如上代碼,結果如下圖 左

2、注釋掉 transform.m34 這行代碼,結果如下圖 中

3、取消 transform 的設置,結果如下圖 右

image

分析以上結果:

我們從第三次測試和第二次測試可以看到:

1、zPosition影響了原有的按先后添加順序的層次(藍色覆蓋在了綠色、灰色上面)

2、zPosition的體現, 數值越大層級越高。

anchorPoint

是一個CGPoint值,x,y取值范圍(0~1),默認為(0.5,0.5) 對于圖層本身而言,顧名思義,錨點就用來定位圖層的點。

錨點有兩個職能:

1)與position一同確定圖層相對于父圖層的位置;

2)作為圖層旋轉、平移、縮放的中心。

錨點 默認為(0.5,0.5),即邊界矩形的中心。

transform :CATransform3D

CATransform3D 的數據結構定義了一個同質的三維變換(4x4 CGFloat值的矩陣),用于圖層的旋轉,縮放,偏移,歪斜和應用的透視。

CALayer的2個屬性指定了變換矩陣:transform 和 sublayerTransform。

transform : 是結合 anchorPoint(錨點)的位置來對圖層和圖層上的子圖層進行變化。

sublayerTransform:是結合anchorPoint(錨點)的位置來對圖層的子圖層進行變化,不包括本身。

CATransform3DIdentity 是單位矩陣,該矩陣沒有縮放,旋轉,歪斜,透視。該矩陣應用到圖層上,就是設置默認值。

分析一下CATransform3D的結構:iOS CATransform3D

CATransform3D 函數

//-----平移
//返回一個平移變換的transform3D對象 tx,ty,tz對應x,y,z軸的平移
CATransform3D CATransform3DMakeTranslation (CGFloat tx, CGFloat ty, CGFloat tz);
//在某個transform3D變換的基礎上進行平移變換,t是上一個transform3D,其他參數同上
CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz);


//-----縮放
//x,y,z分別對應x軸,y軸,z軸的縮放比例
CATransform3D CATransform3DMakeScale (CGFloat sx, CGFloat sy, CGFloat sz);
//在一個transform3D變換的基礎上進行縮放變換,其他參數同上
CATransform3D CATransform3DScale (CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz);


//-----旋轉
//angle參數是旋轉的角度 ,x,y,z決定了旋轉圍繞的中軸,取值為-1 — 1之間,如(1,0,0),則是繞x軸旋轉,(0.5,0.5,0),則是繞x軸與y軸中間45度為軸旋轉
CATransform3D CATransform3DMakeRotation (CGFloat angle, CGFloat x, CGFloat y, CGFloat z);
//在一個transform3D的基礎上進行旋轉變換,其他參數如上
CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z);

接著上面的zPosition的測試我們繼續分析:

1、CALayer的 transform和sublayerTransform 屬性都是CATransform3D 類型,允許實現3D變換

2、從第一次測試和第二次測試可以看到,關于3D旋轉變化,如果不設置上面的m34屬性,整個變化結束后不會有那種透視效果(近大遠?。?/p>

m34負責z軸方向的translation(移動),m34= -1/D,  默認值是0,也就是說D無窮大。D越小透視效果越明顯。 所謂的D,是eye(觀察者)到投射面的距離。

masksToBounds、mask

masksToBounds:是否遮蓋越界部分Layer,比如常用于邊角等

mask:類似于UIView中的 maskView

contents、contentsRect、contentsGravity、contentsScale

1、 CALayer 有一個屬性叫做contents,這個屬性的類型被定義為id,意味著它可以是任何類型的對象。在這種情況下,你可以給contents屬性賦任何值,你的app仍然能夠編譯通過。但是,在實踐中,如果你給contents賦的不是CGImage,那么你得到的圖層將是空白的。 

2、 事實上,你真正要賦值的類型應該是CGImageRef,它是一個指向CGImage結構的指針。UIImage有一個CGImage屬性,它返回一個”CGImageRef”,如果你想把這個值直接賦值給CALayer的contents,那你將會得到一個編譯錯誤。因為CGImageRef并不是一個真正的Cocoa對象,而是一個Core Foundation類型。 

盡管Core Foundation類型跟Cocoa對象在運行時貌似很像(被稱作toll-free bridging),他們并不是類型兼容的,不過你可以通過bridged關鍵字轉換。 
所以要為CALayer圖層設置寄宿圖片屬性的最終代碼: 
layer.contents = (__bridge id)image.CGImage; 


contentsGravity:類似于UIView的contentMode

contentsScale: 類似于UIView的sacle

contentsCenter

image
圖片拉伸

用過xcode應該都知道 圖片的slicing功能


因此我們要設置好拉伸的部位,下圖中黑色框中位置就是 contentsCenter 的(x, y) 值的占比,綠色部分的長寬就是要拉伸的部分。拉伸的寬高為占比。


一定要設置 view.layer.contentsScale = image.scale,否則圖片在Retina 設備會顯示不正確

shadowColor、shadowOpacity、shadowOffset、shadowRadius

self.startButton.layer.borderWidth = 1;//按鈕邊緣寬度
self.startButton.layer.borderColor = [[UIColor whiteColor] CGColor];  //按鈕邊緣顏色
self.startButton.layer.shadowColor = [UIColor blackColor].CGColor; //按鈕陰影顏色
self.startButton.layer.shadowOffset = CGSizeMake(3,3); //按鈕陰影偏移量 正負值確認偏移方向
self.startButton.layer.shadowOpacity = 1; // 陰影的透明度,默認是0   范圍 0-1 越大越不透明

- (void)setNeedsDisplay; - (void)setNeedsDisplayInRect:(CGRect)rect;

設置需要渲染,當運行循環開始就會去更新已標記的layer

問題

1、UIView 自動布局的時候,frame的變化過程,

2、layer繪制的時候默認不會處理scale的問題

在CALayer中繪制圖形會出現鋸齒和模糊,同樣繪圖在UIView中就沒有問題。經查資料發現不自動處理兩倍像素的情況。

解決方案為:設置layer的contentsScale屬性為[[UIScreen mainScreen] scale];

或者復寫drawRect方法也有效

UIView 和 CALayer的聯系

前文整理了UIView和CALayer的使用方法,下面我們通過舉例測試來比較、分析UIView和CALayer之間的聯系和差異。

UIView和CALayer在構建方面的聯系

我們新增兩個類:DemoView:UIView 和 DemoLayer:CALayer

1、重新設置DemoView的rootLayer 的layerClass為DemoLayer(rootLayer:view的默認layer)

DemoView中
+(Class)layerClass {
    return [DemoLayer class];
}

2、在DemoLayer初始化 init方法行打上斷點,打印堆棧信息

0 - [DemoLayer init]
1 - [UIView _createLayerWithFrame:]
2 - UIViewCommonInitWithFrame
3 - [UIView initWithFrame]
4 - [DemoView initWithFrame]

從上面堆棧信息我們可以看到,當我們初始化DemoView的時候,會自動調用 _createLayerWithFrame 方法創建rootLayer

3、layer創建完成后,我們重寫一些方法來檢查一下UIView和CALayer在幾個基礎屬性上面的聯系

UIView : frame、bounds、center

CALayer : frame、bounds、position

重寫上述屬性在DemoLayer和DemoView中的 setter 方法,然后在ViewController中初始化添加DemoView:

ViewController中:
DemoView *view1 = [[DemoView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)];
[self.view addSubview:view1];

執行結果:
DemoLayer - setBounds
DemoView - setFrame 開始
DemoLayer - setFrame
DemoLayer - setPosition
DemoLayer - setBounds
DemoView - setFrame 結束

view1.frame = CGRectMake(0, 0, 150, 150); 動態修改frame執行結果:
DemoView - setFrame 開始
DemoLayer - setFrame
DemoLayer - setPosition
DemoLayer - setBounds
DemoView - setFrame 結束

分析上面執行結果的順序:

我們對DemoView的frame設置中執行了對DemoLayer的frame、position、bounds的設置,并且沒有執行DemoView中的center和bounds的設置。

繼續測試:執行UIView的bounds 和 center的修改、屬性的獲取:

修改center 
DemoView - setCenter
DemoLayer - setPosition

修改bounds
DemoView - setBounds
DemoLayer - setBounds

frame的獲取
DemoView - frame
DemoLayer - frame
...

分析上面執行的結果可知:

1、DemoView中的frame、bounds、center屬性的setter方法執行了DemoLayer中的對應屬性(center -> position)的setter方法

frame屬于派生屬性,依賴于 bounds、 anchorPoint、transform 和 position

當我們設置frame的時候,默認會執行DemoView的setFrame、CALayer的setFrame - setPosition - setBounds

2、DemoView中的frame、bounds和center 的 getter方法,UIView并沒有做什么工作,只是簡單的各自調用它底層的CALayer的frame,bounds和position方法。

注意:

bounds 和 frame的區別: bounds原點默認 (0,0)基于view本身的坐標系統,frame原點基于父視圖中的位置

UIView和CALayer在繪制方面的聯系

接著上面的demo測試:

重寫DemoView中的 drawRect、drawLayer:inContext:方法,用來畫一條線

重寫DemoLayer中的drawInContext方法

DemoView中:
-(void)drawRect:(CGRect)rect {
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    //畫線
    //ctx 的備份
    CGContextSaveGState(ctx);
    
    CGContextSetLineWidth(ctx, 5);
    CGContextSetLineCap(ctx, kCGLineCapRound);
    CGContextSetLineJoin(ctx, kCGLineJoinRound);
    CGContextSetRGBStrokeColor(ctx, 1, 1, 0, 1);
    CGContextMoveToPoint(ctx, 0, 120);    //起點
    CGContextAddLineToPoint(ctx, 200, 120); //畫線
    CGContextStrokePath(ctx);
}

-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    [super drawLayer:layer inContext:ctx];
}

DemoLayer中:
-(void)drawInContext:(CGContextRef)ctx {
    [super drawInContext:ctx];
}

這里做個測試:

結果:
1、正常繪制出一條線,執行順序是 
DemoView - drawRect:
DemoView - drawLayer:inContext:
DemoLayer - drawInContext:

同時打印成功執行測試的執行堆棧:
0 - DemoView drawRect:
1 - [UIView(CALayerDelegate) drawLayer:inContext:]
2 - [CALayer drawInContext:]
3 - [DemoLayer drawInContext:]

注釋掉 drawInContext 中的super調用 再做一次測試:

結果: 不能正常繪制,執行順序是 
DemoLayer - drawInContext
//并不會執行到DemoView

注釋掉 drawLayer:inContext: 中的super調用 再做一次測試:

結果是: 不能正常繪制,執行順序是 
DemoLayer - drawInContext
DemoView - drawLayer:inContext:

//不會執行到 DemoView - drawRect:

分析以上結果:

UIView實現了CALayerDelegate代理,rootLayer的代理就是DemoView,所以會執行 drawLayer:inContext: 方法,由上面的測試結果,我們可以推斷一下,DemoView的drawRect 方法的執行是在 drawLayer:inContext: 方法的過程中完成的。

我們繼續測試:先修改DemoView和DemoLayer中的代碼:

DemoLayer中:
-(void)drawInContext:(CGContextRef)ctx {
//    [super drawInContext:ctx];
    CGContextSaveGState(ctx);

    CGContextSetLineWidth(ctx, 5);
    CGContextSetLineCap(ctx, kCGLineCapRound);
    CGContextSetLineJoin(ctx, kCGLineJoinRound);
    CGContextSetRGBStrokeColor(ctx, 0, 1, 0, 1);
    CGContextMoveToPoint(ctx, 0, 120);    //起點
    CGContextAddLineToPoint(ctx, 200, 120); //畫線
    CGContextStrokePath(ctx);
}
DemoView中:
-(void)drawRect:(CGRect)rect {
}

結果:成功繪制一條線

分析以上結果:

1、CALayer 和 UIView 中都可以根據上下文進行繪制,UIView的drawRect依賴 CALayer 傳遞過來的上下文才能執行

2、CALayer 繪制并不依賴UIView,所以如果 drawRect 中沒有調用super 并不會影響layer中的繪制

3、如果layer中的 drawInContext 中 沒有 super調用,view中的drawRect也無法繪制

4、如果view中的 drawLayer:inContext: 中沒有super調用,view中的drawRect也無法繪制

UIView 和 CALayer的區別

UIView可以響應用戶事件、而 CALayer不能

UIView繼承自UIResponder, 在 UIResponder中定義了處理各種事件和事件傳遞的接口, 而 CALayer直接繼承 NSObject,并沒有相應的處理事件的接口。

UIKit使用UIResponder作為響應對象,來響應系統傳遞過來的事件并進行處理。
UIApplication、UIViewController、UIView、和所有從UIView派生出來的UIKit類(包括UIWindow)都直接或間接地繼承自UIResponder類。

UIView 和 CALayer 在基礎屬性上的區別

前面基礎介紹有描述

UIView 和 CALayer在動畫中的區別

在做 iOS 動畫的時候,修改非 RootLayer的屬性,會默認產生隱式動畫,而修改UIView則不會。

官方文檔

可動畫屬性:在Api屬性說明中 有 Animatable 結尾的都是可動畫屬性,屬性的變化都會產生隱式動畫。

隱式動畫實現原理:

做一個測試: 1、2 號針對rootLayer ; 3、4號針對 非rootLayer ; 5、6號針對UIView屬性變更

DemoView中重寫actionForLayer:forKey:
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
    id<CAAction> action = [super actionForLayer:layer forKey:event];
    NSLog(@"action for layer: %@, for key:%@ is %@", layer, event, action);
    return action;
}
DemoLayer中重寫 addAnimation:forKey
-(void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
    NSLog(@"anim : %@, for key:%@", anim, key);
    [super addAnimation:anim forKey:key];
}

事件觸發:
1:
self.layer.position = CGPointMake(120, 120);
2:
[UIView animateWithDuration:0.3 animations:^{
    self.layer.position = CGPointMake(120, 120);
}];
3:
self.otherLayer.position = CGPointMake(120, 120);
4:
[UIView animateWithDuration:0.3 animations:^{
    self.otherLayer.position = CGPointMake(120, 120);
}];
5:
self.center = CGPointMake(120, 120);
6:
[UIView animateWithDuration:0.3 animations:^{
    self.center = CGPointMake(120, 120);
}];

self.layer是rootLayer、self.otherLayer是加在self.layer上的非rootLayer
結果:
1:
action for layer: <DemoLayer: 0x60300009e260>, for key:position is <null>
anim無輸出
2:
action for layer: <DemoLayer: 0x60300009cc10>, for key:position is <_UIViewAdditiveAnimationAction: 0x6030000b5330>
anim : <CABasicAnimation: 0x6030000b5000>, for key:position
3:
action無輸出
anim : <CABasicAnimation: 0x6030000b66e0>, for key:position
4:
action無輸出
anim : <CABasicAnimation: 0x6030000b4e20>, for key:position
5:
action for layer: <DemoLayer: 0x60300009cbe0>, for key:position is <null>
anim無輸出
6:
action for layer: <DemoLayer: 0x60300009ca60>, for key:position is <_UIViewAdditiveAnimationAction: 0x6030000bc6b0>
anim : <CABasicAnimation: 0x6030000bc380>, for key:position

初始化DemoView時候的輸出:
action for layer: <DemoLayer: 0x60300009c670>, for key:bounds is <null>
action for layer: <DemoLayer: 0x60300009c670>, for key:opaque is <null>
action for layer: <DemoLayer: 0x60300009c670>, for key:position is <null>
action for layer: <DemoLayer: 0x60300009c670>, for key:sublayers is <null>
action for layer: <DemoLayer: 0x60300009c670>, for key:onOrderIn is <null>

分析測試結果:

1、從1、2、5、6號輸出結果來看,view.layer正如官方文檔中所寫:每一個view.layer都以該view作為其delegate,并通過詢問view的actionForLayer:forKey:方法來獲得自己應該執行的CAAction對象。

2、從2、6輸出結果我們可以看到、返回的action是 _UIViewAdditiveAnimationAction這么一個action,然后再有animation被添加到layer中,從上也可以看出來UIView的動畫,屬于對CAAnimation的一層封裝。

3、從3、4號輸出結果,我們看到除了rootLayer之外的layer屬性變化就不再經過UIView這一層的action獲取,而是直接由layer層進行動畫添加。

去除CALayer隱式動畫:

[CATransaction begin];
[CATransaction setDisableActions:YES];
//要去掉動畫的操作
self.otherLayer.position = CGPointMake(120, 120);
[CATransaction commit];

總結

1、每個 UIView 內部都有一個 CALayer 在背后提供內容的繪制和顯示,并且 UIView 的尺寸樣式都由內部的 Layer 所提供。

2、兩者都有樹狀層級結構,layer 內部有 SubLayers,View 內部有 SubViews。但是 Layer 比 View 多了個anchorPoint

3、UIView的frame、bounds、center基礎屬性都獲取于view.layer的基礎屬性,setter方法也會調用view.layer的setter方法

4、CALayer 和 UIView中都可以根據上下文進行繪制,UIView的drawRect依賴CALayer傳遞過來的上下文才能執行、CALayer繪制并不依賴UIView,只依賴UIView進行展示

5、在做 iOS 動畫的時候,修改非 RootLayer的屬性,會默認產生隱式動畫,而修改UIView則不會。

參考

iOS圖形渲染分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容