UIScrollView實(shí)現(xiàn)原理

在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ù)制代碼
自定義簡單UIScrollView

每當(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ù)制代碼
設(shè)置origin范圍之后

這樣,只要修改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ù)制代碼
設(shè)置Inset之后

所以,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。

4.參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容