感謝UIKit的坐標系統特性,使我們之花了30幾行代碼就能重現UIScrollView的精華,當然真正的UIScrollView要比我們所做的復雜的多,反彈效果,動量滾動,放大試圖,還有代理方法,這些特性我們沒有在這裡涉及到。
首先,讓我們先來了解一下UIKit中的坐標系是怎麼工作的。如果你只對滾動試圖的代碼實現感興趣可以放心跳過下一小節。UIKit坐標系每一個View都定義了他自己的坐標系統。如下圖所示,x軸指向右方,y軸指向下方:
注意這個邏輯坐標系並不關注包含在其中View的寬度和高度。整個坐標系沒有邊界向四周無限延伸.我們在坐標系中放置四個子View。每一次色塊代表一個View:
添加View的代碼實現如下:
代碼如下:
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關於UIView的文檔中是這樣描述bounds屬性的:
bounds矩形…描述了該視圖在其自身坐標系中的位置和大小。
一個View可以被看作是定義在其所在坐標系平面上的一扇窗戶或者說是一個矩形的可視區域。View的邊界表明了這個矩形可視區域的位置和大小。
假設我們的View寬320像素,高480像素,原點在(0,0)。那麼這個View就變成了整個坐標系平面的觀察口,它展示的只是整個平面的一小部分。位於該View邊界外的區域依然存在,只是被隱藏起來了。
一個View提供了其所在平面的一個觀察口。View的bounds矩形描述了這個可是區域的位置和大小。
Frame
接下來我們來試著修改bounds的原點坐標:
代碼如下:
CGRect bounds = mainView.bounds;
bounds.origin = CGPointMake(0, 100);
mainView.bounds = bounds;
當我們把bound原點設為(0,100)後,整個畫面看起來就像這樣:
修改bounds的原點就相當與在平面上移動這個可視區域。
看起來好像是這個View向下移動了100像素,在這個View自己的坐標系中這確實沒錯。不過這個View真正位於屏幕上的位置(更准確的說在其父View上的位置)其實沒有改變,因為這是由View的frame屬性決定的,它並沒有改變:
frame矩形…定義了這個View在其父View坐標系中的位置和大小。
由於View的位置是相對固定的,你可以把整個坐標平面想象成我們可以上下拖動的透明幕布,把這個View想象成我們觀察坐標平面的窗口。調整View的Bounds屬性就相當於拖動這個幕布,那麼下方的內容就能在我們View中被觀察到:
修改bounds的原點坐標也相當於把整個坐標系向上拖動,因為View的frame沒由變過,所以它相對於父View的位置沒有變化過。
其實這就是UIScrollView滑動時所發生的事情。注意從一個用戶的角度來看,他以為時這個View中的子View在移動,其實他們的在坐標系中位置(他們的frame)沒有發生過變化。
打造你的UIScrollView
一個scroll view並不需要其中子View的坐標來使他們滾動。唯一要做的就是改變他的bounds屬性。知道了這一點,實現一個簡單的scroll view就沒什麼困難了。我們用一個gesture recognizer來識別用戶的拖動操作,根據用戶拖動的偏移量來改變bounds的原點:
代碼如下:
// 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一樣,我們的類也有一個contentSize屬性,你必須從外部來設置這個值來指定可以滾動的區域,當我們改變bounds的大小時我們要確保設置的值是有效的。
結果:
UIScrollView常用操作方法整理
代碼如下:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
// 創建一個滾動視圖
self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 64, 320, 568-64)];
// 設置代理
self.scrollView.delegate = self;
self.scrollView.backgroundColor = [UIColor redColor];
[self.view addSubview:_scrollView];
/**
* CGPoint contentOffSet 監控目前滾動的位置
* CGSize contentSize 滾動范圍大小(主屬性)
* UIEdgeInsets contentInset 視圖在scrollView中的位置
* BOOL directionalLockEnabled 指定控件是否只能在一個方向上滾動
* BOOL bounces 控制控件遇到邊框是否反彈
* BOOL alwaysBounceVertical 控制垂直方向遇到邊框是否反彈
* BOOL alwaysBounceHorizontal 控制水平方向遇到邊框是否反彈
* BOOL pagingEnabled 控制控件是否整頁翻動
* BOOL scrollEnabled 控制控件是否能滾動
* BOOL showsHorizontalScrollIndicator 控制是否顯示水平方向的滾動條
* BOOL showsVerticalScrollIndicator 控制是否顯示垂直方向的滾動條
* UIEdgeInsets scrollIndicatorInsets 指定滾動條在scrollerView中的位置
* UIScrollViewIndicatorStyle indicatorStyle 設定滾動條的樣式
* float decelerationRate 改變scrollerView的減速點位置
* BOOL tracking 監控當前目標是否正在被跟蹤
* BOOL dragging 監控當前目標是否正在被拖拽
* BOOL decelerating 監控當前目標是否正在減速
* BOOL delaysContentTouches 控制視圖是否延時調用開始滾動的方法
* BOOL canCancelContentTouches 控制控件是否接觸取消touch的事件
* float minimumZoomScale 縮放的最小比例
* float maximumZoomScale 縮放的最大比例
* float zoomScale 設置變化比例
* BOOL bouncesZoom 控制縮放的時候是否會反彈
* BOOL zooming 判斷控件的大小是否正在改變
* BOOL zoomBouncing 判斷是否正在進行縮放反彈
* BOOL scrollsToTop 控制控件滾動到頂部
*/
// 提示用戶,在界面創建的時候,水平滾動條或者垂直滾動條會出現一次閃現效果
[self.scrollView flashScrollIndicators];
// 偏移帶動畫效果
[self.scrollView setContentOffset:CGPointMake(320, 0) animated:YES];
}
#pragma mark UIScrollViewDelegate
// 只要滾動了就會觸發
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
{
}
// 開始拖拽視圖
// 當開始滾動視圖時,執行該方法。一次有效滑動(開始滑動,滑動一小段距離,只要手指不松開,只算一次滑動),只執行一次。
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView;
{
}
// 滑動視圖,當手指離開屏幕那一霎那,調用該方法。一次有效滑動,只執行一次。
// decelerate,指代,當我們手指離開那一瞬後,視圖是否還將繼續向前滾動(一段距離),經過測試,decelerate=YES
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
}
// 將開始降速時
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;
{
}
// 滾動視圖減速完成,滾動將停止時,調用該方法。一次有效滑動,只執行一次。
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
}
// 滾動動畫停止時執行,代碼改變時出發,也就是setContentOffset改變時
// 當滾動視圖動畫完成後,調用該方法,如果沒有動畫,那麼該方法將不被調用
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView;
{
}
// 設置放大縮小的視圖,要是uiscrollview的subview , 返回將要縮放的UIView對象。要執行多次
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView;
{
return nil;
}
// 當將要開始縮放時,執行該方法。一次有效縮放,就只執行一次。
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
{
NSLog(@"scrollViewWillBeginZooming");
}
// 當縮放結束後,並且縮放大小回到minimumZoomScale與maximumZoomScale之間後(我們也許會超出縮放范圍),調用該方法。
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(double)scale;
{
}
// 如果你不是完全滾動到滾軸視圖的頂部,你可以輕點狀態欄,那個可視的滾軸視圖會一直滾動到頂部,那是默認行為,你可以通過該方法返回NO來關閉它
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView;
{
return YES;
}
// 已經滑動到頂部
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView;
{
}