可能你很難相信 UIScrollView 和一個標準的 UIView 差別並不大,scroll view 確實會多出一些方法,但這些方法只是和 UIView 的屬性很好的結合到一塊兒了。所以,在要想弄懂 UIScrollView 是怎麼工做以前,你須要先了解一下 UIView,特別是視圖渲染的兩步過程。html
渲染過程的第一部分是衆所周知的光柵化(rasterization
),光柵化簡單的說就是產生一組繪圖指令而且生成一張圖片。好比繪製一個圓角矩形、帶圖片、標題居中的 UIButtons。這些圖片並無被繪製到屏幕上去;取而代之的是,他們被本身的視圖保持着留到下一個步驟使用。ios
一旦每一個視圖都產生了本身的光柵化圖片,這些圖片便被一個接一個的繪製,併產生一個屏幕大小的圖片,這即是上文所說的組合。視圖層級(view hierarchy)對於組合如何進行扮演了很重要的角色:一個視圖的圖片被組合在它父視圖的圖片上面。而後,組合好的圖片被組合到父視圖的父視圖圖片上面。視圖層級最頂端是窗口(window),它組合好的圖片即是咱們看到的東西了。bash
概念上,依次在每一個視圖上放置獨立分層的圖片並最終產生一個圖片,單調的圖像更容易被理解,特別是若是你之前使用過像 Photoshop 這樣的工具。咱們還有另一篇文章詳細解釋了像素是如何繪製到屏幕上去的。app
如今,回想一下,每一個視圖都有一個 bounds 和 frame。當佈局一個界面時,咱們須要處理視圖的 frame。這容許咱們放置並設置視圖的大小。視圖的 frame 和 bounds 的大小一般是同樣的(雖然能夠被 transforms 改變),可是他們的 origin 常常是不一樣的。弄懂這兩個工做原理是理解 UIScrollView 的關鍵。iphone
在光柵化步驟中,視圖並不關心即將發生的組合步驟。也就是說,它並不關心本身的 frame (這是用來放置視圖的圖像)或本身在視圖層級中的位置(這是決定組合的順序)。這時視圖只關心一件事就是繪製它本身的 content。這個繪製發生在每一個視圖的 drawRect:
方法中。工具
在 drawRect:
方法被調用前,會爲視圖建立一個空白的圖片來繪製 content。這個圖片的座標系統是視圖的 bounds。幾乎每一個視圖 bounds 的 origin 都是 {0,0}。所以,當在光柵化圖片左上角繪製一些東西的時候,你都會在 bounds 的 origin {x:0, y:0} 處繪製。在一個圖片右下角的地方繪製東西的時候,你都會繪製在 {x:width, y:height} 處。若是你的繪製超出了視圖的 bounds,那麼超出的部分就不屬於光柵化圖片的部分了,而且會被丟棄。 佈局
考慮一下組合圖片背後的公式。視圖圖片的左上角會根據它 frame 的 origin 進行偏移,並繪製到父視圖的圖片上:ui
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
複製代碼
正如以前所說的,若是一個視圖 bounds 的 origin 是 {0,0}。那麼,咱們獲得這個公式:spa
CompositedPosition.x = View.frame.origin.x;
CompositedPosition.y = View.frame.origin.y;
複製代碼
咱們能夠經過幾個不一樣的 frames 看一下: ![這樣作是有道理的,咱們改變 button 的 frame.origin後,它會改變本身相對紫色父視圖的位置。注意,若是咱們移動 button 直到它的一部分已經在紫色父視圖 bounds 的外面,當光柵化圖片被截去時這部分也將會經過一樣的繪製方式被截去。然而,技術上講,由於 iOS 處理組合方法的緣由,你能夠將一個子視圖渲染在其父視圖的 bounds 以外,可是光柵化期間的繪製不可能超出一個視圖的 bounds。code
Scroll View 的 Content Offset 如今咱們所講的跟 UIScrollView 有什麼關係呢?一切都和它有關!考慮一種咱們能夠實現的滾動:咱們有一個拖動時 frame 不斷改變的視圖。這達到了相同的效果,對嗎?若是我拖動個人手指到右邊,那麼拖動的同時我增大視圖的 origin.x ,瞧,這貨就是 scroll view。
固然,在 scroll view 中有不少具備表明性的視圖。爲了實現這個平移功能,當用戶移動手指時,你須要時刻改變每一個視圖的 frames。當咱們提到組合一個 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}。獲得下圖:
scroll view.bounds
的 origin。事實上,contentOffset 甚至不是實際存在的。代碼看起來像這樣:
- (void)setContentOffset:(CGPoint)offset
{
CGRect bounds = [self bounds];
bounds.origin = offset;
[self setBounds:bounds];
}
複製代碼
注意前一個圖例,只要足夠的改變 bounds 的 origin,button 將會超出紫色視圖和 button 組合成的圖片的範圍。這也是當你足夠的移動 scroll view 時,一個視圖會消失!
如今,最難的部分已通過去了,咱們再看看 UIScrollView 另外一個屬性:contentSize。 scroll view 的 content size 並不會改變其 bounds 的任何東西,因此這並不會影響 scroll view 如何組合本身的子視圖。反而,content size 定義了可滾動區域。scroll view 的默認 content size 爲 {w:0, h:0}。既然沒有可滾動區域,用戶是不能夠滾動的,可是 scroll view 仍然會顯示其 bounds 範圍內全部的子視圖。 當 content size 設置爲比 bounds 大的時候,用戶就能夠滾動視圖了。你能夠認爲 scroll view 的 bounds 爲可滾動區域上的一個窗口:
content offset 的最大值是 content size 和 scroll view size 的差(不一樣於 content size 和scroll view的 bounds 大小)。這也在情理之中:從左上角一直滾動到右下角,用戶中止時,滾動區域右下角邊緣和滾動視圖 bounds 的右下角邊緣是齊平的。你能夠像這樣記下 content offset 的最大值:
contentOffset.x = contentSize.width - bounds.size.width;
contentOffset.y = contentSize.height - bounds.size.height;
複製代碼
contentInset 屬性能夠改變 content offset 的最大和最小值,這樣即可以滾動出可滾動區域。它的類型爲 UIEdgeInsets,包含四個值:{top,left,bottom,right}。當你引進一個 inset 時,你改變了 content offset 的範圍。好比,設置 content inset 頂部值爲 10,則容許 content offset 的 y 值達到 -10。這介紹了可滾動區域周圍的填充。
當你想要使用 UIRefreshControl 實現拉動刷新時發生了什麼?你不能在 table view 的可滾動區域內放置 UIRefreshControl,不然,table view 將會容許用戶經過 refresh control 中途中止滾動,而且將 refresh control 的頂部彈回到視圖的頂部。所以,你必須將 refresh control 放在可滾動區域上方。這將容許首先將 content offset 彈回第一行,而不是 refresh control。
可是等等,若是你經過滾動足夠多的距離初始化 pull-to-refresh 機制,由於 table view 設置了 content inset,這將容許 content offset 將 refresh control 彈回到可滾動區域。當刷新動做被初始化時,content inset 已經被校訂過,因此 content offset 的最小值包含了完整的 refresh control。當刷新完成後,content inset 恢復正常,content offset 也跟着適應大小,這裏並不須要爲content size 作數學計算。(這裏可能比較難理解,建議看看 EGOTableViewPullRefresh 這樣的類庫就應該明白了)
如何在本身的代碼中使用 content inset?當鍵盤在屏幕上時,有一個很好的用途:你想要設置一個緊貼屏幕的用戶界面。當鍵盤出如今屏幕上時,你損失了幾百個像素的空間,鍵盤下面的東西全都被擋住了。
如今,scroll view 的 bounds 並無改變,content size 也並無改變(也不須要改變)。可是用戶不能滾動 scroll view。考慮一下以前一個公式:content offset 的最大值是 content size 和 bounds 的差。若是他們相等,如今 content offset 的最大值是 {x:0, y:0}.
如今開始出絕招,將界面放入一個 scroll view。scroll view 的 content size 仍然和 scroll view 的 bounds 同樣大。當鍵盤出如今屏幕上時,你設置 content inset 的底部等於鍵盤的高度。
希望這能讓你理解一些滾動視圖內部工做的原理,你對縮放感興趣?好吧,咱們今天不會談論它,可是這兒有一個有趣的小竅門:檢查 viewForZoomingInScrollView:
方法返回視圖的 transform 屬性。你將再次發現 scroll view 只是聰明的利用了 UIView 已經存在的屬性。
相關連接(強烈推薦):