其實一個UIScrollView 和 UIView沒有什么區(qū)別,我們從 UIView 的渲染開始談起,一步步講到 UIScrollView 的原理,開始吧!
渲染:
渲染分成 光柵化 和 組合兩個步驟:
- 光柵化:通過一系列繪制參數(shù),生成一張圖片,任何 UIView (CALayer)繪制到屏幕上時都是一張圖片;
- 組合: 當前 UIView 光柵化后生成的這張圖片,會復合到他的SuperView 上;然后,他的SuperView 光柵化后生成的圖片,也會復合到他自己的SuperView上,一個雪球效應,整個光柵化后的圖片越來越大,最終整個試圖層級中,最下面的那一個UIView (UIWindow)就是我們再屏幕上看到的東西。
了解了渲染的原理之后,我們還了解到一個視圖繪制到屏幕上,和兩個屬性息息相關。(Frame & Bounds); 分清他們之間的區(qū)別,是我們透析出 UIScrollView 滑動的真正原因。
Frame & Bounds
- Frame 和 Bounds 都是一個 矩形, 他們的size 通常都相等 (如果進行了旋轉等變化,size也不慎相等了)
- 他們的 origin 通常都不相等。
Step1: 在 一個 UIView 的光柵化階段,他不關心,后序階段(既組合階段)中怎樣 position 和怎樣組合。在光柵化階段, UIView 只關心他自己是怎樣繪制的(drawRect: 方法中)。如下圖 像是一個人內(nèi)省的過程,只關心自己內(nèi)部的實現(xiàn),需要結合 bounds 參數(shù)來繪制自己的內(nèi)容。

Step2:在一個UIView 的組合階段,他根據(jù) frame 參數(shù),指定了他的位置和尺寸,這時候他會將自己的 光柵化圖片復合到 superView上;如圖2:相比光柵化內(nèi)省的過程,組合像是一個和父視圖溝通交流的過程,最終把自己的這個圖片復合到他的父視圖上。


看到這里,我們知道了, bounds 是用來繪制自己內(nèi)部內(nèi)容的,frame 是用來繪制自己在外部父視圖中位置和尺寸的。 那么,我們來看一組數(shù)學公式:
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
解釋: (組合后該UIView的 origin = 在父視圖中 frame 的origin - 該UIView所在父視圖 bounds 的origin )。 很好理解了,組合的過程中,我們一方面考慮到該UIView 所在父視圖中的frame來繪制位置和尺寸,同時,我們考慮到了該 UIView 的父視圖 bounds 屬性,規(guī)定了他的子視圖是從哪里開始繪制的。 (Eureka!)

UIScrollView 的 ContentOffset
如果我是Apple的工程師,就會想到,我可以這樣實現(xiàn) Scroll 滾動:
給當前的 UIView 的父視圖添加一個 PanGestrue 手勢,手勢滑動過程中,不停地修改他的 Frame, 不就實現(xiàn)了滾動了嗎???
這時候問題來了,一個父視圖中有很多子視圖,在滾動過程中,我們開發(fā)者需要處理所有子視圖的 frame 的修改,這簡直是一件不可能完成的事。
這時候,再看看我們之前總結的那個公式:
CompositedPosition.x = View.frame.origin.x - Superview.bounds.origin.x;
CompositedPosition.y = View.frame.origin.y - Superview.bounds.origin.y;
既然我們對于修改View 的 Frame 無能為力,那么我們能否修改他父視圖的 bounds 屬性呢? 試試唄!
以前,我們總是假定bounds 的 origin 為 (0,0),所以我們經(jīng)常得出 當前 View的最終光柵化并且復合之后的顯示效果只由 frame 決定; 如果 bounds 的 origin 不再是(0,0)了呢? 看看下面這張圖:

我們修改了紫色視圖 bounds,他的content 開始繪制的原點變化了,子視圖綠色的 Button 光柵化并且復合后的位置也就變化了。
到這里,想明白了嗎? 其實 UIScrollView 滾動的真正原理就是,滾動的過程中,不斷修改他的 bounds, 使得他的子視圖們最終復合出來的 image 位置不同了而已。 僅此而已,Apple 的工程師們,為了隱藏這個秘密,還給 UIScrollView 提供了一個屬性,叫做 ContentOffset ,修改 ContentOffset, 其實內(nèi)部就是為了 修改 其 bounds
至此,UIScrollView 的 原理,我們就很清晰了。 我也為此 Blog 實現(xiàn)了一個簡單的 自定義UIScrollView, 就是從創(chuàng)建一個 UIView開始的 GitHub - sishenyihuba/DXLScrollView: This is a custom UIScrollVIew ,for digging into theory begin the sence.
ContentSize
ContentSize這個屬性 其實就是定義了 contentOffset 的最大范圍,既限制了 UIScrollView 的滾動區(qū)域。
ContentInset
這個屬性提供了 UIScrollView 的一種能力,可以在滾動區(qū)域之外再添加一些視圖,這是一個非常有意思的能力,可參考我開源的 DXLRefreshView 來理解其中的原理。