iOS Scroll View 编程指导

该文章参考苹果官方文档:Scroll View Programming Guide for iOS

scroll View在iOSAPP的使用场景是当显示的内容超出屏幕区域时.

使用scroll view可以解决两个问题:

  • 让用户拖动显示的内容
  • 让用户使用捏合手势(UIPinchGestureRecognizer)缩放屏幕上显示的内容

下面是一个使用UIScrollView的例子,在scroll view中有个UIImageView,显示了一个男孩;当用户拖动他/她的手指时,屏幕显示的图片会移动,如下图所示;同时还会显示一个导航条(scroll indicators),当手指离开屏幕时,导航条就会消失不见.

一个使用`UIScrollView`的例子

预览

UIScrollView提供了下面的功能:

  • 可以滚动显示的内容
  • 可以缩放显示的内容
  • 支持翻页滚动显示的内容(paging mode)

UIScrollView内部并不包含一些特殊的控件的视图,只是滚动它的子视图.

ScrollView的滚动

  • 简单的滚动非常容易实现
  • 通过简单的拖拽或者轻划屏幕可以实现滚动,并不需要类似继承或者实现委托等操作来实现.
    UIScrollView的实例提供了设置content size的接口,也可通过Interface Builder来实现. 具体请看-创建并设置scroll view

scrollView缩放

  • 如何进行缩放

    • 通过委托(delegation)来实现缩放手势
    • 实现缩放手势需要用到UIScrollViewDelegate协议,你需要实现其中的一些代理方法来指定那个子 view可以缩放,也需要设置minimum和maximum等影响因素.
  • 简单缩放-scrollView自带捏合缩放

  • 高级缩放-双击缩放 具体请看-通过点击来缩放

  • 高级缩放-缩放时保持图像清晰度不变

scrollView的翻页模式

  • 只需要三个子视图就可以实现scroll view的paging模式
  • 在实现翻页效果时,记得只需要三个子视图就可以了,不要太多,因为考虑到内存消耗和性能问题,三个subview就可以了,具体请看-scroll view的翻页

scrollView的嵌套

  • 同向嵌套
  • 交叉嵌套

创建并设置scroll view

scroll view可以通过代码和interface builder创建.只需要很少的设置即可获得滚动.

创建Scroll Views

scroll view的创建和使用就是其他视图没啥两样,可以插入controller中或者其他view hierarchy中.另外需要再做两步设置来进行scroll view创建和设置:

  • 必须要设置contentSize,来进行scroll view的内容大小的设置
  • 必须向scroll view的中加入子view,scroll view就是通过子view来显示内容

你可以选择性的配置你应用的视觉元素(visual cues),比如scroll view的垂直/水平indicators,是否拖动/缩放弹跳,是否固定方向的滑动.

使用Interface Builder创建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的绑定关系

虽然你可以在interface builder中设置UIScrollView的大部分属性,但是控制scroll view的滚动区域(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的大小后,你可以将显示内容加入到scroll view中,这个过程既可以通过代码也可以通过Interface Builder.

通过代码创建scroll view

可以完全用代码来创建scroll view,通常是在controller中来创建,更确切的说是在controller中的-loadView方法中是实现,下面代码清单是一个示例:

//使用代码来创建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;
}

上面的代码创建的是一个大小为屏幕大小的scroll view,并将scroll view设置为self.view. contentSize设置为(320,758),所以该scroll view可以在垂直方向上滚动.
上面的代码可以进一步对scrollview进行设置,比如加入subViews

添加subviews

当你创建好scrollview后,你就需要往里面添加内容(subviews)了.

  1. 如果你需要支持(zooming)缩放功能,那你通常需要将许多内容组合在一个subview中,然后将subview添加到scrollview中.
  2. 如果对缩放不做要求,那么你就可以随意往scrollview中添加内容即可.

