Scroll View 深刻

轉載自:http://mobile.51cto.com/hot-430409.htmphp

 

可能你很難相信,UIScrollView和一個標準的UIView差別並不大,scroll view確實會多一些方法,但這些方法只是UIView一些屬性的表面而已。所以,要想弄懂UIScrollView是怎麼工做以前,你須要瞭解 UIView,特別是視圖渲染過程的兩步。app

光柵化和組合工具

渲染過程的第一部分是衆所周知的光柵化,光柵化簡單的說就是產生一組繪圖指令而且生成一張圖片。好比繪製一個圓角矩形、帶圖片、標題居中的UIButtons。這些圖片並無被繪製到屏幕上去;取而代之的是,他們被本身的視圖保持着留到下一個步驟用。佈局

一旦每一個視圖都產生了本身的光柵化圖片,這些圖片便被一個接一個的繪製,併產生一個屏幕大小的圖片,這即是上文所說的組合。視圖層級(view hierarchy)對於組合如何進行扮演了很重要的角色:一個視圖的圖片被組合在它父視圖圖片的上面。而後,組合好的圖片被組合到父視圖的父視圖圖片上 面,就這樣。。。最終視圖層級最頂端是窗口(window),它組合好的圖片即是咱們看到的東西了。spa

概念上,依次在每一個視圖上放置獨立分層的圖片並最終產生一個圖片,單調的圖像將會變得更容易理解,特別是若是你之前使用過像Photoshop這樣的工具。咱們還有另一篇文章詳細解釋了像素是如何繪製到屏幕上去的3d

如今,回想一下,每一個視圖都有一個bounds和frame。當佈局一個界面時,咱們須要處理視圖的frame。這容許咱們放置並設置視圖的大小。 視圖的frame和bounds的大小老是同樣的,可是他們的origin有可能不一樣。弄懂這兩個工做原理是理解UIScrollView的關鍵。orm

在光柵化步驟中,視圖並不關心即將發生的組合步驟。也就是說,它並不關心本身的frame(這是用來放置視圖的圖像)或本身在視圖層級中的位置(這 是決定組合的順序)。這時視圖只關心一件事就是繪製它本身的content。這個繪製發生在每一個視圖的drawRect:方法中。htm

在drawRect:方法被調用前,會爲視圖建立一個空白的圖片來繪製content。這個圖片的座標系統是視圖的bounds。幾乎每一個視圖 bounds的origin都是{0,0}。所以,當在刪格化圖片左上角繪製一些東西的時候,你都會在bounds的origin({x:0,y:0}) 處繪製。在一個圖片右下角的地方繪製東西的時候,你都會繪製在{x:width, y:height}處。若是你的繪製超出了視圖的bounds,那麼超出的部分就不屬於刪格化圖片的部分了,而且會被丟棄。blog

在組合的步驟中,每一個視圖將本身光柵化圖片組合到本身父視圖的光柵化圖片上面。視圖的frame決定了本身在父視圖中繪製的位置,frame的 origin代表了視圖光柵化圖片左上角相對父視圖光柵化圖片左上角的偏移量。因此,一個origin爲{x:20,y:15}的frame所繪製的圖片 左邊距其父視圖20點,上邊距父視圖15點。由於視圖的frame和bounds矩形的大小老是同樣的,因此光柵化圖片組合的時候是像素對齊的。這確保了 光柵化圖片不會被拉伸或縮小。圖片

記住,咱們才僅僅討論了一個視圖和它父視圖之間的組合操做。一旦這兩個視圖被組合到一塊兒,組合的結果圖片將會和父視圖的父視圖進行組合。。。這是一個雪球效應。

考慮一下組合圖片背後的公式。視圖圖片的左上角會根據它frame的origin進行偏移,並繪製到父視圖的圖片上:

  1. CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;  
  2.    
  3. CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;  

咱們能夠經過幾個不一樣的frames看一下: 

