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

在iOS開發中咱們會大量用到UIScrollView這個控件,咱們使用的UITableView/UICollectionView/UITextView都繼承自它。UIScrollView的頻繁使用讓我對它的底層實現產生了興趣,它究竟是如何工做的?如何實現一個UIScrollView?讀完本篇文章,相信你必定也能夠本身實現一個簡易的UIScrollView。源代碼git

1.frame與bounds

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

2.UIScrollView實現

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

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)。因此以下:spa

ContentOffset = bounds.origin;
複製代碼

這樣,理解了ContentOffset以後咱們就能夠實現一個簡單的UIScrollView,代碼以下:code

- (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的時候:cdn

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

其實,從上面能夠看出來,當咱們拖動的時候,動的並非ScrollView,而是SubViews。blog

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.參考

相關文章
相關標籤/搜索