注意:虽然大部分情况下,都是往scroll view添加一个subview以支持缩放,但如果你需要对scroll view的subviews选择性地来进行缩放,可以通过委托方法-viewForZoomingInScrollView:来指定需要需要进行缩放的subview. 这部分内容会在使用捏合手势进行简单缩放讲到

对scroll view的content size, content inset, scroll indicators等属性设置

contentSize

contentSize是用来控制scrollView的内容大小的.下图是展示了contentSize对内容大小的控制:


contentSize

contentInset

如果想给你的内容边缘加上padding,比如有时scroll view中的内容会被conroller中一些navigationBar/toolBar等控件遮住,这是需要在上/下边缘给scrollview中的内容加上padding,要实现这个功能,需要用scroll view的另一个重要属性contentInset.通过设置contentInset可以在scrollview的四周增加一个缓冲区域(buffer area). 你也可以认为通过设置contentInset来增大scrollview的contentSize,从而不改变它内部的subviews的大小.

contentInset是一个UIEdgeInsets结构体,有个 top,bottom,left,right四个成员,如下图是对contentInset的一个展示:

contentInset

通过设置contentInset为(64,0,44,0),这样就可以显示controller的导航栏和toolbar而不遮住scrollview的内容了.

代码展示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的值,会产生一个对scroll view的indicator无法预测的副作用.当用户拖动内容到屏幕的顶部或者底部时,indicator会滚动到navigation/tool bar的范围,将超出scroll view内容显示的区域.

为了纠正这个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的内容

scroll view开始滚动的一般发生在用户用直接用手指拖动操作屏幕. 然后scrollview中的内容开始响应用户的操作,这个过程可以称为拖动手势(drag gesture).

轻划(flick gesture)是拖动手势的一个变种. 轻划手势是用户用手指快速在屏幕上划动,然后离开屏幕.该手势不仅会使屏幕滚动,还会产生一个冲量(imparts a momentum),既手指离开屏幕后,滚动的势头不会立即停止而是会继续做减速滚动.这种手势UIScrollView默认帮开发者实现了.

但有时候,有些特殊的需求需要开发者手动实现这些手势,UIScrollView也提供了接口供开发者实现这部分特性需求,在UIScrollView的委托协议中UIScrollViewDelegate提供了一些方法供开发这来控制scroll view的滚动过程.

通过代码来控制滚动

scrollview的滚动不一定都是通过用户手势来控制,也可通过代码设置来进行特殊的滚动,比如:

  • 设置scroll view的contentOffset属性
  • 滚动到特定的区域(exposed the specific rectangular)
  • 滚动的顶部(scrolls to the top)

滚动到特定offset

要是scrollView的内容滚动特定的位置(top-left,contentOffset属性)可以通过两种办法实现.

  1. 方法setContentOffset:animated:的调用,参数animated设置为YES;scrollView会匀速滚动到特定的位置,如果animated参数设置NO,那么会瞬间跳动到特定位置.
    • 不管animated是NO还是YES,delegate都会调用scrollViewDidScroll:方法.
    • 如果animated=YES,在滚动动画期间delegate会多次调用scrollViewDidScroll:,当动画结束后delegate会调用scrollViewDidEndScrollingAnimation:
  2. 直接通过代码设置contentOffset(CGPoint),不会产生动画,调用一次scrollViewDidScroll:

显示特定区域(rectangle)

有时需要将scroll view滚动到特定区域,以显示特定区域的内容,特别地,当要展示的内容是一个在屏幕显示区域外的控件时,这个功能比较有用.

  • 方法scrollRectToVisible:animated:可以指定特定的区域滚动到显示区域.当需要做动画是,animated设置YES.
  • 当animated为YES时,delegate会多次调用scrollViewDidScroll:,动画结束后再调用scrollViewDidEndScrollingAnimation:
  • 使用scrollRectToVisible:animated:滚动特定区域时,scrollView的属性trackingdragging的值为NO(这些属性后面会讲到)

滚动到顶部(scroll to top)

