UIScrollView相关

iOS Scroll View 编程指导

2018-09-10  本文已影响102人  陵无山

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

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

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

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

一个使用`UIScrollView`的例子

预览

UIScrollView提供了下面的功能:

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

ScrollView的滚动

scrollView缩放

scrollView的翻页模式

scrollView的嵌套


创建并设置scroll view

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

创建Scroll Views

scroll view的创建和使用就是其他视图没啥两样,可以插入controller中或者其他view hierarchy中.另外需要再做两步设置来进行scroll 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的滚动不一定都是通过用户手势来控制,也可通过代码设置来进行特殊的滚动,比如:

滚动到特定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滚动到特定区域,以显示特定区域的内容,特别地,当要展示的内容是一个在屏幕显示区域外的控件时,这个功能比较有用.

滚动到顶部(scroll to top)

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

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滚动开始和结束的简单方式

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

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


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

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

如何使用Pinch Gesture来进行缩放

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return self.imageView;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.scrollView.minimumZoomScale=0.5;
    self.scrollView.maximumZoomScale=6.0;
    self.scrollView.contentSize=CGSizeMake(1280, 960);
    self.scrollView.delegate=self;
}

代码控制缩放

- (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

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

#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

请看下面的代码

- (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

- (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

- (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中的内容.经常用来展示一系列的内容,比如电纸书/指导页

如何设置翻页模式

如何设置翻页的内容

有两种方式:

  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


示例代码

上一篇 下一篇

猜你喜欢

热点阅读