深入理解iOS開(kāi)發(fā)中的UIScrollView
我是Mike Ash的Let’s Build…系列文章的忠實(shí)粉絲,在這一系列文章中他從頭設(shè)計(jì)Cocoa的控件來(lái)解釋他們的工作原理。在這里我要做一點(diǎn)類(lèi)似的事情,用幾行代碼來(lái)實(shí)現(xiàn)我自己的滾動(dòng)試圖。不過(guò)首先,讓我們先來(lái)了解一下UIKit中的坐標(biāo)系是怎么工作的。如果你只對(duì)滾動(dòng)試圖的代碼實(shí)現(xiàn)感興趣可以放心跳過(guò)下一小節(jié)。UIKit坐標(biāo)系每一個(gè)View都定義了他自己的坐標(biāo)系統(tǒng)。如下圖所示,x軸指向右方,y軸指向下方:
注意這個(gè)邏輯坐標(biāo)系并不關(guān)注包含在其中View的寬度和高度。整個(gè)坐標(biāo)系沒(méi)有邊界向四周無(wú)限延伸.我們?cè)谧鴺?biāo)系中放置四個(gè)子View。每一次色塊代表一個(gè)View:
添加View的代碼實(shí)現(xiàn)如下:
- UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
- redView.backgroundColor = [UIColor colorWithRed:0.815 green:0.007
- blue:0.105 alpha:1];
- UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(150, 160, 150, 200)];
- greenView.backgroundColor = [UIColor colorWithRed:0.494 green:0.827
- blue:0.129 alpha:1];
- UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(40, 400, 200, 150)];
- blueView.backgroundColor = [UIColor colorWithRed:0.29 green:0.564
- blue:0.886 alpha:1];
- UIView *yellowView = [[UIView alloc] initWithFrame:CGRectMake(100, 600, 180, 150)];
- yellowView.backgroundColor = [UIColor colorWithRed:0.972 green:0.905
- blue:0.109 alpha:1];
- [mainView addSubview:redView];
- [mainView addSubview:greenView];
- [mainView addSubview:blueView];
- [mainView addSubview:yellowView];
bounds
Apple關(guān)于UIView的文檔中是這樣描述bounds屬性的:
bounds矩形…描述了該視圖在其自身坐標(biāo)系中的位置和大小。
一個(gè)View可以被看作是定義在其所在坐標(biāo)系平面上的一扇窗戶(hù)或者說(shuō)是一個(gè)矩形的可視區(qū)域。View的邊界表明了這個(gè)矩形可視區(qū)域的位置和大小。
假設(shè)我們的View寬320像素,高480像素,原點(diǎn)在(0,0)。那么這個(gè)View就變成了整個(gè)坐標(biāo)系平面的觀察口,它展示的只是整個(gè)平面的一小部分。位于該View邊界外的區(qū)域依然存在,只是被隱藏起來(lái)了。
一個(gè)View提供了其所在平面的一個(gè)觀察口。View的bounds矩形描述了這個(gè)可是區(qū)域的位置和大小。
Frame
接下來(lái)我們來(lái)試著修改bounds的原點(diǎn)坐標(biāo):
- CGRect bounds = mainView.bounds;
- bounds.origin = CGPointMake(0, 100);
- mainView.bounds = bounds;
當(dāng)我們把bound原點(diǎn)設(shè)為(0,100)后,整個(gè)畫(huà)面看起來(lái)就像這樣:
修改bounds的原點(diǎn)就相當(dāng)與在平面上移動(dòng)這個(gè)可視區(qū)域。
看起來(lái)好像是這個(gè)View向下移動(dòng)了100像素,在這個(gè)View自己的坐標(biāo)系中這確實(shí)沒(méi)錯(cuò)。不過(guò)這個(gè)View真正位于屏幕上的位置(更準(zhǔn)確的說(shuō)在其父View上的位置)其實(shí)沒(méi)有改變,因?yàn)檫@是由View的frame屬性決定的,它并沒(méi)有改變:
frame矩形…定義了這個(gè)View在其父View坐標(biāo)系中的位置和大小。
由于View的位置是相對(duì)固定的,你可以把整個(gè)坐標(biāo)平面想象成我們可以上下拖動(dòng)的透明幕布,把這個(gè)View想象成我們觀察坐標(biāo)平面的窗口。調(diào)整View的Bounds屬性就相當(dāng)于拖動(dòng)這個(gè)幕布,那么下方的內(nèi)容就能在我們View中被觀察到:
Since the view’s position is fixed (from its own perspective), think of the coordinate system plane as a piece of transparent film we can drag around, and of the view as a fixed window we are looking through. Adjusting the bounds
’s origin is equivalent to moving the transparent film such that another part of it becomes visible through the view:
修改bounds的原點(diǎn)坐標(biāo)也相當(dāng)于把整個(gè)坐標(biāo)系向上拖動(dòng),因?yàn)閂iew的frame沒(méi)由變過(guò),所以它相對(duì)于父View的位置沒(méi)有變化過(guò)。
其實(shí)這就是UIScrollView滑動(dòng)時(shí)所發(fā)生的事情。注意從一個(gè)用戶(hù)的角度來(lái)看,他以為時(shí)這個(gè)View中的子View在移動(dòng),其實(shí)他們的在坐標(biāo)系中位置(他們的frame)沒(méi)有發(fā)生過(guò)變化。
打造你的UIScrollView
一個(gè)scroll view并不需要其中子View的坐標(biāo)來(lái)使他們滾動(dòng)。***要做的就是改變他的bounds屬性。知道了這一點(diǎn),實(shí)現(xiàn)一個(gè)簡(jiǎn)單的scroll view就沒(méi)什么困難了。我們用一個(gè)gesture recognizer來(lái)識(shí)別用戶(hù)的拖動(dòng)操作,根據(jù)用戶(hù)拖動(dòng)的偏移量來(lái)改變bounds的原點(diǎn):
- // CustomScrollView.h
- @import UIKit;
- @interface CustomScrollView : UIView
- @property (nonatomic) CGSize contentSize;
- @end
- // CustomScrollView.m
- #import "CustomScrollView.h"
- @implementation CustomScrollView
- - (id)initWithFrame:(CGRect)frame
- {
- self = [super initWithFrame:frame];
- if (self == nil) {
- return nil;
- }
- UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc]
- initWithTarget:self action:@selector(handlePanGesture:)];
- [self addGestureRecognizer:gestureRecognizer];
- return self;
- }
- - (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer
- {
- CGPoint translation = [gestureRecognizer translationInView:self];
- CGRect bounds = self.bounds;
- // Translate the view's bounds, but do not permit values that would violate contentSize
- CGFloat newBoundsOriginX = bounds.origin.x - translation.x;
- CGFloat minBoundsOriginX = 0.0;
- CGFloat maxBoundsOriginX = self.contentSize.width - bounds.size.width;
- bounds.origin.x = fmax(minBoundsOriginX, fmin(newBoundsOriginX, maxBoundsOriginX));
- CGFloat newBoundsOriginY = bounds.origin.y - translation.y;
- CGFloat minBoundsOriginY = 0.0;
- CGFloat maxBoundsOriginY = self.contentSize.height - bounds.size.height;
- bounds.origin.y = fmax(minBoundsOriginY, fmin(newBoundsOriginY, maxBoundsOriginY));
- self.bounds = bounds;
- [gestureRecognizer setTranslation:CGPointZero inView:self];
- }
- @end
和真正的UIScrollView一樣,我們的類(lèi)也有一個(gè)contentSize屬性,你必須從外部來(lái)設(shè)置這個(gè)值來(lái)指定可以滾動(dòng)的區(qū)域,當(dāng)我們改變bounds的大小時(shí)我們要確保設(shè)置的值是有效的。
結(jié)果:
我們的scroll view已經(jīng)能夠工作了,不過(guò)還缺少動(dòng)量滾動(dòng),反彈效果還有滾動(dòng)提示符。
總結(jié)
感謝UIKit的坐標(biāo)系統(tǒng)特性,使我們之花了30幾行代碼就能重現(xiàn)UIScrollView的精華,當(dāng)然真正的UIScrollView要比我們所做的復(fù)雜的多,反彈效果,動(dòng)量滾動(dòng),放大試圖,還有代理方法,這些特性我們沒(méi)有在這里涉及到。
更新 5/ 2, 2014: 本文的代碼在https://github.com/ole/CustomScrollView。
更新 5/ 8, 2014:
1.坐標(biāo)系并非無(wú)限延伸的。坐標(biāo)系的范圍由CGFloat的長(zhǎng)度來(lái)決定,根據(jù)32位和64位系統(tǒng)有所不同,通常來(lái)講這是一個(gè)很大的值。
2.事實(shí)上,除非你設(shè)置clipToBounds == YES,所有子View超出的部分其實(shí)仍然是可見(jiàn)的。只是View不會(huì)再去檢測(cè)超出部分的觸摸事件而已。
原文鏈接: Ole Begemann 翻譯:袁欣