內(nèi)容來自于objc.io
可以說scroll view是iOS 生動(dòng)特性的一個(gè)非常重要的注腳,而在完全掌握它之前,你甚至?xí)幸环N夜不能寐的感覺,接下來,讓我們細(xì)細(xì)剖析scrollview
由于scrollview的特性其實(shí)只是來自于UIView 特性的疊加,所以對(duì)scrollview的理解更多來自對(duì)UIView的理解,如下是兩過程的view渲染的細(xì)節(jié):
Rasterization and Composition
第一個(gè)步驟稱為光柵化,它只是執(zhí)行一些繪制指令并產(chǎn)生一份圖像。比如按鈕只是繪制一個(gè)圓角矩形,并在中間繪制文字,這些內(nèi)容由view持有,等待交由第二個(gè)步驟使用。一旦每個(gè)view均執(zhí)行得到了光柵化圖像,則會(huì)使用稱為 合成 的過程將它們組合成屏幕大小的圖片。view層級(jí)在合成過程中起到了很重要的作用:子view會(huì)覆蓋在父view之上。最頂層的view是window,而其合成的內(nèi)容是用戶最終看到的內(nèi)容。
這個(gè)時(shí)候重點(diǎn)來了,大家都知道view均有frame和bounds屬性,它們有相同的size(除了transform屬性所帶來的影響之外),但orgin通常不一樣,理解這兩個(gè)屬性的原理就可以理解scrollView的原理。
在光柵化的過程中,view并不關(guān)心它的frame(決定view的位置和大?。┖驮趘iew層級(jí)中的位置(決定其合成的順序),而只關(guān)心自己的繪制內(nèi)容,繪制發(fā)生在每個(gè)view的drawRect方法中。
在drawRect調(diào)用之前,會(huì)為view創(chuàng)建一個(gè)空白的image以供繪制。這個(gè)image的坐標(biāo)系統(tǒng)是其bounds,如果在bounds之外繪制,那這份繪制并不會(huì)成為光柵化圖像的一部分,并會(huì)被廢棄。雖然ios底層的繪制過程使得可以將子view在superview的bounds之外渲染出來,但在光柵化的過程中,在bounds之外繪制的內(nèi)容是會(huì)被廢棄的。
Scroll View’s Content Offset
以上這些跟scrollview有什么關(guān)系呢?答案是關(guān)系大發(fā)啦。想象一下滾動(dòng)的時(shí)候發(fā)生的事情:在拖拽過程中,我們改變了view的frame,如果往右拽,會(huì)增加origin.x
之前在合成的時(shí)候計(jì)算子View在父view中的位置時(shí),父view的bounds.origin 通常是{0,0},所以子view.frame.origin即對(duì)應(yīng)父view中對(duì)應(yīng)坐標(biāo)的點(diǎn)。但如果父view的bounds.origin不為0的時(shí)候,則需要將子view.frame.origin+父view.bounds.origin。
所以更改bounds的origin可以調(diào)整子View在父view中顯示的位置,而且實(shí)際上,scrollview的contentOffset屬性即是通過調(diào)整bounds.origin完成滾動(dòng)的。
Content Size
有了關(guān)于contentOffset的理解,接下來關(guān)注下contentSize
contentSize并不會(huì)更改scrollview 的bounds,所以不會(huì)影響Scrollview合成子view。scroll view的默認(rèn)contentSize是{w:0,h:0},由于沒有可滾動(dòng)的區(qū)域,用戶不可以滾動(dòng),但scrollview仍然會(huì)在其bounds中顯示所有子view。
當(dāng)contentSize比Scrollview大的時(shí)候,才允許滾動(dòng)
上圖中visible area的bounds應(yīng)當(dāng)是{80,40,200,300}
當(dāng)contentOffset為{0,0}時(shí),可見窗口的左上角正好是可滾動(dòng)區(qū)域的左上角,這也是contentOffset的最小值,最大 contentOffset 是contentSize與Scrollview.bounds的差值。
Tweaking the Window with Content Insets
contentInset可以改變contentOffset的最大和最小值(顯示上而已,因?yàn)閏ontentOffset的最小值仍是{0,0},最大值亦不變)
contentInset看起來很有用,但為什么不直接更改contentSize呢,以UITableView為例,它已經(jīng)精準(zhǔn)地根據(jù)各Cell的情況算出了contentSize??紤]使用UIRefreshControl的情況:不能將UIRefreshControl放在可滾動(dòng)區(qū)域中,因?yàn)檫@樣會(huì)使得用戶可以滾動(dòng)經(jīng)過UIRefreshControl并停留在UIRefreshControl的上方,并無(wú)法主動(dòng)彈回到第一個(gè)Cell的上邊界。所以需要將UIRefreshControl放置在可滾動(dòng)區(qū)域的上方,這樣使得contentOffset可以彈回第一行,而不是停留在UIRefreshControl上。
等一下,當(dāng)滾動(dòng)得夠遠(yuǎn)以致于觸發(fā)了refresh時(shí),這時(shí)tableview并沒有彈回第一行隱藏refreshControl是因?yàn)槭褂昧薱ontentInset。而當(dāng)刷新結(jié)束時(shí),會(huì)恢復(fù)contentInset,此時(shí)contentOffset保持原值,且不需要對(duì)contentSize做新的計(jì)算,維持原值即可,此時(shí)view恢復(fù)到將refreshControl隱藏。
那么在代碼中什么時(shí)候應(yīng)該用到contentInset呢:一個(gè)極佳的例子是鍵盤出現(xiàn)的時(shí)候。
當(dāng)然還有zooming,今天不會(huì)討論這個(gè),但有一個(gè)有趣的地方可以注意下:從viewForZoomingInScrollView:返回的時(shí)候,檢查transform屬性,可以發(fā)現(xiàn)scrollview巧妙地運(yùn)用了UIView現(xiàn)存的屬性。