天天看點

iOS開發UIScrollView的底層實作

關于scrollView的思考

在iOS開發中我們會大量用到scrollView這個控件,我們使用的tableView/collectionview/textView都繼承自它。scrollView的頻繁使用讓我對它的底層實作産生了興趣,它到底是如何工作的?如何實作一個scrollView?讀完本篇部落格,相信你一定也可以自己實作一個簡易的scrollView。

我們首先來思考以下幾個問題:

  • scrollView繼承自誰,它如何檢測到手指滑動?
  • scrollView如何實作滾動?
  • scrollView裡的各種屬性是如何實作的?如contentSize/contentOffSet......

通過一步步解決上邊的問題,我們就能實作一個自己的scrollView。

一步一步實作scrollView

1.毫無疑問我們都知道scrollView繼承自UIView,檢測手指滑動應該是在view上放置了一個手勢識别,實作代碼如下:

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] init];
        [panGesture addTarget:self action:@selector(panGestureAction:)];
        [self addGestureRecognizer:panGesture];
    }
    return self;
}
           

2.要搞清楚第二個問題,首先我們必須了解frame和bounds的概念。

提到它們,大家都知道frame是相對于父視圖坐标系來說自己的位置和尺寸,bounds相對于自身坐标系來說的位置和尺寸,并且origin一般為(0,0)。但是bounds的origin有什麼用處?改變它會出現什麼效果呢?

當我們嘗試改變bounds的origin時,我們就會發現視圖本身沒有發生變化,但是它的子視圖的位置卻發生了變化,why???其實當我們改變bounds的origin的時候,視圖本身位置沒有變化,但是由于origin的值是基于自身的坐标系,是以自身坐标系的位置被我們改變了。而子視圖的frame正是基于父視圖的坐标系,當我們更改父視圖bounds中origin的時候子視圖的位置就發生了變化,這就是實作scrollView的關鍵點!!!

是不是很好了解?

基于這點我們很容易實作一個簡單的最初級版本的scrollView,代碼如下:

- (void)panGestureAction:(UIPanGestureRecognizer *)pan {
    // 記錄每次滑動開始時的初始位置
    if (pan.state == UIGestureRecognizerStateBegan) {
        self.startLocation = self.bounds.origin;
        NSLog(@"%@", NSStringFromCGPoint(self.startLocation));
    }

    // 相對于初始觸摸點的偏移量
    if (pan.state == UIGestureRecognizerStateChanged) {
        CGPoint point = [pan translationInView:self];
        NSLog(@"%@", NSStringFromCGPoint(point));
        CGFloat newOriginalX = self.startLocation.x - point.x;
        CGFloat newOriginalY = self.startLocation.y - point.y;

        CGRect bounds = self.bounds;
        bounds.origin = CGPointMake(newOriginalX, newOriginalY);
        self.bounds = bounds;
    }
}
           

3.了解了上邊内容的關鍵點,下邊我們将對我們實作的scrollView做一個簡單的優化。

通過contentSize限制scrollView的内部空間,實作代碼如下

if (newOriginalX < ) {
    newOriginalX = ;
} else {
    CGFloat maxMoveWidth = self.contentSize.width - self.bounds.size.width;
    if (newOriginalX > maxMoveWidth) {
        newOriginalX = maxMoveWidth;
    }
}
if (newOriginalY < ) {
    newOriginalY = ;
} else {
    CGFloat maxMoveHeight = self.contentSize.height - self.bounds.size.height;
    if (newOriginalY > maxMoveHeight) {
        newOriginalY = maxMoveHeight;
    }
}
           

通過contentOffset設定scrollView的初始偏移量,相信大家已經懂了如何設定偏移量了吧?沒錯我們隻需設定view自身bounds的origin是實作代碼如下:

- (void)setContentOffset:(CGPoint)contentOffset {
    _contentOffset = contentOffset;
    CGRect newBounds = self.bounds;
    newBounds.origin = contentOffset;
    self.bounds = newBounds;
 }
           

防止scrollView的子視圖超出scrollView

self.layer.masksToBounds = YES;
           

總結

UIScrollView還有很多其它強大的功能,以上我們隻是完成了一個特别簡單的scrollView,以後如果有時間我會對它進行完善。當然如果你有興趣,你完全可以對它進行擴充,下載下傳位址放在這裡。同時我也會繼續研究UIKit中其它控件的底層實作,歡迎您的持續關注!

文/Paul哥(簡書作者)

原文連結:http://www.jianshu.com/p/a9a1ca54ca54

著作權歸作者所有,轉載請聯系作者獲得授權,并标注“簡書作者”。