該文章參考蘋果官方文檔:Scroll View Programming Guide for iOS
scroll View在iOSAPP的使用場景是當顯示的內(nèi)容超出屏幕區(qū)域時.
使用scroll view可以解決兩個問題:
- 讓用戶拖動顯示的內(nèi)容
- 讓用戶使用捏合手勢(UIPinchGestureRecognizer)縮放屏幕上顯示的內(nèi)容
下面是一個使用UIScrollView
的例子,在scroll view中有個UIImageView
,顯示了一個男孩;當用戶拖動他/她的手指時,屏幕顯示的圖片會移動,如下圖所示;同時還會顯示一個導航條(scroll indicators),當手指離開屏幕時,導航條就會消失不見.
預覽
UIScrollView
提供了下面的功能:
- 可以滾動顯示的內(nèi)容
- 可以縮放顯示的內(nèi)容
- 支持翻頁滾動顯示的內(nèi)容(paging mode)
UIScrollView
內(nèi)部并不包含一些特殊的控件的視圖,只是滾動它的子視圖.
ScrollView的滾動
- 簡單的滾動非常容易實現(xiàn)
- 通過簡單的拖拽或者輕劃屏幕可以實現(xiàn)滾動,并不需要類似繼承或者實現(xiàn)委托等操作來實現(xiàn).
UIScrollView
的實例提供了設置content size的接口,也可通過Interface Builder來實現(xiàn). 具體請看-創(chuàng)建并設置scroll view
scrollView縮放
-
如何進行縮放
- 通過委托(delegation)來實現(xiàn)縮放手勢
- 實現(xiàn)縮放手勢需要用到
UIScrollViewDelegate
協(xié)議,你需要實現(xiàn)其中的一些代理方法來指定那個子 view可以縮放,也需要設置minimum和maximum等影響因素.
簡單縮放-scrollView自帶捏合縮放
高級縮放-雙擊縮放 具體請看-通過點擊來縮放
高級縮放-縮放時保持圖像清晰度不變
scrollView的翻頁模式
- 只需要三個子視圖就可以實現(xiàn)scroll view的paging模式
- 在實現(xiàn)翻頁效果時,記得只需要三個子視圖就可以了,不要太多,因為考慮到內(nèi)存消耗和性能問題,三個subview就可以了,具體請看-scroll view的翻頁
scrollView的嵌套
- 同向嵌套
- 交叉嵌套
創(chuàng)建并設置scroll view
scroll view可以通過代碼和interface builder創(chuàng)建.只需要很少的設置即可獲得滾動.
創(chuàng)建Scroll Views
scroll view的創(chuàng)建和使用就是其他視圖沒啥兩樣,可以插入controller中或者其他view hierarchy中.另外需要再做兩步設置來進行scroll view創(chuàng)建和設置:
- 必須要設置contentSize,來進行scroll view的內(nèi)容大小的設置
- 必須向scroll view的中加入子view,scroll view就是通過子view來顯示內(nèi)容
你可以選擇性的配置你應用的視覺元素(visual cues),比如scroll view的垂直/水平indicators,是否拖動/縮放彈跳,是否固定方向的滑動.
使用Interface Builder創(chuàng)建Scroll View
打開Interface Builder,然后在視圖庫中拖出scroll view到容器中,然后你可以將UIViewController
的view和scroll view綁定,將scroll view當做controller中的self.view.如下圖,scroll view是File's Owner UIViewController
的view outlet:
雖然你可以在interface builder中設置UIScrollView
的大部分屬性,但是控制scroll view的滾動區(qū)域(scrollable area)的屬性contentSize
需要你通過代碼手動設置,設置的位置可以在controller(scroll view的擁有者File's Owner)中的-viewDidLoad
方法中,如下代碼清單:
//設置scroll view的大小
- (void)viewDidLoad {
[super viewDidLoad];
UIScrollView *tempScrollView = (UIScrollView *)self.view;
tempScrollView.contentSize = CGSizeMake(1280,960);
}
設置好scroll view的大小后,你可以將顯示內(nèi)容加入到scroll view中,這個過程既可以通過代碼也可以通過Interface Builder.
通過代碼創(chuàng)建scroll view
可以完全用代碼來創(chuàng)建scroll view,通常是在controller中來創(chuàng)建,更確切的說是在controller中的-loadView
方法中是實現(xiàn),下面代碼清單是一個示例:
//使用代碼來創(chuàng)建scroll view
- (void)loadView {
CGRect fullScreenRect = [[UIScreen mainScreen] applicationsFrame];
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:fullScreenRect];
scrollView.contentSize = CGSizeMake(320,758);
// do any further configuration to the scroll view
// add a view, or views, as a subview of the scroll view.
// set scrollView as self.view returns it
self.view = scrollView;
}
上面的代碼創(chuàng)建的是一個大小為屏幕大小的scroll view,并將scroll view設置為self.view. contentSize設置為(320,758),所以該scroll view可以在垂直方向上滾動.
上面的代碼可以進一步對scrollview進行設置,比如加入subViews
添加subviews
當你創(chuàng)建好scrollview后,你就需要往里面添加內(nèi)容(subviews)了.
- 如果你需要支持(zooming)縮放功能,那你通常需要將許多內(nèi)容組合在一個subview中,然后將subview添加到scrollview中.
- 如果對縮放不做要求,那么你就可以隨意往scrollview中添加內(nèi)容即可.
注意:雖然大部分情況下,都是往scroll view添加一個subview以支持縮放,但如果你需要對scroll view的subviews選擇性地來進行縮放,可以通過委托方法
-viewForZoomingInScrollView:
來指定需要需要進行縮放的subview. 這部分內(nèi)容會在使用捏合手勢進行簡單縮放講到
對scroll view的content size, content inset, scroll indicators等屬性設置
contentSize
contentSize是用來控制scrollView的內(nèi)容大小的.下圖是展示了contentSize對內(nèi)容大小的控制:
contentInset
如果想給你的內(nèi)容邊緣加上padding,比如有時scroll view中的內(nèi)容會被conroller中一些navigationBar/toolBar等控件遮住,這是需要在上/下邊緣給scrollview中的內(nèi)容加上padding,要實現(xiàn)這個功能,需要用scroll view的另一個重要屬性contentInset
.通過設置contentInset可以在scrollview的四周增加一個緩沖區(qū)域(buffer area). 你也可以認為通過設置contentInset來增大scrollview的contentSize,從而不改變它內(nèi)部的subviews的大小.
contentInset是一個UIEdgeInsets
結(jié)構(gòu)體,有個 top,bottom,left,right四個成員,如下圖是對contentInset的一個展示:
通過設置contentInset為(64,0,44,0),這樣就可以顯示controller的導航欄和toolbar而不遮住scrollview的內(nèi)容了.
代碼展示contentInset的設置
CGRect fullScreenRect = [[UIScreen mainScreen] applicationFrame];
UIScrollView* scrollView = [[UIScrollView alloc] initWithFrame:fullScreenRect];
self.view = scrollView;
scrollView.contentSize = CGSizeMake(320,758);
scrollView.contentInset = UIEdgeInsetsMake(64.0,0.0,44.0,0.0);
// do any further configuration to the scroll view.
self.view = scrollView;
下圖顯示contentInset對scroll view顯示的影響. 當將scroll滾動最上方時(左圖),屏幕上方留下了navigation bar和status bar的空間. 右圖顯示的是將scroll滾動到最底部,留了給toolbar的空間.
然而,改變contentInset的值,會產(chǎn)生一個對scroll view的indicator無法預測的副作用.當用戶拖動內(nèi)容到屏幕的頂部或者底部時,indicator會滾動到navigation/tool bar的范圍,將超出scroll view內(nèi)容顯示的區(qū)域.
為了糾正這個bug,需要同時設置scrollIndicatorInsets
屬性來配合contentInset
使用,下面代碼清單展示了這個場景:
- (void)loadView {
CGRect fullScreenRect = [[UIScreen mainScreen] applicationFrame];
UIScrollView * scrollView = [[UIScrollView alloc] initWithFrame:fullScreenRect];
scrollView.contentSize=CGSizeMake(320,758);
scrollView.contentInset=UIEdgeInsetsMake(64.0,0.0,44.0,0.0);
scrollView.scrollIndicatorInsets=UIEdgeInsetsMake(64.0,0.0,44.0,0.0);
self.view=scrollView;
}
滾動和scroll view的內(nèi)容
scroll view開始滾動的一般發(fā)生在用戶用直接用手指拖動操作屏幕. 然后scrollview中的內(nèi)容開始響應用戶的操作,這個過程可以稱為拖動手勢(drag gesture).
輕劃(flick gesture)是拖動手勢的一個變種. 輕劃手勢是用戶用手指快速在屏幕上劃動,然后離開屏幕.該手勢不僅會使屏幕滾動,還會產(chǎn)生一個沖量(imparts a momentum),既手指離開屏幕后,滾動的勢頭不會立即停止而是會繼續(xù)做減速滾動.這種手勢UIScrollView
默認幫開發(fā)者實現(xiàn)了.
但有時候,有些特殊的需求需要開發(fā)者手動實現(xiàn)這些手勢,UIScrollView
也提供了接口供開發(fā)者實現(xiàn)這部分特性需求,在UIScrollView的委托協(xié)議中UIScrollViewDelegate
提供了一些方法供開發(fā)這來控制scroll view的滾動過程.
通過代碼來控制滾動
scrollview的滾動不一定都是通過用戶手勢來控制,也可通過代碼設置來進行特殊的滾動,比如:
- 設置scroll view的
contentOffset
屬性 - 滾動到特定的區(qū)域(exposed the specific rectangular)
- 滾動的頂部(scrolls to the top)
滾動到特定offset
要是scrollView的內(nèi)容滾動特定的位置(top-left,contentOffset屬性)可以通過兩種辦法實現(xiàn).
- 方法
setContentOffset:animated:
的調(diào)用,參數(shù)animated設置為YES;scrollView會勻速滾動到特定的位置,如果animated參數(shù)設置NO,那么會瞬間跳動到特定位置.- 不管animated是NO還是YES,delegate都會調(diào)用
scrollViewDidScroll:
方法. - 如果animated=YES,在滾動動畫期間delegate會多次調(diào)用
scrollViewDidScroll:
,當動畫結(jié)束后delegate會調(diào)用scrollViewDidEndScrollingAnimation:
- 不管animated是NO還是YES,delegate都會調(diào)用
- 直接通過代碼設置contentOffset(CGPoint),不會產(chǎn)生動畫,調(diào)用一次
scrollViewDidScroll:
顯示特定區(qū)域(rectangle)
有時需要將scroll view滾動到特定區(qū)域,以顯示特定區(qū)域的內(nèi)容,特別地,當要展示的內(nèi)容是一個在屏幕顯示區(qū)域外的控件時,這個功能比較有用.
- 方法
scrollRectToVisible:animated:
可以指定特定的區(qū)域滾動到顯示區(qū)域.當需要做動畫是,animated設置YES. - 當animated為YES時,delegate會多次調(diào)用
scrollViewDidScroll:
,動畫結(jié)束后再調(diào)用scrollViewDidEndScrollingAnimation:
- 使用
scrollRectToVisible:animated:
滾動特定區(qū)域時,scrollView的屬性tracking
和dragging
的值為NO(這些屬性后面會講到)
滾動到頂部(scroll to top)
如果狀態(tài)欄可見,可以單擊狀態(tài)欄使scrollView滾動到頂部.這個特性在很多應用都有,非常方便用戶瀏覽頂部的內(nèi)容,比如iPhone自帶應用Photos有這個特性,方便用戶上翻內(nèi)容. 大多數(shù)UITableView(UIScrollView的子類)實現(xiàn)了這個功能.
- 要想支持該特性只需要實現(xiàn)委托方法
scrollViewShouldScrollToTop:
,在里面return YES就好了,在里面還可以判斷那個scrollView需要支持該特性. - 當滾動到頂部結(jié)束后,delegate會調(diào)用
scrollViewDidScrollToTop:
,里面指定了是那個ScrollView.
scroll View滾動時,delegate回調(diào)委托方法的過程
當scroll view滾動時,scroll view會同時跟蹤一些屬性值的改變以記錄當前scroll view的狀態(tài),這些屬性有:tracking,dragging,decelerating,zooming,zoomBouncing. 另外屬性contentOffset記錄了當前內(nèi)容的左上角在屏幕上的位置,既當前scrollview滾動到了那個位置.
State property | Description |
---|---|
tracking | YES 當用戶的手指接觸屏幕時 |
dragging | YES 當用戶在屏幕上拖動時 |
decelerating | YES 當用戶使用flick手勢時,或者拖動scrollView超過邊界彈跳時 |
zooming | YES 當用戶使用捏合手勢時去改變scrollview的屬性zoomScale時 |
contentOffset | 它的值為CGPoint,它表示內(nèi)容滾動的位置 |
在滾動的時候,沒必要循環(huán)遍歷上述屬性值,在滾動時delegate會調(diào)用的方法來告訴開發(fā)者當前scroll view的狀態(tài). 在相應的委托方法中,開發(fā)者可以做一些相應的處理來使scrollview符合自己的需求.在這些方法中可以訪問上述的幾個狀態(tài)值,以確定scrollView的狀態(tài).
標記scroll view滾動開始和結(jié)束的簡單方式
- 如果你的應用只關(guān)心scroll的起始和結(jié)束狀態(tài),那么你只需要關(guān)心少數(shù)幾個委托方法.
- 當scroll開始滾動時delegate會調(diào)用
scrollViewWillBeginDragging:
- 當scrollview結(jié)束滾動式會調(diào)用兩個
scrollViewDidEndDragging:willDecelerate:
(decelerate 參數(shù)為NO時)和scrollViewDidEndDecelerating:
,只要調(diào)用二者之一就說明scroll結(jié)束了.
滾動時delegate方法調(diào)用的整個過程(Delegate-Message-Sequence)
- 當用戶接觸屏幕時
tracking
立即改為YES,只要用戶一直接觸屏幕tracking的值就不會改變. - 如果用戶手指按住屏幕不動,scroll的content view開始響應改觸摸事件,導致delegate方法調(diào)用過程結(jié)束, 如果用戶開始移動手指的話,message-sequence開始
- 當用戶拖動手指時,scroll view會取消touch事件的處理程序,直接開始進行delegate的message-sequence處理.
- 當delegate調(diào)用
scrollViewWillBeginDragging:
時,dragging的值為YES. - 當用戶拖動手指時,delegate調(diào)用
scrollViewDidScroll:
,而且拖動過程中該方法會一直被調(diào)用.在方法中可以訪問contentOffset的值來查看當前scrollView滾動到了那個位置. - 如果用戶使用flick手勢的話,里面涉及到了一個滾動減速問題. tracking的值為NO,delegate調(diào)用
scrollViewDidEndDragging:willDecelerate:
方法,并且decelerate參數(shù)為YES.開始做減速滾動,這一滾動過程中的減速運動受屬性decelerationRate
的影響. 屬性decelerating
為YES. - 當用停止拖動時,delegate會調(diào)用
scrollViewDidEndDragging:willDecelerate
,這時因為是拖動手勢,所以參數(shù)deceleration為NO.同時將tracking和decelerating設置為NO,message-sequence結(jié)束. - 另外當用戶給允許scrollview可以彈跳(bounces)的話,delegate也會調(diào)用
scrollViewDidEndDragging:willDecelerate
,deceleration參數(shù)為YES;即使用戶長按scrollView然后離開屏幕后也會調(diào)用上述方法. 這個過程受到屬性bounces/alwaysBounceVertical/alwaysBounceHorizontal的影響,另外當bounces為NO時,其他兩個屬性也不管用了.當bounces為YES時,scrollView的contentSize小于scrollView的bounds時才會發(fā)生bouncing. - 只要調(diào)用了
scrollViewDidEndDragging:willDecelerate:
方法,且deceleration參數(shù)為YES時,在scrollView做減速滾動過程中delegate會繼續(xù)調(diào)用scrollViewWillBeginDecelerating:
,而且還會多次調(diào)用scrollViewDidScroll:
,此時dragging/tracking為NO,decelerating為YES - 最終,減速滾動結(jié)束,delegate會調(diào)用
scrollViewDidEndDecelerating:
方法,且decelerating為NO.
注意:當scrollView進行縮放時,tracking/dragging的值可能一直為NO,zooming為YES,這種情況存在.
使用捏合手勢進行簡單縮放
UIScrollView的縮放非常容易實現(xiàn),scrollView本身自帶捏合手勢(pinch gesture)進行縮放. 實現(xiàn)步驟是:
- 設置縮放因子(zoom factors:對內(nèi)容的縮放程度),設置
minimumZoomScale
和maximumZoomScale
- 然后再實現(xiàn)一個delegate方法
viewForZoomingInScrollView:
如何使用Pinch Gesture來進行縮放
-
捏合手勢是iOS應用的一個標準手勢,分pinch-in/pinch-out兩個動作,pinch-in進行縮小,pinch-out進行放大,如下圖展示:
為了使scrollView支持縮放,開發(fā)者需要給scrollView設置一個delegate.該delegate需要實現(xiàn)
UISCrollViewDelegate
協(xié)議中的viewForZommingInScrollView:
方法,在該方法中返回需要進行縮放的內(nèi)容(contents,or subviews).在下面代碼清單展示了如何使用
viewForZommingInScrollView:
delegate方法來指定需要縮放的內(nèi)容,當進行捏合手勢時,控制器的Imageview需要響應該手勢進行縮放動作:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return self.imageView;
}
- 可以通過設置屬性
minimumZoomScale
和maximumZoomScale
這兩個縮放因子來控制縮放程度,這兩個屬性的初始值為1.0, 進行縮放時maximumZoomScale必須大于minimumZoomScale. 可以通過代碼或者InterfaceBuilder來進行設置,下面的代碼展示了如何使用這兩個屬性:
- (void)viewDidLoad {
[super viewDidLoad];
self.scrollView.minimumZoomScale=0.5;
self.scrollView.maximumZoomScale=6.0;
self.scrollView.contentSize=CGSizeMake(1280, 960);
self.scrollView.delegate=self;
}
- 設置縮放因子(zoom factors)和實現(xiàn)delegate方法這兩個步驟是使scrollView支持縮放的基本條件
代碼控制縮放
-
除了使用捏合手勢進行控制縮放,還有其他條件可以進行scrollView的縮放,比如用戶雙擊手勢(double taps gestures)或者其他手勢.所以UIScrollView還提供了下面兩個接口供開發(fā)者控制縮放:
- 方法
setZoomScale:animated:
- 方法
zoomToRect:animated:
- 方法
使用
setZoomScale:animated:
給屬性zoomScale
賦值,該值范圍是minimumZoomScale-maximumZoomScale. 如果animated = YES,那么縮放過程是一個動畫,如果為NO,那么縮放效果是瞬間完成,直接給zoomScale賦值也是瞬間的,沒有動畫效果. 在縮放過程中,進行縮放的view的center不改變,也就是位置不變,只縮放.使用
zoomToRect:animated:
方法進行縮放時,會縮放以剛好充滿指定的rect.該方法在進行縮放時會改變位置,參數(shù)animated的值對縮放的影響和setZoomScale:animted:
一樣.有時需要根據(jù)用戶點擊屏幕(tap gesture)來設置zoom scale和location, 開發(fā)者經(jīng)常會遇到這樣的需求.因為
setZoomScale:animated:
進行縮放時位置不變,所以滿足不了需求,所以要使用zoomToRect:animated:
,下面是一個工具方法,用來計算rect(位置):
- (CGRect)zoomRectForScrollView:(UIScrollView *)scrollView withScale:(float)scale withCenter:(CGPoint)center {
CGRect zoomRect;
// The zoom rect is in the content view's coordinates.
// At a zoom scale of 1.0, it would be the size of the
// imageScrollView's bounds.
// As the zoom scale decreases, so more content is visible,
// the size of the rect grows.
zoomRect.size.height = scrollView.frame.size.height / scale;
zoomRect.size.width = scrollView.frame.size.width / scale;
// choose an origin so as to get the right center.
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0);
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0);
return zoomRect;
}
上面的工具方法在"雙擊進行縮放"時很有用. 要使用上面的工具方法需要傳一個scrollView,一個newScale(通常為zoomScale+zoomAmount或者zoomScale*zoomAmout),需要進行縮放的中心點,得到rect后,將rect傳入zoomToRect:animated:
方法中
當zoom結(jié)束時通知delegate
- 當scrollView的縮放結(jié)束后,delegate會調(diào)用
scrollViewDidEndZooming:withView:atScale:
方法 - 該方法調(diào)用時會傳入scrollView,進行縮放的subview,縮放因子scaleFactor
在進行縮放時如何保持圖像清晰
- scrollView在縮放時,僅僅是對內(nèi)容根據(jù)zoomScale進行大小縮放,這會導致圖像不夠清晰.因為縮放時,內(nèi)容不會重繪.
- 如果你希望內(nèi)容在縮放時需要保持清晰,那么你可以去參考蘋果提供的demo:ScrollViewSuit. ScrollViewSuit會使用一種提前渲染(pre-rendering)的技術(shù)來縮放內(nèi)容,然后再單獨展示它.
- 如果你進行縮放的內(nèi)容是實時繪制且縮放時還要保持清晰,那么需要用到
Core Animation
,將UIView的layer改為CATileLayer,并且使用drawLayer:inContext:
進行繪制 - 下面的代碼清單展示了UIView的子類ZoomableView.該類用于縮放且能保持清晰時使用.
#import "ZoomableView.h"
#import <QuartzCore/QuartzCore.h>
@implementation ZoomableView
// Set the UIView layer to CATiledLayer
+(Class)layerClass
{
return [CATiledLayer class];
}
// Initialize the layer by setting
// the levelsOfDetailBias of bias and levelsOfDetail
// of the tiled layer
-(id)initWithFrame:(CGRect)r
{
self = [super initWithFrame:r];
if(self) {
CATiledLayer *tempTiledLayer = (CATiledLayer*)self.layer;
tempTiledLayer.levelsOfDetail = 5;
tempTiledLayer.levelsOfDetailBias = 2;
self.opaque=YES;
}
return self;
}
// Implement -drawRect: so that the UIView class works correctly
// Real drawing work is done in -drawLayer:inContext
-(void)drawRect:(CGRect)r
{
}
-(void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context
{
// The context is appropriately scaled and translated such that you can draw to this context
// as if you were drawing to the entire layer and the correct content will be rendered.
// We assume the current CTM will be a non-rotated uniformly scaled
// affine transform, which implies that
// a == d and b == c == 0
// CGFloat scale = CGContextGetCTM(context).a;
// While not used here, it may be useful in other situations.
// The clip bounding box indicates the area of the context that
// is being requested for rendering. While not used here
// your app may require it to do scaling in other
// situations.
// CGRect rect = CGContextGetClipBoundingBox(context);
// Set and draw the background color of the entire layer
// The other option is to set the layer as opaque=NO;
// eliminate the following two lines of code
// and set the scroll view background color
CGContextSetRGBFillColor(context, 1.0,1.0,1.0,1.0);
CGContextFillRect(context,self.bounds);
// draw a simple plus sign
CGContextSetRGBStrokeColor(context, 0.0, 0.0, 1.0, 1.0);
CGContextBeginPath(context);
CGContextMoveToPoint(context,35,255);
CGContextAddLineToPoint(context,35,205);
CGContextAddLineToPoint(context,135,205);
CGContextAddLineToPoint(context,135,105);
CGContextAddLineToPoint(context,185,105);
CGContextAddLineToPoint(context,185,205);
CGContextAddLineToPoint(context,285,205);
CGContextAddLineToPoint(context,285,255);
CGContextAddLineToPoint(context,185,255);
CGContextAddLineToPoint(context,185,355);
CGContextAddLineToPoint(context,135,355);
CGContextAddLineToPoint(context,135,255);
CGContextAddLineToPoint(context,35,255);
CGContextClosePath(context);
// Stroke the simple shape
CGContextStrokePath(context);
}
注意: 上述代碼有很大的使用限制,UIKit繪制時線程不安全的,而core graphic是線程安全的,drawLayer:inRect:
的調(diào)用是發(fā)生在后臺線程中的,所以里面的繪制要是core graphic.
通過點擊來縮放
通過上面學習我們知道要進行縮放很簡單,通過捏合手勢等很容易實現(xiàn).但有些場景的縮放需求比較復雜,比如雙擊縮放,我們需要對tap手勢的探測來進行特定的縮放,比較典型的例子如地圖的雙擊縮放效果.根據(jù)點擊的手指數(shù),點擊次數(shù),連續(xù)點擊的速度,可以進行不同的縮放處理,所以這一效果比較復雜,需要重寫UIView中touch的處理方法(touchesBegan..,touchesEnded..,touchesCanceled..)
重寫UIView的Touch-Handing方法
為檢測不同點擊動作響應不同的縮放效果,所以需要重寫touch-handing方法,這里可以參考ScrollViewSuit中的列子TapToZoom中的類TapDetectingImageView,它是UIImageView的子類.下面就開始講者個類的實現(xiàn)
Initialization
- 進行"點擊縮放"的視圖需要在使用
initWithImage
初始化時開啟User Interaction(用戶交互)和multiple touches(多次安觸碰) - 變量
twoFingerTapIsPossible
用來記錄觸摸屏幕的手指數(shù)是否超過2(手指數(shù)>2:NO) - 變量
multipleTouches
用來記錄touches event的數(shù)量是否超過1(touch event > 1:YES) - 變量
tapLocation
用來記錄點擊的位置(當是雙手指的時候,表示兩個指頭間的中間點)
請看下面的代碼
- (id)initWithImage:(UIImage *)image {
self = [super initWithImage:image];
if (self) {
[self setUserInteractionEnabled:YES];
[self setMultipleTouchEnabled:YES];
twoFingerTapIsPossible = YES;
multipleTouches = NO;
}
return self;
}
The touchesBegan:withEvent: Implementation
- 取消單擊手勢動作
handleSingleTap
- 設置狀態(tài)變量
multipleTouches
,twoFingerTapIsPossible
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Cancel any pending handleSingleTap messages.
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleSingleTap) object:nil];
// Update the touch state.
if ([[event touchesForView:self] count] > 1)
multipleTouches = YES;
if ([[event touchesForView:self] count] > 2)
twoFingerTapIsPossible = NO;
}
The touchesEnded:withEvent: Implementation
- 主要實現(xiàn)都在這個方法,比較長
- 函數(shù)
midPointBetweenPoints
用來計算兩個touch的中間點
代碼如下:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
BOOL allTouchesEnded = ([touches count] == [[event touchesForView:self] count]);
// first check for plain single/double tap, which is only possible if we haven't seen multiple touches
if (!multipleTouches) {
UITouch *touch = [touches anyObject];
tapLocation = [touch locationInView:self];
if ([touch tapCount] == 1) {
[self performSelector:@selector(handleSingleTap)
withObject:nil
afterDelay:DOUBLE_TAP_DELAY];
} else if([touch tapCount] == 2) {
[self handleDoubleTap];
}
}
// Check for a 2-finger tap if there have been multiple touches
// and haven't that situation has not been ruled out
else if (multipleTouches && twoFingerTapIsPossible) {
// case 1: this is the end of both touches at once
if ([touches count] == 2 && allTouchesEnded) {
int i = 0;
int tapCounts[2];
CGPoint tapLocations[2];
for (UITouch *touch in touches) {
tapCounts[i] = [touch tapCount];
tapLocations[i] = [touch locationInView:self];
i++;
}
if (tapCounts[0] == 1 && tapCounts[1] == 1) {
// it's a two-finger tap if they're both single taps
tapLocation = midpointBetweenPoints(tapLocations[0],
tapLocations[1]);
[self handleTwoFingerTap];
}
}
// Case 2: this is the end of one touch, and the other hasn't ended yet
else if ([touches count] == 1 && !allTouchesEnded) {
UITouch *touch = [touches anyObject];
if ([touch tapCount] == 1) {
// If touch is a single tap, store its location
// so it can be averaged with the second touch location
tapLocation = [touch locationInView:self];
} else {
twoFingerTapIsPossible = NO;
}
}
// Case 3: this is the end of the second of the two touches
else if ([touches count] == 1 && allTouchesEnded) {
UITouch *touch = [touches anyObject];
if ([touch tapCount] == 1) {
// if the last touch up is a single tap, this was a 2-finger tap
tapLocation = midpointBetweenPoints(tapLocation,
[touch locationInView:self]);
[self handleTwoFingerTap];
}
}
}
// if all touches are up, reset touch monitoring state
if (allTouchesEnded) {
twoFingerTapIsPossible = YES;
multipleTouches = NO;
}
}
The touchesCancelled:withEvent: Implementation
如果scrollview上的點擊手勢變成拖動手勢時,調(diào)用此方法:
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
twoFingerTapIsPossible = YES;
multipleTouches = NO;
}
ScrollViewSuite-蘋果講解ScrollView高級用法的示例代碼
在ScrollViewSuite包含了很多scrollView的高級用法代碼實例,比如TapToZoom可以用在地圖應用,可以多去參考借鑒
scroll view的翻頁
UIScrollView有種模式叫翻頁模式(paging mode),指用戶只能一屏一屏的滾動scrollView中的內(nèi)容.經(jīng)常用來展示一系列的內(nèi)容,比如電紙書/指導頁
如何設置翻頁模式
- scrollView的翻頁模式需要使用代碼設置
- 初始化scrollView后需要將屬性
pagingMode
設置為YES - 如果
contentSize
的height設置為屏幕的高度,那么width需要為屏幕寬度的整數(shù)倍 - 使用UIPageControl替代UIScrollView自帶的indicator
- 下圖顯示展示了例子PageControl: Using a Paginated UIScrollView
如何設置翻頁的內(nèi)容
有兩種方式:
- 在同一個view中一次性全部加載完內(nèi)容(適合內(nèi)容較少的時候)
- 使用多個(最好3個)view來部分加載當前要顯示的內(nèi)容和將要顯示的內(nèi)容(適合內(nèi)容較多,加載完全部內(nèi)容需要更多時間),具體請看apple實例代碼:PageControl:USing a Paginated UIScrollView
在使用多個view展示內(nèi)容時:
- 使用3個頁面來展示展示內(nèi)容,第一頁展示已經(jīng)顯示過得內(nèi)容,第二頁展示當前顯示的內(nèi)容,第三頁顯示將要顯示的內(nèi)容,這樣既不浪費內(nèi)存又不耽誤顯示
- 在controller初始化時,翻頁的三個頁面就要開始初始化,計算好三個頁面的位置,準備滾動操作,三個頁面要交替循環(huán)顯示對應的內(nèi)容.
- 需要實現(xiàn)delegate方法
scrollViewDidScroll:
,用來跟蹤scrollView的contentOffset,判斷何時超過scrollview的中間.根據(jù)手指滑動的方向判斷要顯示的頁面(next/first page).然后重繪將要顯示的內(nèi)容 - 根據(jù)上面的策略可以顯示大量的頁面
- 如果頁面的創(chuàng)建比較耗時,可以創(chuàng)建一個view pool來存放將要顯示的頁面,類似tableView
scrollView的嵌套(Nesting Scroll View)
iOS3.0之前不支持嵌套,之后的話嵌套變得比較容易了
scrollView的嵌套分為同向嵌套(same-direction scrolling)和交叉嵌套(cross-direction scrolling)
同向嵌套
指scrollView中的subview也是scrollView,且滾動方向是相同的,如下圖展示兩種不同的嵌套
交叉嵌套
子scrollView的滾動方向和父scrollView的滾動方向是垂直的.
就像Apple自帶的股票
應用一樣,底層是水平方向翻頁的scrollView,但頂層是一個垂直方向滾動的tableView
示例代碼
- Scroll 展示了scroll view的滾動效果
- PageControl:USing a Paginated UIScrollView 展示了scroll view的翻頁效果
- ScrollViewSuite 展示了scroll view的高級使用
- PhotoScroller 使用UIPageViewController,UIScrollView和CATiledLayer來展示巨型(高分辨率)圖片,能在翻頁/拖動/縮放時無壓力