iOS開發筆記(五):UIScrollView實現原理

卡布達巨人發表於2018-01-15

在iOS開發中我們會大量用到UIScrollView這個控制元件,我們使用的UITableView/UICollectionView/UITextView都繼承自它。UIScrollView的頻繁使用讓我對它的底層實現產生了興趣,它到底是如何工作的?如何實現一個UIScrollView?讀完本篇文章,相信你一定也可以自己實現一個簡易的UIScrollView。原始碼

1.frame與bounds

這部分請參考我之前的文章——iOS開發筆記(四):frame與bounds的區別詳解

2.UIScrollView實現

UIScrollView其實就是bounds.origin != (0,0)的特殊情況。而ContentOffset、ContentSize和ContentInset作為UIScrollView三個基本的屬性,其實都是跟origin相關,下面詳細討論。

2.1 ContentOffset

ContentOffset是UIScrollView當前顯示區域頂點相對於frame頂點的偏移量,比如你把檢視上拉了100個點,也就是y偏移了100,ContentOffset就是(0,100)。

前文可以得知,當修改SuperView.bounds.origin時,會變相的修改SubView的實際座標,從而影響SubView在SuperView中的位置。如果我們修改SuperView.bounds.origin從(0,0)變為(0,100),SubViews的frame並不會產生變化,產生變化的實際是SubViews的真實座標點。SubViews的真實座標點會減小100點,也就是上移100點,而對應到SubView在檢視的效果就是上移了100個點,也就是ContentOffset為(0,100)。所以如下:

ContentOffset = bounds.origin;
複製程式碼

這樣,理解了ContentOffset之後我們就可以實現一個簡單的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];
//獲取手勢位置 CGFloat newOriginY = self.view.bounds.origin.y - touchPoint.y;
//根據手勢位置計算新的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];

}複製程式碼
自定義簡單UIScrollView

每當我們拖動SuperView的時候:

  • SuperView.bounds.origin會根據我們拖動的座標,生成新的origin。
  • SuperView發現自己bounds被修改,會呼叫layoutSubviews方法,此方法會使Subviews根據SuperView.bounds.origin和自身的Frame.origin重新計算出自身的實際座標。
  • 重新計算位置的SubViews顯示在SuperView中。

其實,從上面可以看出來,當我們拖動的時候,動的並不是ScrollView,而是SubViews。

2.2 ContentSize

ContentSize其實就是UIScrollView可以滾動的區域,比如frame = (0,0,320,480) ,contentSize = (320,960),代表你的scrollview可以上下滾動,滾動區域為frame大小的兩倍。這個東西其實是抽象的。抽象的目的是為了讓大家更好地運用UIScrollView,而不用去理解其背後的實現原理(其實就是修改bounds.origin這一點而已)。

看上面的執行圖,小夥伴們會發現,拖動起來無邊無界啊,於是ContentSize橫空出世,其本質就是對bounds.origin的變化約束一個範圍,使其在規定的範圍內拖動。

我們修改程式碼如下:

- (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));
//比最大值小的同時比最小值大:min<
=newOriginY<
=maxOriginY viewBounds.origin.x = fmax(minOriginX, fmin(newOriginX, maxOriginX));
self.view.bounds = viewBounds;
[panGestureRecognizer setTranslation:CGPointZero inView:self.view];

}複製程式碼
設定origin範圍之後

這樣,只要修改minOriginY、minOriginX、maxOriginY、maxOriginX四個值就能確定UIScrollView的滾動範圍,由此,ContentSize也能推到得出,如下:

ContentSize.height = view.bounds.size.height + maxOriginY;ContentSize.width = view.bounds.size.width + maxOriginX;複製程式碼

2.3 ContentInset

ContentInset是UIScrollView的contentView的頂點相對於UIScrollView的位置,例如你的ContentInset = (0,100),那麼你的contentView就是從UIScrollView的(0,100)開始顯示。

這個屬效能夠在UIScrollView的4周增加額外的滾動區域,以此可以實現下拉重新整理,鍵盤彈出的同時抬高View等等。

修改程式碼如下:

- (void)handlePanGesture:(UIPanGestureRecognizer *)panGestureRecognizer { 
//ContentInset CGPoint touchPoint = [panGestureRecognizer translationInView:self.view];
//獲取手勢位置 CGFloat newOriginY = self.view.bounds.origin.y - touchPoint.y;
//根據手勢位置計算新的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));
//比最大值小的同時比最小值大:min<
=newOriginY<
=maxOriginY viewBounds.origin.x = fmax(0, fmin(newOriginX, maxOriginX));
self.view.bounds = viewBounds;
[panGestureRecognizer setTranslation:CGPointZero inView:self.view];

}複製程式碼
設定Inset之後

所以,ContentInset其實只是在不同狀態下修改了maxOriginY、maxOriginX等等的值,從而實現了在不改變ContentSize的情況下使滾動區域得到了擴充套件。

3.總結

回顧一下,其實ContentOffset、ContentSize、ContentInset還是Bounces效果本質都是在跟origin玩耍:Offset直接是origin的別稱,而Size、Inset都是修改了origin的改變範圍。但是各個屬性又有自己專有的作用,Size可以確定滾動的範圍,而Inset可以在不修改原有滾動範圍的同時,擴大總體滾動範圍。實現了這三個屬性,也就能實現最基本的UIScrollView。

4.參考

來源:https://juejin.im/post/5a5c5d64f265da3e5b32c7d2

相關文章