序言
UIScrollView滾動視圖闲延,絕對算的上是iOS開發(fā)中最重要的控件,用來展示多于一個屏幕的內(nèi)容陆馁,可以滾動顯示超過屏幕外的內(nèi)容的特性使其產(chǎn)生了更多強大的子類:UITableView合愈、UICollectionView想暗、UITextView等等。盡管功能如此強大说莫,但是scrollView本質(zhì)上只是一個UIView的黑魔法储狭,本文將剖析UIScrollView這種強大特性的實現(xiàn)過程
圖層渲染
這里不得不提到UIView和CALayer的關(guān)系捣郊。在UIKit框架中慈参,UIView是所有界面元素的基礎(chǔ)驮配,我們頁面上可見的控件幾乎都是從這個類派生出來的。之所以說幾乎意味著我們也可以不通過UIView及其子類的途徑來展示一些頁面效果琐旁,比如有漸變效果的進度條——通過CALayer直接完成猜绣。關(guān)于兩者的具體區(qū)別以及關(guān)系,我們不在這里詳說牺陶,只需要知道每一個UIView管理著一個CALayer辣之,所有我們看到的內(nèi)容都是由后者進行渲染的。
當(dāng)我們添加子視圖的時候碱工,會基于當(dāng)前視圖的坐標(biāo)系原點進行計算奏夫,然后在設(shè)置好的位置對子視圖的layer進行渲染酗昼,假設(shè)現(xiàn)在添加一個frame為40, 40, 120, 40的按鈕梳猪,那么渲染圖示如下:
在按鈕添加到當(dāng)前視圖之前春弥,按鈕自身先進行了渲染,然后在距離父視圖上邊40點扫责,左邊40點的位置進行組合逃呼。正是因為這種組合的渲染模式者娱,在頂層的視圖總是會遮蓋下層的視圖黄鳍。通過下面的視圖組合流程平匈,我們也能明白為什么創(chuàng)建的view的bounds總是{0, 0, width, height}
根據(jù)上面的視圖組合增炭,我們試想一下,如果button的坐標(biāo)是(100, 40)弟跑,那么這個按鈕還會顯示在view上面嗎孟辑?答案是肯定的,因為根據(jù)上面視圖組合的實現(xiàn)炭玫,我們可以得出一個結(jié)論:當(dāng)前的視圖也存在一個父視圖貌虾,父視圖也存在其所在的父視圖。如此循環(huán)衔憨,直到這個視圖是keyWindow為止袄膏。那么我們就有下面的結(jié)構(gòu)圖示
因此按鈕是處在我們可視的范圍內(nèi)的沉馆。但是,按照這種組合方式揖盘,scrollView的實現(xiàn)就顯得非常的神奇了锌奴,因為在scrollView上面的子視圖一旦超過了它的顯示范圍。這里需要說到view的clipsToBounds和layer的maskToBounds屬性椭符,這兩個屬性盡管名字不一樣,但是如果你在堆棧調(diào)用的時候進行調(diào)試有咨,會發(fā)現(xiàn)最終調(diào)用的是maskToBounds方法蒸健。這兩個值任意一個設(shè)置YES的時候似忧,在上面視圖組合的③步驟中,超出父視圖范圍內(nèi)的部分將不進行渲染淳衙。
那么scrollView是否跟我們猜測的一樣饺著,通過設(shè)置maskToBounds這個值來屏蔽超出其顯示范圍的子視圖呢幼衰?如果是的話,那么scrollView就只是一個普通的UIView渡嚣。我們通過下面的代碼驗證
UIScrollView * scrollView = [[UIScrollView alloc] initWithFrame: CGRectMake(0, 0, 200, 180)];
scrollView.backgroundColor = [UIColor orangeColor];
scrollView.center = self.view.center;
[self.view addSubview: scrollView];
UIView * subview = [[UIView alloc] initWithFrame: CGRectMake(60, 60, 180, 180)];
subview.backgroundColor = [UIColor blueColor];
[scrollView addSubview: subview];
這時候scrollView的效果是這樣的:
接下來設(shè)置scrollView的maskToBounds屬性
scrollView.layer.masksToBounds = NO;
效果圖
可以看到绝葡,scrollView本質(zhì)上不過是一個默認遮蓋范圍外子視圖的UIView罷了腹鹉。那么种蘸,UIView到底使用了什么黑魔法來實現(xiàn)滾動視圖呢竞膳?
contentOffset
用過scrollView的開發(fā)者對這個屬性都不陌生,contentOffset決定了當(dāng)前scrollView顯示內(nèi)容的范圍刊侯,即是當(dāng)前scrollView的左上角的顯示位置坐標(biāo)锉走。通過圖片輪播控件來探究這個屬性的實現(xiàn)
上圖中scrollView發(fā)生了滾動,使得顯示的圖片從1變成2休偶。在這個過程中辜羊,contentOffset也從(0, 0)變?yōu)?width, 0) 從這張圖上看更像是子視圖的位置發(fā)生了移動,從右向左移動碱妆。但是在這一切發(fā)生的過程中昔驱,子視圖的frame沒有發(fā)生過任何變化,因此與其說是滾動纳本,不如說是scrollView基于子視圖的所在的坐標(biāo)系發(fā)生了偏移:
兩張圖都表示了圖片輪播的過程饮醇,但是第二張更加接近scrollView滾動實現(xiàn)的本質(zhì)——基于自身的坐標(biāo)系發(fā)生了位置偏移秕豫。因此混移,contentOffset實際上表示的是scrollView的bounds的改變,其實現(xiàn)大概如下
- (void)setContentOffset: (CGPoint)contentOffset
{
_contentOffset = contentOffset;
CGRect bounds = self.bounds;
bounds.origin = contentOffset;
self.bounds = bounds;
}
contentSize
如果說contentOffset決定了scrollView的窗口毁嗦,那么contentSize決定了這個窗口背后的風(fēng)光回铛。
contentSize決定了scrollView顯示內(nèi)容的尺寸范圍茵肃,從上圖看,我們可以知道捞附,在contentSize的寬度或者長度任意一個尺寸大于scrollView等邊長度的時候,scrollView才能實現(xiàn)滾動效果胆绊。當(dāng)然了欧募,單單是contentSize是不足以讓我們實現(xiàn)scrollView的滾動范圍限制的,這是
contentSize
和contentOffset
的共同實現(xiàn)效果:contentInset
contentInset是一個相當(dāng)有用的屬性何缓,我在做的一個毛玻璃效果導(dǎo)航欄上下拉效果時就通過這個屬性實現(xiàn)碌廓。這個屬性可以在某種意義上增加或者減少我們的滾動尺寸范圍:
可以看到谷婆,
contentInset
讓我們原本contentOffset
和contentSize
協(xié)同作用的滾動范圍發(fā)生了改變辽聊,原本最上角(0, 0)
的限制坐標(biāo)變成了(-contentInset.left, -contentInset.top)
既然contentInset只是簡單的改變了滾動范圍的規(guī)則,為什么我們不直接通過contentSize來實現(xiàn)呢异袄?這是由于更多時間玛臂,我們還需要在滾動視圖的某個方向上面留下一塊空白的區(qū)域進行自定義,這時候直接設(shè)置contentInset是最快的方式讽营。而換成contentSize來實現(xiàn)泡徙,我們還必須同時改變bounds跟center來實現(xiàn)(不要直接改變frame堪藐,在組合視圖時,frame最后是由bounds和center決定的)
尾話
蘋果對于scrollView的實現(xiàn)十分的巧妙糖荒,在沒有造成過多損耗的情況下賦予UIView一份強大無比的力量苏章。解剖UIKit不僅僅是為了探索實現(xiàn),這對于我們自定義控件能有更多的認識泉孩。在scrollView更上一層的UITableView通過復(fù)用隊列的方式將scrollView的能力更加完美的展示出來并淋,而這個復(fù)用機制值得我們?nèi)ニ伎紝崿F(xiàn)過程县耽。本文demo
文集:iOS開發(fā)
轉(zhuǎn)載注明鏈接:探索UIScrollView的實現(xiàn)