Core Animation
iOS系統的核心地位:應用內和應用間都會應用(多個程序間的手勢切換)SpringBoard→BackBoard。
應用內的工作:Core Animation Pipeline管線(layout,display,preload,commit)。
應用外的工作:提交到Render Server服務,反序列化構建渲染樹GPU的工作:提交到GPU進行合成和渲染,輸出到緩沖區。
注1:代碼只能控制CPU處理中的管線中的布局和顯示兩個階段(通過setNeedLayout和setNeedDisplay)(setNeedDisplayInRect還能設置臟矩形來減少不必要的繪制)。
注2:CPU偏計算;而圖像的處理和渲染,用硬件會更快,因為GPU對圖像計算進行了優化。
隱式動畫
概念:更改一個CALayer的可做動畫屬性,Core Animation會執行一個隱式動畫,平滑過渡到新值。動畫的執行時間取決于事務的設置,動畫的類型取決于圖層行為。
實現:layer屬性更改時會調用actionForKey方法,然后執行actionForLayer:forKey:代理方法(UIView禁用隱式動畫的原理),如果沒有實現代理,就查找屬性-名稱的actions映射字典和style字典,最后直接調defaultActionForKey。查找完actionForKey方法會返回一個CAAction做動畫或NULL不做動畫。
時機:由事務的管理機制控制。
注:UIView更改默認沒有隱式動畫,因為UIKit禁用了:UIView實現了CALayerDelegate的-actionForLayer:forKey方法,當屬性的更改不在一個動畫塊中時,所有圖層行為都會返回null,以此來禁用隱式動畫。
顯式動畫
CABasicAnimation和CAKeyframeAnimation
虛擬屬性:keyPath = "transform.scale .position .rotation",不用創建CATransform3D指定動畫變換。
[layer removeAnimationForKey];? // 通過key在動畫過程中取消動畫。
動畫的暫停
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
動畫的繼續
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
事務
事務是一系列屬性動畫集合,通過CATransaction管理事務組。CATransaction沒有屬性和實例方法,只能通過+begin和+commit管理出入棧,+setAnimationDuration管理事務的動畫時長和+animationDuration獲取值(默認0.25),setCompletionBlock:可以設置事務完成后的回調。
事務的管理機制
每個RunLoop周期都會自動開始一個新的事務,把更改的屬性收集起來做一次默認0.25s的隱式動畫。
注:一般手動更改隱式動畫的時間,需要新開事務避免影響別的動畫(如屏幕旋轉)。
UIView的動畫
UIView的begin/commitAnimation底層原理也是設置了CATransaction。
UIView的animateWithDuration:animations:的Block動畫方法其實也是內部自動調用了CATransaction的begin和commit,這樣Block中的屬性改變都會被事務所包含。
呈現樹
presentationLayer其實更改圖層屬性后,屬性值立刻更新,但屏幕沒有馬上發生改變,因為它只是定義了圖層動畫結束后將要變化的外觀,即圖層樹模型。但在設置新值后和新值生效前,屏幕渲染的過程中,當前顯示的屬性值,在某些場景下更有意義:1、在基于定時器的動畫過程中獲取當前圖層的準確位置;2、在做動畫的圖層中,通過hitTest響應用戶交互。(touchesBegan/Target-Action中,判斷觸摸點是否在presentationLayer內)
注:關鍵方法:locationInView / convertPoint->Point轉化觸摸點坐標系,presentationLayer -> CALayer獲取呈現樹中的視圖位置,hitTest -> CALayer/ Contains-> Bool返回觸摸的Layer / 判斷觸摸點是否在Layer上。
圖片IO
imageWithContentsOfFile方法加載本地目錄的圖片文件,會在加載到內存之后才進行解碼解壓,所以會在繪制的時候影響性能,且不會進行內存緩存,適用于讀取一次性大圖的場景imageNamed可以避免延遲加載,會馬上解壓并加載到內存中緩存起來,但是只對assets中的資源有效。
強制解壓和提前渲染
使用CGContext從上下文中繪制一個新的圖片(直接繪制成CGImageRef位圖)來代替(或繪制成一個點也會解壓但不會節省繪制時間),優化了因解壓增加的繪制時間,還可以丟棄解壓后的圖片節省內存,但需要更復雜的計算,CPU消耗總量增加。
總結:使用CGContext異步重繪圖片,得到解壓縮后的位圖。
UIView和CALayer
View持有Layer用于顯示,View可以響應交互負責管理,View的屬性大部分從Layer映射而來,UIView支持自動布局;Layer的delegate是View,動畫會通知View,Layer更輕量級。
提供View和layer兩個平行的層級關系是為了職責分離,避免重復代碼。
frame和bounds
frame:外部坐標,在父圖層占據的位置bounds:內部坐標本地坐標系,作用于子視圖,左上角通常是{0, 0}(通過更改bounds實現簡單的scrollView)
注:frame是一個虛擬屬性,是根據bounds+position+transform計算而來;當對圖層進行旋轉或縮放時,frame實際是變換后,補齊的正矩形區域,即寬高可能和bounds不一致。
anchorPoint
錨點,UIView未暴露的屬性,默認中心{0.5, 0.5},左上角{0, 0} 右下角{1, 1},小于0大于1將放置在圖層外。UIView的Center和CALayer的Position屬性加上錨點,才是圖層的實際位置,所以圖層錨點更改,frame會發生變化。
masksToBounds
Layer屬性,設置為YES截取子圖層實現圓角(cornerRadius);設為NO避免截取陰影(shadowOpacity,shadowColor,shadowOffset,shadowRadius,shadowPath-CGPath)。
mask蒙版
Layer屬性,定義了父視圖的可見區域,mask圖層的顏色不重要,實心部分將會保留,其他隱藏
CGAffineTransform
UIView 仿射變換,僅支持2D變換:MakeRotation,Scale,Translation等。
CATransform3D
CALayer 3D變換,方法和仿射變換類似,加入zPosition形成4*4矩陣。
CAShapeLayer
基于CGPath路徑創建CAShapeLayer矢量圖形(非CG的Bitmap),bezierPathWithRoundedRect: byRoundingCorners: cornerRadii:可以單獨指定每個角的圓角,作為蒙版為宿主層加圓角(mask)。
AVPlayerLayer
AVFoundation提供,和CA緊密結合,是CALayer的子類。AVPlayer + AVPlayerLayer簡單的播放視頻。
UIView的布局更新和重繪機制
-loadViewIfNeeded
VC初始化后,強制提前執行viewdidload加載控件和數據裝載。
-layoutSubviews
此函數用于,父視圖的布局、大小等發生了改變,你想要同時更新其子視圖的布局時,更新子視圖布局的代碼應寫在父類重寫后的layoutSubviews里(它默認不做任何事),但這時就不能在外部設置子視圖的布局了。
注:初始化含frame(且顯示出來)或改變frame,addSubview,滾動UIScrollView,旋轉屏幕,改變子view的大小會觸發layoutSubviews。
-setNeedsLayout
設置布局刷新標記——標記為需要重新布局。此方法會異步觸發layoutSubviews(執行完成后刷新標記又置為NO),如果想立刻觸發,應立刻調用layoutIfNeeded。
-layoutIfNeeded(寫在animation內,用于實現更新約束時的動畫效果)
view.layoutIfNeeded()后frame及坐標為約束完成后的值,可用于viewdidload中獲取準確的frame。
配合setNeedsLayout,在有刷新標記的情況下立刻觸發layoutSubviews。但在view的init方法后、第一次顯示前(addSubview前),標記是“需要刷新”的。
self.testView= [[LayoutTestViewalloc]initWithFrame:CGRectMake(0,0,300,300)];? //初始化會調用兩次setNeedsDisplay
//此時視圖標記是“需要刷新”的,直接調用[self.testView layoutIfNeeded];會觸發layoutSubviews,但之后的addSubview就不觸發了
[self.view addSubview:self.testView]; //默認觸發layoutSubviews
self.testView.frame = CGRectMake(0,0,500,500); //改變frame會再次觸發layoutSubviews()
- (void)click {
? ? ? [self.testView setNeedsLayout]; //標記為可刷新且異步觸發layoutSubviews
? ? ? [self.testView layoutIfNeeded]; //立刻觸發layoutSubviews
? ? ? //只觸發一次
}
Drawing and Updating the View
-drawRect:(CGRect)rect(內存殺手)
view首次顯示或調用setNeedsDisplay時執行。
重寫此方法,執行自定義繪制內容任務。默認不起作用。永遠不要直接調用drawRect。重寫此方法多用于UIView結合CGContextRef的手動繪圖(與CALayer對應的是drawInContext)
CGContextRef ctx =UIGraphicsGetCurrentContext(); //獲取當前畫板(圖形上下文,只在UIView的drawRect中有效)(還可以使用)
注:view重寫drawRect后如果不指定背景色或把opaque設成NO背景將變黑。
實際需求:1、在當前畫板上畫圖像,曲線、虛線、幾何圖形、寫字等等。
2、UITextView添加自定義placeholder的需求中,將holder的label添加在寫drawRect內,設置holder字符串時調用setNeedsDisplay重繪UITextView。
-setNeedDisplay
在下一個繪畫周期(1/60秒后)主動觸發drawRect執行重繪。setNeedDisplayInRect方法可限定在rect范圍內重繪。一般結合重寫drawRect來使用。