可能你很難相信 UIScrollView 和一個標(biāo)準(zhǔn)的 UIView 差異并不大诫尽,scroll view 確實會多出一些方法哗蜈,但這些方法只是和 UIView 的屬性很好的結(jié)合到一起了。因此盗似,在要想弄懂 UIScrollView 是怎么工作之前拄氯,你需要先了解一下 UIView,特別是視圖渲染的兩步過程酌心。
光柵化和組合
渲染過程的第一部分是眾所周知的光柵化(rasterization),光柵化簡單的說就是產(chǎn)生一組繪圖指令并且生成一張圖片灸撰。比如繪制一個圓角矩形、帶圖片拼坎、標(biāo)題居中的 UIButtons浮毯。這些圖片并沒有被繪制到屏幕上去;取而代之的是泰鸡,他們被自己的視圖保持著留到下一個步驟使用债蓝。
一旦每個視圖都產(chǎn)生了自己的光柵化圖片,這些圖片便被一個接一個的繪制盛龄,并產(chǎn)生一個屏幕大小的圖片饰迹,這便是上文所說的組合。視圖層級(view hierarchy)對于組合如何進(jìn)行扮演了很重要的角色:一個視圖的圖片被組合在它父視圖的圖片上面余舶。然后啊鸭,組合好的圖片被組合到父視圖的父視圖圖片上面。視圖層級最頂端是窗口(window)匿值,它組合好的圖片便是我們看到的東西了赠制。
概念上,依次在每個視圖上放置獨立分層的圖片并最終產(chǎn)生一個圖片挟憔,單調(diào)的圖像更容易被理解钟些,特別是如果你以前使用過像 Photoshop 這樣的工具。我們還有另外一篇文章詳細(xì)解釋了像素是如何繪制到屏幕上去的绊谭。
現(xiàn)在政恍,回想一下,每個視圖都有一個 bounds 和 frame达传。當(dāng)布局一個界面時篙耗,我們需要處理視圖的 frame迫筑。這允許我們放置并設(shè)置視圖的大小。視圖的 frame 和 bounds 的大小通常是一樣的(雖然可以被 transforms 改變)鹤树,但是他們的 origin 經(jīng)常是不同的铣焊。弄懂這兩個工作原理是理解 UIScrollView 的關(guān)鍵。
在光柵化步驟中罕伯,視圖并不關(guān)心即將發(fā)生的組合步驟曲伊。也就是說,它并不關(guān)心自己的 frame (這是用來放置視圖的圖像)或自己在視圖層級中的位置(這是決定組合的順序)追他。這時視圖只關(guān)心一件事就是繪制它自己的 content坟募。這個繪制發(fā)生在每個視圖的 drawRect: 方法中。
在 drawRect: 方法被調(diào)用前邑狸,會為視圖創(chuàng)建一個空白的圖片來繪制 content懈糯。這個圖片的坐標(biāo)系統(tǒng)是視圖的 bounds。幾乎每個視圖 bounds 的 origin 都是 {0单雾,0}赚哗。因此,當(dāng)在光柵化圖片左上角繪制一些東西的時候硅堆,你都會在 bounds 的 origin {x:0, y:0} 處繪制屿储。在一個圖片右下角的地方繪制東西的時候,你都會繪制在 {x:width, y:height} 處渐逃。如果你的繪制超出了視圖的 bounds够掠,那么超出的部分就不屬于光柵化圖片的部分了,并且會被丟棄茄菊。
![Uploading SV2_203165.png . . .]
在組合的步驟中疯潭,每個視圖將自己光柵化圖片組合到自己父視圖的光柵化圖片上面。視圖的 frame 決定了自己在父視圖中繪制的位置面殖,frame 的 origin 表明了視圖光柵化圖片左上角相對父視圖光柵化圖片左上角的偏移量竖哩。所以,一個 origin 為 {x:20, y:15} 的 frame 所繪制的圖片左邊距其父視圖 20 點脊僚,上邊距父視圖 15 點期丰。因為視圖的 frame 和 bounds 矩形的大小總是一樣的,所以光柵化圖片組合的時候是像素對齊的吃挑。這確保了光柵化圖片不會被拉伸或縮小钝荡。
![Uploading SV1_210659.png . . .]
記住,我們才僅僅討論了一個視圖和它父視圖之間的組合操作舶衬。一旦這兩個視圖被組合到一起埠通,組合的結(jié)果圖片將會和父視圖的父視圖進(jìn)行組合,這是一個雪球效應(yīng)逛犹。
考慮一下組合圖片背后的公式端辱。視圖圖片的左上角會根據(jù)它 frame 的 origin 進(jìn)行偏移梁剔,并繪制到父視圖的圖片上:
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
正如之前所說的,如果一個視圖 bounds 的 origin 是 {0,0}舞蔽。那么荣病,我們得到這個公式:
CompositedPosition.x = View.frame.origin.x;
CompositedPosition.y = View.frame.origin.y;
我們可以通過幾個不同的 frames 看一下:
![Uploading SV3_244181.png . . .]
這樣做是有道理的,我們改變 button 的 frame.origin后渗柿,它會改變自己相對紫色父視圖的位置个盆。注意,如果我們移動 button 直到它的一部分已經(jīng)在紫色父視圖 bounds 的外面朵栖,當(dāng)光柵化圖片被截去時這部分也將會通過同樣的繪制方式被截去颊亮。然而,技術(shù)上講陨溅,因為 iOS 處理組合方法的原因终惑,你可以將一個子視圖渲染在其父視圖的 bounds 之外,但是光柵化期間的繪制不可能超出一個視圖的 bounds门扇。
現(xiàn)在我們所講的跟 UIScrollView 有什么關(guān)系呢雹有?一切都和它有關(guān)!考慮一種我們可以實現(xiàn)的滾動:我們有一個拖動時 frame 不斷改變的視圖臼寄。這達(dá)到了相同的效果霸奕,對嗎?如果我拖動我的手指到右邊脯厨,那么拖動的同時我增大視圖的 origin.x 铅祸,瞧坑质,這貨就是 scroll view合武。
當(dāng)然,在 scroll view 中有很多具有代表性的視圖涡扼。為了實現(xiàn)這個平移功能稼跳,當(dāng)用戶移動手指時,你需要時刻改變每個視圖的 frames吃沪。當(dāng)我們提到組合一個 view 的光柵化圖片到它父視圖什么地方時汤善,記住這個公式:
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
我們減少 Superview.bounds.origin 的值(因為他們總是0)。但是如果他們不為0呢票彪?我們用和前一個圖例相同的 frames红淡,但是我們改變了紫色視圖 bounds 的 origin 為 {-30, -30}。得到下圖:
![Uploading SV4_294631.png . . .]
現(xiàn)在降铸,巧妙的是通過改變這個紫色視圖的 bounds在旱,它每一個單獨的子視圖都被移動了。事實上推掸,這正是 scroll view 工作的原理桶蝎。當(dāng)你設(shè)置它的 contentOffset 屬性時它改變 scroll view.bounds 的 origin驻仅。事實上,contentOffset 甚至不是實際存在的登渣。代碼看起來像這樣:
- (void)setContentOffset:(CGPoint)offset
{
CGRect bounds = [self bounds];
bounds.origin = offset;
[self setBounds:bounds];
}
注意前一個圖例噪服,只要足夠的改變 bounds 的 origin,button 將會超出紫色視圖和 button 組合成的圖片的范圍胜茧。這也是當(dāng)你足夠的移動 scroll view 時粘优,一個視圖會消失!
世界之窗:Content Size
現(xiàn)在竹揍,最難的部分已經(jīng)過去了敬飒,我們再看看 UIScrollView 另一個屬性:contentSize。 scroll view 的 content size 并不會改變其 bounds 的任何東西芬位,所以這并不會影響 scroll view 如何組合自己的子視圖无拗。反而,content size 定義了可滾動區(qū)域昧碉。scroll view 的默認(rèn) content size 為 {w:0, h:0}英染。既然沒有可滾動區(qū)域,用戶是不可以滾動的被饿,但是 scroll view 仍然會顯示其 bounds 范圍內(nèi)所有的子視圖四康。 當(dāng) content size 設(shè)置為比 bounds 大的時候,用戶就可以滾動視圖了狭握。你可以認(rèn)為 scroll view 的 bounds 為可滾動區(qū)域上的一個窗口:
![Uploading SV5_320996.png . . .]
當(dāng) content offset 為 {x:0, y:0} 時闪金,可見窗口的左上角在可滾動區(qū)域的左上角處。這也是 content offset 的最小值论颅;用戶不能再往可滾動區(qū)域的左邊或上邊移動了哎垦。那兒沒啥,別滾了恃疯!
content offset 的最大值是 content size 和 scroll view size 的差(不同于 content size 和scroll view的 bounds 大小)漏设。這也在情理之中:從左上角一直滾動到右下角,用戶停止時今妄,滾動區(qū)域右下角邊緣和滾動視圖 bounds 的右下角邊緣是齊平的郑口。你可以像這樣記下 content offset 的最大值:
contentOffset.x = contentSize.width - bounds.size.width;
contentOffset.y = contentSize.height - bounds.size.height;
用 Content Insets 對窗口稍作調(diào)整
contentInset 屬性可以改變 content offset 的最大和最小值,這樣便可以滾動出可滾動區(qū)域盾鳞。它的類型為 UIEdgeInsets犬性,包含四個值:{top,left腾仅,bottom乒裆,right}。當(dāng)你引進(jìn)一個 inset 時攒砖,你改變了 content offset 的范圍缸兔。比如日裙,設(shè)置 content inset 頂部值為 10,則允許 content offset 的 y 值達(dá)到 -10惰蜜。這介紹了可滾動區(qū)域周圍的填充昂拂。
![Uploading SV6_342303.png . . .]
這咋一看好像沒什么用。實際上抛猖,為什么不僅僅增加 content size 呢格侯?除非沒辦法,否則你需要避免改變scroll view 的 content size财著。想要知道為什么联四?想想一個 table view(UItableView是UIScrollView 的子類,所以它有所有相同的屬性)撑教,table view 為了適應(yīng)每一個cell朝墩,它的可滾動區(qū)域是通過精心計算的。當(dāng)你滾動經(jīng)過 table view 的第一個或最后一個 cell 的邊界時伟姐,table view將 content offset 彈回并復(fù)位收苏,所以 cells 又一次恰到好處的緊貼 scroll view 的 bounds。
當(dāng)你想要使用 UIRefreshControl 實現(xiàn)拉動刷新時發(fā)生了什么愤兵?你不能在 table view 的可滾動區(qū)域內(nèi)放置 UIRefreshControl鹿霸,否則,table view 將會允許用戶通過 refresh control 中途停止?jié)L動秆乳,并且將 refresh control 的頂部彈回到視圖的頂部懦鼠。因此,你必須將 refresh control 放在可滾動區(qū)域上方屹堰。這將允許首先將 content offset 彈回第一行肛冶,而不是 refresh control。
但是等等双藕,如果你通過滾動足夠多的距離初始化 pull-to-refresh 機(jī)制淑趾,因為 table view 設(shè)置了 content inset阳仔,這將允許 content offset 將 refresh control 彈回到可滾動區(qū)域忧陪。當(dāng)刷新動作被初始化時,content inset 已經(jīng)被校正過近范,所以 content offset 的最小值包含了完整的 refresh control嘶摊。當(dāng)刷新完成后,content inset 恢復(fù)正常评矩,content offset 也跟著適應(yīng)大小叶堆,這里并不需要為content size 做數(shù)學(xué)計算。(這里可能比較難理解斥杜,建議看看 EGOTableViewPullRefresh 這樣的類庫就應(yīng)該明白了)
如何在自己的代碼中使用 content inset虱颗?當(dāng)鍵盤在屏幕上時沥匈,有一個很好的用途:你想要設(shè)置一個緊貼屏幕的用戶界面。當(dāng)鍵盤出現(xiàn)在屏幕上時忘渔,你損失了幾百個像素的空間高帖,鍵盤下面的東西全都被擋住了。
現(xiàn)在畦粮,scroll view 的 bounds 并沒有改變散址,content size 也并沒有改變(也不需要改變)。但是用戶不能滾動 scroll view宣赔≡铮考慮一下之前一個公式:content offset 的最大值是 content size 和 bounds 的差。如果他們相等儒将,現(xiàn)在 content offset 的最大值是 {x:0, y:0}.
現(xiàn)在開始出絕招吏祸,將界面放入一個 scroll view。scroll view 的 content size 仍然和 scroll view 的 bounds 一樣大钩蚊。當(dāng)鍵盤出現(xiàn)在屏幕上時犁罩,你設(shè)置 content inset 的底部等于鍵盤的高度。
![Uploading SV7_351650.png . . .]
這允許在 content offset 的最大值下顯示滾動區(qū)域外的區(qū)域两疚〈补溃可視區(qū)域的頂部在 scroll view bounds 的外面,因此被截取了(雖然它在屏幕之外了诱渤,但這并沒有什么)丐巫。
但愿這能讓你理解一些滾動視圖內(nèi)部工作的原理,你對縮放感興趣勺美?好吧递胧,我們今天不會談?wù)撍沁@兒有一個有趣的小竅門:檢查 viewForZoomingInScrollView: 方法返回視圖的 transform 屬性赡茸。你將再次發(fā)現(xiàn) scroll view 只是聰明的利用了 UIView 已經(jīng)存在的屬性缎脾。
相關(guān)鏈接(強(qiáng)烈推薦):
計算機(jī)圖形渲染的流程
原文 Understanding Scroll Views
譯文 理解Scroll View - answer-huang