如果状态栏可见,可以单击状态栏使scrollView滚动到顶部.这个特性在很多应用都有,非常方便用户浏览顶部的内容,比如iPhone自带应用Photos有这个特性,方便用户上翻内容. 大多数UITableView(UIScrollView的子类)实现了这个功能.

  • 要想支持该特性只需要实现委托方法scrollViewShouldScrollToTop:,在里面return YES就好了,在里面还可以判断那个scrollView需要支持该特性.
  • 当滚动到顶部结束后,delegate会调用scrollViewDidScrollToTop:,里面指定了是那个ScrollView.

scroll View滚动时,delegate回调委托方法的过程

当scroll view滚动时,scroll view会同时跟踪一些属性值的改变以记录当前scroll view的状态,这些属性有:tracking,dragging,decelerating,zooming,zoomBouncing. 另外属性contentOffset记录了当前内容的左上角在屏幕上的位置,既当前scrollview滚动到了那个位置.

State property Description
tracking YES 当用户的手指接触屏幕时
dragging YES 当用户在屏幕上拖动时
decelerating YES 当用户使用flick手势时,或者拖动scrollView超过边界弹跳时
zooming YES 当用户使用捏合手势时去改变scrollview的属性zoomScale时
contentOffset 它的值为CGPoint,它表示内容滚动的位置

在滚动的时候,没必要循环遍历上述属性值,在滚动时delegate会调用的方法来告诉开发者当前scroll view的状态. 在相应的委托方法中,开发者可以做一些相应的处理来使scrollview符合自己的需求.在这些方法中可以访问上述的几个状态值,以确定scrollView的状态.

标记scroll view滚动开始和结束的简单方式

  • 如果你的应用只关心scroll的起始和结束状态,那么你只需要关心少数几个委托方法.
  • 当scroll开始滚动时delegate会调用scrollViewWillBeginDragging:
  • 当scrollview结束滚动式会调用两个scrollViewDidEndDragging:willDecelerate:(decelerate 参数为NO时)和scrollViewDidEndDecelerating:,只要调用二者之一就说明scroll结束了.

滚动时delegate方法调用的整个过程(Delegate-Message-Sequence)

  • 当用户接触屏幕时tracking立即改为YES,只要用户一直接触屏幕tracking的值就不会改变.
  • 如果用户手指按住屏幕不动,scroll的content view开始响应改触摸事件,导致delegate方法调用过程结束, 如果用户开始移动手指的话,message-sequence开始
  • 当用户拖动手指时,scroll view会取消touch事件的处理程序,直接开始进行delegate的message-sequence处理.
  • 当delegate调用scrollViewWillBeginDragging:时,dragging的值为YES.
  • 当用户拖动手指时,delegate调用scrollViewDidScroll:,而且拖动过程中该方法会一直被调用.在方法中可以访问contentOffset的值来查看当前scrollView滚动到了那个位置.
  • 如果用户使用flick手势的话,里面涉及到了一个滚动减速问题. tracking的值为NO,delegate调用scrollViewDidEndDragging:willDecelerate:方法,并且decelerate参数为YES.开始做减速滚动,这一滚动过程中的减速运动受属性decelerationRate的影响. 属性decelerating为YES.
  • 当用停止拖动时,delegate会调用scrollViewDidEndDragging:willDecelerate,这时因为是拖动手势,所以参数deceleration为NO.同时将tracking和decelerating设置为NO,message-sequence结束.
  • 另外当用户给允许scrollview可以弹跳(bounces)的话,delegate也会调用scrollViewDidEndDragging:willDecelerate,deceleration参数为YES;即使用户长按scrollView然后离开屏幕后也会调用上述方法. 这个过程受到属性bounces/alwaysBounceVertical/alwaysBounceHorizontal的影响,另外当bounces为NO时,其他两个属性也不管用了.当bounces为YES时,scrollView的contentSize小于scrollView的bounds时才会发生bouncing.
  • 只要调用了scrollViewDidEndDragging:willDecelerate:方法,且deceleration参数为YES时,在scrollView做减速滚动过程中delegate会继续调用scrollViewWillBeginDecelerating:,而且还会多次调用scrollViewDidScroll:,此时dragging/tracking为NO,decelerating为YES
  • 最终,减速滚动结束,delegate会调用scrollViewDidEndDecelerating:方法,且decelerating为NO.