這樣作是有道理的。咱們改變button的frame.origin後,它會改變本身相對紫色父視圖的位置。注意,若是咱們移動button直到它 的一部分已經在紫色父視圖bounds的外面,當光柵化圖片被截去時這部分也將會經過一樣的繪製方式被截去。然而,技術上講,由於iOS處理組合方法的原 因,你能夠將一個子視圖渲染在其父視圖的bounds以外,可是光柵化期間的繪製不可能超出一個視圖的bounds。

Scroll View的Content Offset

如今,咱們所講的跟UIScrollView有什麼關係呢?一切都和它有關!考慮一種咱們能夠實現的滾動:咱們有一個拖動時frame不斷改變的視 圖。這達到了相同的效果,對嗎?若是我拖動個人手指到右邊,那麼拖動的同時我增大視圖的origin.x,瞧,這貨就是scroll view。

固然,在scroll view中有不少具備表明性的視圖。爲了實現這個平移功能,當用戶移動手指時,你須要時刻改變每一個視圖的frames。當咱們提出組合一個view的光柵化圖片到它父視圖什麼地方時,記住這個公式:

  1. CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;  
  2.    
  3. CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;  

咱們減小Superview.bounds.origin的值(由於他們老是0)。可是若是他們不爲0呢?咱們用和前一個圖例相同的frames,可是咱們改變了紫色視圖bounds的origin爲{-30,-30}。獲得下圖:

如今,巧妙的是經過改變這個紫色視圖的bounds,它每個單獨的子視圖都被移動了。事實上,這正是一個scroll view工做的原理。當你設置它的contentOffset屬性時:它改變scroll view.bounds的origin。事實上,contentOffset甚至不是實際存在的。代碼看起來像這樣:

  1. - (void)setContentOffset:(CGPoint)offset  
  2. {  
  3.     CGRect bounds = [self bounds];  
  4.     bounds.origin = offset;  
  5.     [self setBounds:bounds];  
  6. }  

注意:前一個圖例,只要足夠的改變bounds的origin,button將會超出紫色視圖和button組合成的圖片的範圍。這也是當你足夠的移動scroll view時,一個視圖會消失!

世界之窗:Content Size

如今,最難的部分已通過去了,咱們再看看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爲{x:0,y:0}時,可見窗口的左上角在可滾動區域的左上角處。這也是content offset的最小值;用戶不能再往可滾動區域的左邊或上邊移動了。那兒沒啥,別滾了!

content offset的最大值是content size和scroll view size的差。這也在情理之中:從左上角一直滾動到右下角,用戶中止時,滾動區域右下角邊緣和滾動視圖bounds的右下角邊緣是齊平的。你能夠像這樣記 下content offset的最大值:

  1. contentOffset.x = contentSize.width - bounds.size.width;  
  2. contentOffset.y = contentSize.height - bounds.size.height;  

用Content Insets對窗口稍做調整

contentInset屬性能夠改變content offset的最大和最小值,這樣即可以滾動出可滾動區域。它的類型爲UIEdgeInsets,包含四個值: {top,left,bottom,right}。當你引進一個inset時,你改變了content offset的範圍。好比,設置content inset頂部值爲10,則容許content offset的y值達到10。這介紹了可滾動區域周圍的填充。

這咋一看好像沒什麼用。實際上,爲何不只僅增長content size呢?除非沒辦法,不然你須要避免改變scroll view的content size。想要知道爲何?想一想一個table view(UItableView是UIScrollView的子類,因此它有全部相同的屬性),table view爲了適應每個cell,它的可滾動區域是經過精心計算的。當你滾動通過table view的第一個或最後一個cell的邊界時,table view將content offset彈回並復位,因此cells又一次恰到好處的緊貼scroll view的bounds。

當你想要使用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的底部等於鍵盤的高度。

這容許在content offset的最大值下顯示滾動區域外的區域。可視區域的頂部在scroll view bounds的外面,所以被截取了(雖然它在屏幕以外了,但這並無什麼)。

希望這能讓你理解一些滾動視圖內部工做的原理,你對縮放感興趣?好吧,咱們今天不會談論它,可是這兒有一個有趣的小竅門:檢查 viewForZoomingInScrollView:方法返回視圖的transform屬性。你將再次發現scroll view只是聰明的利用了UIView已經存在的屬性。

相關文章
相關標籤/搜索