iOS Scroll View 編程指導

該文章參考蘋果官方文檔: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`的例子

預覽

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:

UIViewController和scroll view的綁定關(guān)系

雖然你可以在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)了.

  1. 如果你需要支持(zooming)縮放功能,那你通常需要將許多內(nèi)容組合在一個subview中,然后將subview添加到scrollview中.
  2. 如果對縮放不做要求,那么你就可以隨意往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)容大小的控制:


contentSize

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

通過設置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中top/bottom后的scroll view

然而,改變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).

  1. 方法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:
  2. 直接通過代碼設置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的屬性trackingdragging的值為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)容的縮放程度),設置minimumZoomScalemaximumZoomScale
  • 然后再實現(xiàn)一個delegate方法viewForZoomingInScrollView:

如何使用Pinch Gesture來進行縮放

  • 捏合手勢是iOS應用的一個標準手勢,分pinch-in/pinch-out兩個動作,pinch-in進行縮小,pinch-out進行放大,如下圖展示:


    iOS應用的標準捏合手指
  • 為了使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;
}
  • 可以通過設置屬性minimumZoomScalemaximumZoomScale這兩個縮放因子來控制縮放程度,這兩個屬性的初始值為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)容

有兩種方式:

  1. 在同一個view中一次性全部加載完內(nèi)容(適合內(nèi)容較少的時候)
  2. 使用多個(最好3個)view來部分加載當前要顯示的內(nèi)容和將要顯示的內(nèi)容(適合內(nèi)容較多,加載完全部內(nèi)容需要更多時間),具體請看apple實例代碼:PageControl:USing a Paginated UIScrollView

在使用多個view展示內(nèi)容時:

  1. 使用3個頁面來展示展示內(nèi)容,第一頁展示已經(jīng)顯示過得內(nèi)容,第二頁展示當前顯示的內(nèi)容,第三頁顯示將要顯示的內(nèi)容,這樣既不浪費內(nèi)存又不耽誤顯示
  2. 在controller初始化時,翻頁的三個頁面就要開始初始化,計算好三個頁面的位置,準備滾動操作,三個頁面要交替循環(huán)顯示對應的內(nèi)容.
  3. 需要實現(xiàn)delegate方法scrollViewDidScroll:,用來跟蹤scrollView的contentOffset,判斷何時超過scrollview的中間.根據(jù)手指滑動的方向判斷要顯示的頁面(next/first page).然后重繪將要顯示的內(nèi)容
  4. 根據(jù)上面的策略可以顯示大量的頁面
  5. 如果頁面的創(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的滾動方向和父scrollView的滾動方向是垂直的.
就像Apple自帶的股票應用一樣,底層是水平方向翻頁的scrollView,但頂層是一個垂直方向滾動的tableView


示例代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌翎苫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件牺汤,死亡現(xiàn)場離奇詭異寞蚌,居然都是意外死亡于游,警方通過查閱死者的電腦和手機窖壕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門忧勿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞻讽,你說我怎么就攤上這事狐蜕。” “怎么了卸夕?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長婆瓜。 經(jīng)常有香客問我快集,道長贡羔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任个初,我火速辦了婚禮乖寒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘院溺。我一直安慰自己楣嘁,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布珍逸。 她就那樣靜靜地躺著逐虚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谆膳。 梳的紋絲不亂的頭發(fā)上叭爱,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音漱病,去河邊找鬼买雾。 笑死,一個胖子當著我的面吹牛杨帽,可吹牛的內(nèi)容都是我干的漓穿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼注盈,長吁一口氣:“原來是場噩夢啊……” “哼晃危!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起当凡,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤山害,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沿量,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浪慌,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年朴则,在試婚紗的時候發(fā)現(xiàn)自己被綠了权纤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡乌妒,死狀恐怖汹想,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撤蚊,我是刑警寧澤古掏,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站侦啸,受9級特大地震影響槽唾,放射性物質(zhì)發(fā)生泄漏丧枪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一庞萍、第九天 我趴在偏房一處隱蔽的房頂上張望拧烦。 院中可真熱鬧,春花似錦钝计、人聲如沸恋博。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽债沮。三九已至,卻和暖如春践付,著一層夾襖步出監(jiān)牢的瞬間秦士,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工永高, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留隧土,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓命爬,卻偏偏與公主長得像曹傀,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子饲宛,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1皆愉、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_x閱讀 15,968評論 3 119
  • 早晨是被一通父親的電話打醒的。我們之間的關(guān)系向來不好艇抠,我覺得他不是一個好父親他則覺得我不是一個爭氣的兒子幕庐。想想也是...
    人生若霧懵懂閱讀 445評論 1 0
  • 文/王裕森每篇文章都是自我的一種感悟异剥,帶有強烈的自我色彩,揭露了一些客觀真理絮重,不喜勿讀冤寿! 本來這個世界應該是命中注...
    大幟閱讀 332評論 2 4
  • 1 超市的粽子山一樣高,各式的粽子散發(fā)著節(jié)日的氣息青伤。站在熱鬧的粽子間督怜,恍惚看到了熟悉的古舊深隧院落里,賢淑溫和的奶...
    鄭劍霓閱讀 397評論 0 2
  • 兒子心心念念的跑步機今天安裝到位狠角,現(xiàn)在他拿著說明書仔細的看号杠,在跑道上試跑,這么胖了本應該早點買丰歌,原來不買...
    姚亞君閱讀 171評論 0 3