注意:当scrollView进行缩放时,tracking/dragging的值可能一直为NO,zooming为YES,这种情况存在.


使用捏合手势进行简单缩放

UIScrollView的缩放非常容易实现,scrollView本身自带捏合手势(pinch gesture)进行缩放. 实现步骤是:

  • 设置缩放因子(zoom factors:对内容的缩放程度),设置minimumZoomScalemaximumZoomScale
  • 然后再实现一个delegate方法viewForZoomingInScrollView:

如何使用Pinch Gesture来进行缩放

  • 捏合手势是iOS应用的一个标准手势,分pinch-in/pinch-out两个动作,pinch-in进行缩小,pinch-out进行放大,如下图展示:


    iOS应用的标准捏合手指
  • 为了使scrollView支持缩放,开发者需要给scrollView设置一个delegate.该delegate需要实现UISCrollViewDelegate协议中的viewForZommingInScrollView:方法,在该方法中返回需要进行缩放的内容(contents,or subviews).

  • 在下面代码清单展示了如何使用viewForZommingInScrollView:delegate方法来指定需要缩放的内容,当进行捏合手势时,控制器的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)和实现delegate方法这两个步骤是使scrollView支持缩放的基本条件

代码控制缩放

  • 除了使用捏合手势进行控制缩放,还有其他条件可以进行scrollView的缩放,比如用户双击手势(double taps gestures)或者其他手势.所以UIScrollView还提供了下面两个接口供开发者控制缩放:

    • 方法setZoomScale:animated:
    • 方法zoomToRect:animated:
  • 使用setZoomScale:animated:给属性zoomScale赋值,该值范围是minimumZoomScale-maximumZoomScale. 如果animated = YES,那么缩放过程是一个动画,如果为NO,那么缩放效果是瞬间完成,直接给zoomScale赋值也是瞬间的,没有动画效果. 在缩放过程中,进行缩放的view的center不改变,也就是位置不变,只缩放.

  • 使用zoomToRect:animated:方法进行缩放时,会缩放以刚好充满指定的rect.该方法在进行缩放时会改变位置,参数animated的值对缩放的影响和setZoomScale:animted:一样.

  • 有时需要根据用户点击屏幕(tap gesture)来设置zoom scale和location, 开发者经常会遇到这样的需求.因为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结束时通知delegate

  • 当scrollView的缩放结束后,delegate会调用scrollViewDidEndZooming:withView:atScale:方法
  • 该方法调用时会传入scrollView,进行缩放的subview,缩放因子scaleFactor

在进行缩放时如何保持图像清晰

  • scrollView在缩放时,仅仅是对内容根据zoomScale进行大小缩放,这会导致图像不够清晰.因为缩放时,内容不会重绘.
  • 如果你希望内容在缩放时需要保持清晰,那么你可以去参考苹果提供的demo:ScrollViewSuit. ScrollViewSuit会使用一种提前渲染(pre-rendering)的技术来缩放内容,然后再单独展示它.
  • 如果你进行缩放的内容是实时绘制且缩放时还要保持清晰,那么需要用到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:的调用是发生在后台线程中的,所以里面的绘制要是core graphic.


通过点击来缩放

通过上面学习我们知道要进行缩放很简单,通过捏合手势等很容易实现.但有些场景的缩放需求比较复杂,比如双击缩放,我们需要对tap手势的探测来进行特定的缩放,比较典型的例子如地图的双击缩放效果.根据点击的手指数,点击次数,连续点击的速度,可以进行不同的缩放处理,所以这一效果比较复杂,需要重写UIView中touch的处理方法(touchesBegan..,touchesEnded..,touchesCanceled..)

