在iOS開發(fā)中我們會(huì)大量用到UIScrollView這個(gè)控件,我們使用的UITableView/UICollectionView/UITextView都繼承自它。UIScrollView的頻繁使用讓我對(duì)它的底層實(shí)現(xiàn)產(chǎn)生了興趣,它到底是如何工作的?如何實(shí)現(xiàn)一個(gè)UIScrollView?讀完本篇文章,相信你一定也可以自己實(shí)現(xiàn)一個(gè)簡易的UIScrollView。源代碼
1.frame與bounds
這部分請(qǐng)參考我之前的文章——iOS frame與bounds的區(qū)別詳解
2.UIScrollView實(shí)現(xiàn)
UIScrollView其實(shí)就是bounds.origin != (0,0)的特殊情況。而ContentOffset、ContentSize和ContentInset作為UIScrollView三個(gè)基本的屬性,其實(shí)都是跟origin相關(guān),下面詳細(xì)討論。
2.1 ContentOffset
ContentOffset是UIScrollView當(dāng)前顯示區(qū)域頂點(diǎn)相對(duì)于frame頂點(diǎn)的偏移量,比如你把視圖上拉了100個(gè)點(diǎn),也就是y偏移了100,ContentOffset就是(0,100)。
從前文可以得知,當(dāng)修改SuperView.bounds.origin時(shí),會(huì)變相的修改SubView的實(shí)際坐標(biāo),從而影響SubView在SuperView中的位置。如果我們修改SuperView.bounds.origin從(0,0)變?yōu)?0,100),SubViews的frame并不會(huì)產(chǎn)生變化,產(chǎn)生變化的實(shí)際是SubViews的真實(shí)坐標(biāo)點(diǎn)。SubViews的真實(shí)坐標(biāo)點(diǎn)會(huì)減小100點(diǎn),也就是上移100點(diǎn),而對(duì)應(yīng)到SubView在視圖的效果就是上移了100個(gè)點(diǎn),也就是ContentOffset為(0,100)。所以如下:
ContentOffset = bounds.origin;
復(fù)制代碼
這樣,理解了ContentOffset之后我們就可以實(shí)現(xiàn)一個(gè)簡單的UIScrollView,代碼如下:
- (void)addGestureAndViews {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePanGesture:)];
[self.view addGestureRecognizer:pan];
UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(0, 20, 100, 100)];
[view1 setBackgroundColor:[UIColor blueColor]];
[self.view addSubview:view1];
UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(SCREEN_WIDTH - 100, SCREEN_HEIGHT - 100, 100, 100)];
[view2 setBackgroundColor:[UIColor brownColor]];
[self.view addSubview:view2];
}
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
//ContentOffset
CGPoint touchPoint = [panGestureRecognizer translationInView:self.view];//獲取手勢(shì)位置
CGFloat newOriginY = self.view.bounds.origin.y - touchPoint.y;//根據(jù)手勢(shì)位置計(jì)算新的origin值
CGFloat newOriginX = self.view.bounds.origin.x - touchPoint.x;
CGRect viewBounds = self.view.bounds;
viewBounds.origin.y = newOriginY;//賦值
viewBounds.origin.x = newOriginX;
self.view.bounds = viewBounds;
[panGestureRecognizer setTranslation:CGPointZero inView:self.view];
}
復(fù)制代碼
每當(dāng)我們拖動(dòng)SuperView的時(shí)候:
- SuperView.bounds.origin會(huì)根據(jù)我們拖動(dòng)的坐標(biāo),生成新的origin。
- SuperView發(fā)現(xiàn)自己bounds被修改,會(huì)調(diào)用layoutSubviews方法,此方法會(huì)使Subviews根據(jù)SuperView.bounds.origin和自身的Frame.origin重新計(jì)算出自身的實(shí)際坐標(biāo)。
- 重新計(jì)算位置的SubViews顯示在SuperView中。
其實(shí),從上面可以看出來,當(dāng)我們拖動(dòng)的時(shí)候,動(dòng)的并不是ScrollView,而是SubViews。
2.2 ContentSize
ContentSize其實(shí)就是UIScrollView可以滾動(dòng)的區(qū)域,比如frame = (0,0,320,480) ,contentSize = (320,960),代表你的scrollview可以上下滾動(dòng),滾動(dòng)區(qū)域?yàn)閒rame大小的兩倍。這個(gè)東西其實(shí)是抽象的。抽象的目的是為了讓大家更好地運(yùn)用UIScrollView,而不用去理解其背后的實(shí)現(xiàn)原理(其實(shí)就是修改bounds.origin這一點(diǎn)而已)。
看上面的運(yùn)行圖,小伙伴們會(huì)發(fā)現(xiàn),拖動(dòng)起來無邊無界啊,于是ContentSize橫空出世,其本質(zhì)就是對(duì)bounds.origin的變化約束一個(gè)范圍,使其在規(guī)定的范圍內(nèi)拖動(dòng)。
我們修改代碼如下:
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
//ContentSize
CGPoint touchPoint = [panGestureRecognizer translationInView:self.view];
CGFloat newOriginY = self.view.bounds.origin.y - touchPoint.y;
CGFloat newOriginX = self.view.bounds.origin.x - touchPoint.x;
CGFloat minOriginY = 0.0;
CGFloat minOriginX = 0.0;
CGFloat maxOriginY = 20.0;
CGFloat maxOriginX = 20.0;
CGRect viewBounds = self.view.bounds;
viewBounds.origin.y = fmax(minOriginY, fmin(newOriginY, maxOriginY));//比最大值小的同時(shí)比最小值大:min<=newOriginY<=maxOriginY
viewBounds.origin.x = fmax(minOriginX, fmin(newOriginX, maxOriginX));
self.view.bounds = viewBounds;
[panGestureRecognizer setTranslation:CGPointZero inView:self.view];
}
復(fù)制代碼
這樣,只要修改minOriginY、minOriginX、maxOriginY、maxOriginX四個(gè)值就能確定UIScrollView的滾動(dòng)范圍,由此,ContentSize也能推到得出,如下:
ContentSize.height = view.bounds.size.height + maxOriginY;
ContentSize.width = view.bounds.size.width + maxOriginX;
復(fù)制代碼
2.3 ContentInset
ContentInset是UIScrollView的contentView的頂點(diǎn)相對(duì)于UIScrollView的位置,例如你的ContentInset = (0,100),那么你的contentView就是從UIScrollView的(0,100)開始顯示。
這個(gè)屬性能夠在UIScrollView的4周增加額外的滾動(dòng)區(qū)域,以此可以實(shí)現(xiàn)下拉刷新,鍵盤彈出的同時(shí)抬高View等等。
修改代碼如下:
- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer {
//ContentInset
CGPoint touchPoint = [panGestureRecognizer translationInView:self.view];//獲取手勢(shì)位置
CGFloat newOriginY = self.view.bounds.origin.y - touchPoint.y;//根據(jù)手勢(shì)位置計(jì)算新的origin值
CGFloat newOriginX = self.view.bounds.origin.x - touchPoint.x;
CGFloat min = 0.0;
CGFloat maxOriginY = 600.0;
CGFloat maxOriginX = 0;
if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
min = 0.0;
maxOriginY = 600.0;
} else {
min = -50.0;
maxOriginY = 650.0;
}
CGRect viewBounds = self.view.bounds;
viewBounds.origin.y = fmax(min, fmin(newOriginY, maxOriginY));//比最大值小的同時(shí)比最小值大:min<=newOriginY<=maxOriginY
viewBounds.origin.x = fmax(0, fmin(newOriginX, maxOriginX));
self.view.bounds = viewBounds;
[panGestureRecognizer setTranslation:CGPointZero inView:self.view];
}
復(fù)制代碼
所以,ContentInset其實(shí)只是在不同狀態(tài)下修改了maxOriginY、maxOriginX等等的值,從而實(shí)現(xiàn)了在不改變ContentSize的情況下使?jié)L動(dòng)區(qū)域得到了擴(kuò)展。
3.總結(jié)
回顧一下,其實(shí)ContentOffset、ContentSize、ContentInset還是Bounces效果本質(zhì)都是在跟origin玩耍:Offset直接是origin的別稱,而Size、Inset都是修改了origin的改變范圍。但是各個(gè)屬性又有自己專有的作用,Size可以確定滾動(dòng)的范圍,而Inset可以在不修改原有滾動(dòng)范圍的同時(shí),擴(kuò)大總體滾動(dòng)范圍。實(shí)現(xiàn)了這三個(gè)屬性,也就能實(shí)現(xiàn)最基本的UIScrollView。