iOS開發-探索scrollView的實現

發表於2016-03-15

序言

UIScrollView滾動檢視,絕對算的上是iOS開發中最重要的控制元件,用來展示多於一個螢幕的內容,可以滾動顯示超過螢幕外的內容的特性使其產生了更多強大的子類:UITableView、UICollectionView、UITextView等等。儘管功能如此強大,但是scrollView本質上只是一個UIView的黑魔法,本文將剖析UIScrollView這種強大特性的實現過程


圖層渲染

這裡不得不提到UIView和CALayer的關係。在UIKit框架中,UIView是所有介面元素的基礎,我們頁面上可見的控制元件幾乎都是從這個類派生出來的。之所以說幾乎意味著我們也可以不通過UIView及其子類的途徑來展示一些頁面效果,比如有漸變效果的進度條——通過CALayer直接完成。關於兩者的具體區別以及關係,我們不在這裡詳說,只需要知道每一個UIView管理著一個CALayer,所有我們看到的內容都是由後者進行渲染的。
當我們新增子檢視的時候,會基於當前檢視的座標系原點進行計算,然後在設定好的位置對子檢視的layer進行渲染,假設現在新增一個frame為40, 40, 120, 40的按鈕,那麼渲染圖示如下:

1.png

在按鈕新增到當前檢視之前,按鈕自身先進行了渲染,然後在距離父檢視上邊40點,左邊40點的位置進行組合。正是因為這種組合的渲染模式,在頂層的檢視總是會遮蓋下層的檢視。通過下面的檢視組合流程,我們也能明白為什麼建立的view的bounds總是{0, 0, width, height}

1.png

根據上面的檢視組合,我們試想一下,如果button的座標是(100, 40),那麼這個按鈕還會顯示在view上面嗎?答案是肯定的,因為根據上面檢視組合的實現,我們可以得出一個結論:當前的檢視也存在一個父檢視,父檢視也存在其所在的父檢視。如此迴圈,直到這個檢視是keyWindow為止。那麼我們就有下面的結構圖示

1.png

因此按鈕是處在我們可視的範圍內的。但是,按照這種組合方式,scrollView的實現就顯得非常的神奇了,因為在scrollView上面的子檢視一旦超過了它的顯示範圍。這裡需要說到view的clipsToBoundslayer的maskToBounds屬性,這兩個屬性儘管名字不一樣,但是如果你在堆疊呼叫的時候進行除錯,會發現最終呼叫的是maskToBounds方法。這兩個值任意一個設定YES的時候,在上面檢視組合的③步驟中,超出父檢視範圍內的部分將不進行渲染。
那麼scrollView是否跟我們猜測的一樣,通過設定maskToBounds這個值來遮蔽超出其顯示範圍的子檢視呢?如果是的話,那麼scrollView就只是一個普通的UIView。我們通過下面的程式碼驗證

這時候scrollView的效果是這樣的:

1.png

接下來設定scrollView的maskToBounds屬性

效果圖

1.png

可以看到,scrollView本質上不過是一個預設遮蓋範圍外子檢視的UIView罷了。那麼,UIView到底使用了什麼黑魔法來實現滾動檢視呢?


contentOffset

用過scrollView的開發者對這個屬性都不陌生,contentOffset決定了當前scrollView顯示內容的範圍,即是當前scrollView的左上角的顯示位置座標。通過圖片輪播控制元件來探究這個屬性的實現

1.png

上圖中scrollView發生了滾動,使得顯示的圖片從1變成2。在這個過程中,contentOffset也從(0, 0)變為(width, 0) 從這張圖上看更像是子檢視的位置發生了移動,從右向左移動。但是在這一切發生的過程中,子檢視的frame沒有發生過任何變化,因此與其說是滾動,不如說是scrollView基於子檢視的所在的座標系發生了偏移:

1.png

兩張圖都表示了圖片輪播的過程,但是第二張更加接近scrollView滾動實現的本質——基於自身的座標系發生了位置偏移。因此,contentOffset實際上表示的是scrollView的bounds的改變,其實現大概如下


contentSize

如果說contentOffset決定了scrollView的視窗,那麼contentSize決定了這個視窗背後的風光。

1.png

contentSize決定了scrollView顯示內容的尺寸範圍,從上圖看,我們可以知道,在contentSize的寬度或者長度任意一個尺寸大於scrollView等邊長度的時候,scrollView才能實現滾動效果。當然了,單單是contentSize是不足以讓我們實現scrollView的滾動範圍限制的,這是contentSizecontentOffset的共同實現效果:

1.png

contentInset

contentInset是一個相當有用的屬性,我在做的一個毛玻璃效果導航欄上下拉效果時就通過這個屬性實現。這個屬性可以在某種意義上增加或者減少我們的滾動尺寸範圍:

1.png

可以看到,contentInset讓我們原本contentOffsetcontentSize協同作用的滾動範圍發生了改變,原本最上角(0, 0)的限制座標變成了(-contentInset.left, -contentInset.top)
既然contentInset只是簡單的改變了滾動範圍的規則,為什麼我們不直接通過contentSize來實現呢?這是由於更多時間,我們還需要在滾動檢視的某個方向上面留下一塊空白的區域進行自定義,這時候直接設定contentInset是最快的方式。而換成contentSize來實現,我們還必須同時改變bounds跟center來實現(不要直接改變frame,在組合檢視時,frame最後是由bounds和center決定的)


尾話

蘋果對於scrollView的實現十分的巧妙,在沒有造成過多損耗的情況下賦予UIView一份強大無比的力量。解剖UIKit不僅僅是為了探索實現,這對於我們自定義控制元件能有更多的認識。在scrollView更上一層的UITableView通過複用佇列的方式將scrollView的能力更加完美的展示出來,而這個複用機制值得我們去思考實現過程。

相關文章