重写UIView的Touch-Handing方法

为检测不同点击动作响应不同的缩放效果,所以需要重写touch-handing方法,这里可以参考ScrollViewSuit中的列子TapToZoom中的类TapDetectingImageView,它是UIImageView的子类.下面就开始讲者个类的实现

Initialization

  • 进行"点击缩放"的视图需要在使用initWithImage初始化时开启User Interaction(用户交互)和multiple touches(多次安触碰)
  • 变量twoFingerTapIsPossible用来记录触摸屏幕的手指数是否超过2(手指数>2:NO)
  • 变量multipleTouches用来记录touches event的数量是否超过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
  • 设置状态变量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

  • 主要实现都在这个方法,比较长
  • 函数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上的点击手势变成拖动手势时,调用此方法:

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    twoFingerTapIsPossible = YES;
    multipleTouches = NO;
}

ScrollViewSuite-苹果讲解ScrollView高级用法的示例代码

在ScrollViewSuite包含了很多scrollView的高级用法代码实例,比如TapToZoom可以用在地图应用,可以多去参考借鉴


scroll view的翻页

UIScrollView有种模式叫翻页模式(paging mode),指用户只能一屏一屏的滚动scrollView中的内容.经常用来展示一系列的内容,比如电纸书/指导页

如何设置翻页模式

  • scrollView的翻页模式需要使用代码设置
  • 初始化scrollView后需要将属性pagingMode设置为YES
  • 如果contentSize的height设置为屏幕的高度,那么width需要为屏幕宽度的整数倍
  • 使用UIPageControl替代UIScrollView自带的indicator
  • 下图显示展示了例子PageControl: Using a Paginated UIScrollView
    翻页模式

如何设置翻页的内容

有两种方式:

  1. 在同一个view中一次性全部加载完内容(适合内容较少的时候)
  2. 使用多个(最好3个)view来部分加载当前要显示的内容和将要显示的内容(适合内容较多,加载完全部内容需要更多时间),具体请看apple实例代码:PageControl:USing a Paginated UIScrollView

在使用多个view展示内容时:

  1. 使用3个页面来展示展示内容,第一页展示已经显示过得内容,第二页展示当前显示的内容,第三页显示将要显示的内容,这样既不浪费内存又不耽误显示
  2. 在controller初始化时,翻页的三个页面就要开始初始化,计算好三个页面的位置,准备滚动操作,三个页面要交替循环显示对应的内容.
  3. 需要实现delegate方法scrollViewDidScroll:,用来跟踪scrollView的contentOffset,判断何时超过scrollview的中间.根据手指滑动的方向判断要显示的页面(next/first page).然后重绘将要显示的内容
  4. 根据上面的策略可以显示大量的页面
  5. 如果页面的创建比较耗时,可以创建一个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


示例代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,969评论 3 119
  • 早晨是被一通父亲的电话打醒的。我们之间的关系向来不好,我觉得他不是一个好父亲他则觉得我不是一个争气的儿子。想想也是...
    人生若雾懵懂阅读 450评论 1 0
  • 文/王裕森每篇文章都是自我的一种感悟,带有强烈的自我色彩,揭露了一些客观真理,不喜勿读! 本来这个世界应该是命中注...
    大帜阅读 334评论 2 4
  • 1 超市的粽子山一样高,各式的粽子散发着节日的气息。站在热闹的粽子间,恍惚看到了熟悉的古旧深隧院落里,贤淑温和的奶...
    郑剑霓阅读 400评论 0 2
  • 儿子心心念念的跑步机今天安装到位,现在他拿着说明书仔细的看,在跑道上试跑,这么胖了本应该早点买,原来不买...
    姚亚君阅读 171评论 0 3