【IOS开发基础系列】UIScrollView专题

2023-10-16 11:41:23 浏览数 (3)

1 UIScrollView原理

       在滚动过程当中,其实是在修改原点坐标。当手指触摸后,scroll view会暂时拦截触摸事件,使用一个计时器。假如在计时器到点后没有发生手指移动事件,那么scroll view 发送 tracking events 到被点击的subview。假如在计时器到点前发生了移动事件,那么 scroll view 取消tracking 自己发生滚动。

    子类可以重载 

touchesShouldBegin:withEvent:inContentView:

    决定自己是否接收 touch 事件

pagingEnabled:    

    当值是 YES 会自动滚动到 subview 的边界,默认是NO

touchesShouldCancelInContentView:    

    开始发送 tracking messages 消息给 subview 的时候调用这个方法,决定是否发送 tracking messages 消息到subview。假如返回 NO,发送。YES 则不发送。假如 canCancelContentTouches属性是NO,则不调用这个方法来影响如何处理滚动手势。

            scroll view 还处理缩放和平移手势,要实现缩放和平移,必须实现委托 viewForZoomingInScrollView:和scrollViewDidEndZooming:withView:atScale: 两个方法。另外 maximumZoomScale和minimumZoomScale 两个属性要不一样。

1.1 核心原理

        UIScrollView的核心理念是,它是一个可以在内容视图之上,调整自己原点位置的视图。它根据自身框架的大小,剪切视图中的内容,通常框架是和应用程序窗口一样大。一个滚动的视图可以根据手指的移动,调整原点的位置。展示内容的视图,根据滚动视图的原点位置,开始绘制视图的内容,这个原点位置就是滚动视图的偏移量。ScrollView本身不能绘制,除非显示水平和竖直的指示器。滚动视图必须知道内容视图的大小,以便于知道什么时候停止;一般而言,当滚动出内容的边界时,它就返回了。

        某些对象是用来管理内容显示如何绘制的,这些对象应该是管理如何平铺显示内容的子视图,以便于没有子视图可以超过屏幕的尺寸。就是当用户滚动时,这些对象应该恰当的增加或者移除子视图。

         因为滚动视图没有滚动条,它必须知道一个触摸信号是打算滚动还是打算跟踪里面的子视图。为了达到这个目的,它临时中断了一个touch-down的事件,通过建立一个定时器,在定时器开始行动之前,看是否触摸的手指做了任何的移动。假如定时器行动时,没有任何的大的位置改变,滚动视图就发送一个跟踪事件给触摸的子视图。如果在定时器消失前,用户拖动他们的手指足够的远,滚动视图取消子视图的任何跟踪事件,滚动它自己。子类可以重载touchesShouldBegin:withEvent:inContentView:,  pagingEnabled 和touchesShouldCancelInContentView:方法,从而影响滚动视图的滚动手势。

        一个滚动视图也可以控制一个视图的缩放和平铺。当用户做捏合手势时,滚动视图调整偏移量和视图的比例。当手势结束的时候,管理视图内容显示的对象,就应该恰当的升级子视图的显示。当手势在处理的过程中,滚动视图不能够给子视图,发送任何跟踪的调用。

1.2 事件处理

        UIScrollView类有一个delegate,需要适配的协议是UIScrollViewDelegate。为了缩放和平铺工作,代理必须实现viewForZoomingInScrollView:和scrollViewDidEndZooming:withView:atScale:方法。另外,最大和最小缩放比例应该是不同的。

        重要的提示:在UIScrollView对象中,你不应该嵌入任何UIWebView和UITableView。假如这样做,会出现一些异常情况,因为2个对象的触摸事件可能被混合,从而错误的处理。

        这些都是官方API的解释,重点是理解UIScrollView怎么来控制手势的。可以由canCancelContentTouches这个方法的运用来解释UIScrollView如何控制手势的。

         假如你设置canCancelContentTouches为YES,那么当你在UIScrollView上面放置任何子视图的时候,当你在子视图上移动手指的时候,UIScrollView会给子视图发送touchCancel的消息。而如果该属性设置为NO,ScrollView本身不处理这个消息,全部交给子视图处理。

        那么这里就有疑问了,既然该属性设置未来NO了,那么岂不是UIScrollView不能处理任何事件了,那么为何在子视图上快速滚动的时候,UIScrollView还能移动那。这个一定要区分前面所说的UIScrollView中断touch-Down事件,开启一个定时器。我们设置的这个cancancelContentTouches属性为NO时,只是让UIScrollView不能发送cancel事件给子视图。而前面所说的时,中断touch-down事件,和取消touch事件是俩码事,所以当快速在子视图上移动的时候,当然可以滚动。但是如果你慢速的移动的话,就可以区分这个属性了,假如设定为YES,在子视图上慢速移动也可以滚动视图,但是如果为NO 。因为UIScrollView,发送了cancel事件给子视图处理了,自己当然滚动不了了。

        首先了解下UIScrollView对于touch事件的接收处理原理:UIScrollView应该是重载了hitTest 方法,并总会返回itself 。所以所有的touch 事件都会进入到它自己里面去了。内部的touch事件检测到这个事件是不是和自己相关的,或者处理或者除递给内部的view。

为了检测touch是处理还是传递,UIScrollView当touch发生时会生成一个timer。   

     (1)如果150ms内touch未产生移动,它就把这个事件传递给内部view

    (2)如果150ms内touch产生移动,开始scrolling,不会传递给内部的view。(如当你touch一个table时候,直接scrolling,你touch的那行永远不会highlight。)

    (3)如果150ms内touch未产生移动并且UIScrollView开始传递内部的view事件,但是移动足够远的话,且canCancelContentTouches = YES,UIScrollView会调用touchesCancelled方法,cancel掉内部view的事件响应,并开始scrolling。(如当你touch一个table, 停止了一会,然后开始scrolling,那一行就首先被highlight,但是随后就不在高亮了)

        在滚动过程当中,其实是在修改原点坐标。当手指触摸后, scroll view会暂时拦截触摸事件,使用一个计时器。假如在计时器到点后没有发生手指移动事件,那么 scroll view 发送 tracking events 到被点击的 subview。假如在计时器到点前发生了移动事件,那么 scroll view 取消 tracking 自己发生滚动。

1.3 内存重用

        事件处理看过了,就要考虑scrollView如何重用内存的,下面写了一个例子模仿UITableView的重用的思想,这里只是模仿,至于苹果公司怎么实现这种重用的,他们应该有更好的方法。

     这里的例子是在scrollView上放置4个2排2列的视图,但是内存中只占用6个视图的内存空间。当scrollView滚动的时候,通过不停的重用之前视图的内存空间,从而达到节省内存的效果。重用的方法如下:

    1.如果scrollView向下面滚动,一旦一排视图滚出了可视范围,就改变滚动出去的那个view在scrollView中的frame,也就是改变位置到达末尾,达到重用的效果。

    2.如果scrollView向上面滚动,一旦最末排的视图view滚出了可视范围,就改变滚动出去的那个view在scrollView中的frame,移动到最前面。

       下面就需要在你创建的视图控制器中,创建一个重用的视图数组,用来把这些要显示的视图放入内存中,这里虽然界面上显示的是2排2列的四个视图,但是当拖动的时候,可能出现前面一排的视图显示一部分,末尾一排的视图显示一部分的情况,所以重用的数组中要放置6个视图。下面是定义的一些宏:

#define sMyViewTotal 6

#define sMyViewWidth 150

#define sMyViewHeight 220

#define sMyViewGap 10

具体实现代码如下:

_aryViews = [[NSMutableArray alloc] init];

for (int i = 0; i < sMyViewTotal; i ) {

    CGFloat x;

    if (i%2) {

        x = sMyViewWidth   sMyViewGap   sMyViewGap/2;

    }

    else{

    x = sMyViewGap/2;

    }

    CGFloat y = (sMyViewHeight   sMyViewGap)*(i/2);

    MyView *myView = [[MyView alloc] initWithFrame: CGRectMake(x, y, sMyViewWidth, sMyViewHeight)];

    myView.showNumber = i;

    [myScrollView addSubview: myView];

    [_aryViews addObject: myView];

    [myView release];

}

所以这里的核心方法是,首先要判断是向上滚动还是向下滚动方法如下:

- (void) scrollViewDidScroll: (UIScrollView *)scrollView{

    BOOL directDown;

    if (previousOffSet.y < scrollView.contentOffset.y) {

        directDown = YES;

    }

    else{

        directDown = NO;

    }

    previousOffSet.y = scrollView.contentOffset.y;

    //防止最开始就向上面拖动的时候,改变数组视图树的位置。

    if (scrollView.contentOffset.y < 0) {

        return;

    }

    if (directDown) {

        NSLog(@"down");

        MyView * subView = [_aryViews objectAtIndex: firstViewIndex];

        CGFloat firstViewYOffset = subView.frame.origin.y   subView.frame.size.height   sMyViewGap;

        //寻找第一个视图是否滚动出去

        if (firstViewYOffset < scrollView.contentOffset.y) {

            //改变数组中第一排可见视图的位置。

            [self moveIndexInViewsWithDirect: YES];

        }

    }

    else{

        NSLog(@"up");

        MyView * subView = [_aryViews objectAtIndex: (firstViewIndex  sMyViewTotal - 2) % sMyViewTotal];

        CGFloat lastViewYOffset = subView.frame.origin.y - scrollView.bounds.size.height;

        if (lastViewYOffset > scrollView.contentOffset.y) {

            [self moveIndexInViewsWithDirect: NO];

        }

    }

}

        每次滚动的时候先判断滚动位置即offset,和先前的比较。如果先前的大就是向下滚动,否则就是向上滚动。

        找到了向下滚动了,就该判断是否子视图已经离开了可视范围。方法就是判断当前offset和视图的位置进行比较。如果判断滚到离开了可视范围,然后就是要改变重用视图数组中第一个视图的位置了。这里用了firstViewIndex来记录scrollView中第一个可见视图的位置, 循环使用这6个视图达到重用的目的。自然firstViewIndex上面的一个视图就是最后一个视图的位置(firstViewIndex   sMyViewTotal - 1) %sMyViewTotal。所以这里需要改变重用视图中firstViewIndex即第一个可见视图的位置。代码如下:

- (void) moveIndexInViewsWithDirect: (BOOL)forward{

    [UIView setAnimationsEnabled: NO];

    if (forward) {

        for (int i = firstViewIndex; i < (firstViewIndex   2); i ) {

            MyView *subView = [_aryViews objectAtIndex: i % sMyViewTotal];

            subView.showNumber = subView.showNumber   sMyViewTotal;

            subView.frame = CGRectMake(subView.frame.origin.x, subView.frame.origin.y   (sMyViewTotal/2) * (sMyViewHeight   sMyViewGap), subView.frame.size.width, subView.frame.size.height);

        }

        firstViewIndex = (firstViewIndex   2) % sMyViewTotal;

    }

    else{

        int lastViewIndex = firstViewIndex   sMyViewTotal - 1;

        for (int i = lastViewIndex; i > (lastViewIndex - 2); i-) {            

            MyView *subView = [_aryViews objectAtIndex:(firstViewIndex  sMyViewTotal - i) % sMyViewTotal];

           subView.showNumber = subView.showNumber - sMyViewTotal;

            subView.frame = CGRectMake(subView.frame.origin.x, subView.frame.origin.y - (sMyViewTotal/2) * (sMyViewHeight   sMyViewGap), subView.frame.size.width, subView.frame.size.height);

        }

        firstViewIndex = (firstViewIndex   sMyViewTotal - 2) % sMyViewTotal;

    }

    [UIView setAnimationsEnabled: YES];

}

      这里创建的子视图数字属性,是用来在视图上画数字的,这样就可以看到视图重用的效果了,应该是从0开始到无穷多,但是实际上内存中就创建了6个视图。

- (void)drawRect:(CGRect)rect

{

    // Drawing code

    NSString *text = [NSString stringWithFormat:@"%d",showNumber];

    [[UIColor redColor] set];

    [text drawInRect: CGRectMake(rect.origin.x, rect.origin.y   rect.size.height/2 - 30, rect.size.width, 30) withFont:[UIFont      fontWithName: @"Helvetica" size:20] lineBreakMode: UILineBreakModeWordWrap alignment: UITextAlignmentCenter];

}

2 开发运用

2.1 重要属性

2.1.1 Contentsize与contentInset

        contentsize是内容的宽和高,contentsize.width是内容的宽度,contentsize.heght是高度,contentsize是UIScrollView的一个属性,它是一个CGSize,是由核心图形所定义的架构,那定义了你可以滚轴内容的宽度和高度,你也可以添加可以上下滚动的额外区域。第一种方法是你可以通过添加内容的大小来完成。另外一个比较动态的选择是UIScrollView的另一个属性contentInset,contentInset增加你在contentsize中指定的内容能够滚动的上下左右区域数量contentInset.top以及contentInset.buttom分别表示上面和下面的距离。

        在滚轴视图中,有一个叫做ContentOffset的属性跟踪UIScrollView的具体位置,你能够自己获取和设置它,ContentOffset是你当前可视内容在滚轴视图边界的左上角那个点。如图:

        可以看出,ContentOffset内容中的那个点不是从contentInset的左上角开始的,而是内容的左上角,此时的ContentOffset是正值,但有时也是负值,如下图所示:

2.1.2 API介绍

touchesShouldBegin:withEvent:inContentView: 

    决定自己是否接收 touch 事件

pagingEnabled:

    当值是 YES 会自动滚动到 subview 的边界,默认是NO

touchesShouldCancelInContentView: 

    开始发送 tracking messages 消息给 subview 的时候调用这个方法,决定是否发送 tracking messages 消息到subview。假如返回 NO,发送。YES 则不发送。假如 canCancelContentTouches属性是NO,则不调用这个方法来影响如何处理滚动手势。

        scroll view 还处理缩放和平移手势,要实现缩放和平移,必须实现委托 viewForZoomingInScrollView:和scrollViewDidEndZooming:withView:atScale:两个方法。另外 maximumZoomScale和minimumZoomScale 两个属性要不一样。

    几个属性介绍:

tracking

    当 touch 后还没有拖动的时候值是YES,否则NO

zoomBouncing

    当内容放大到最大或者最小的时候值是 YES,否则NO

zooming

    当正在缩放的时候值是 YES,否则NO

decelerating

    当滚动后,手指放开但是还在继续滚动中。这个时候是 YES,其它时候是NO

decelerationRate

    设置手指放开后的减速率

maximumZoomScale

    一个浮点数,表示能放最大的倍数

minimumZoomScale

    一个浮点数,表示能缩最小的倍数

pagingEnabled

    当值是 YES 会自动滚动到 subview 的边界。默认是NO

scrollEnabled

    决定是否可以滚动

delaysContentTouches

    是个布尔值,当值是 YES 的时候,用户触碰开始,scroll view要延迟一会,看看是否用户有意图滚动。假如滚动了,那么捕捉 touch-down 事件,否则就不捕捉。假如值是NO,当用户触碰, scroll view 会立即触发touchesShouldBegin:withEvent:inContentView:,默认是YES

canCancelContentTouches

    当值是 YES 的时候,用户触碰后,然后在一定时间内没有移动,scrollView 发送 tracking events,然后用户移动手指足够长度触发滚动事件,这个时候,scrollView 发送了 touchesCancelled:withEvent: 到 subview,然后 scroView 开始滚动。假如值是 NO,scrollView 发送 tracking events 后,就算用户移动手指,scrollView 也不会滚动。

contentSize

    里面内容的大小,也就是可以滚动的大小,默认是0,没有滚动效果。

showsHorizontalScrollIndicator

    滚动时是否显示水平滚动条

showsVerticalScrollIndicator

    滚动时是否显示垂直滚动条

bounces

    默认是 yes,就是滚动超过边界会反弹有反弹回来的效果。假如是 NO,那么滚动到达边界会立刻停止。

bouncesZoom

    和 bounces 类似,区别在于:这个效果反映在缩放上面,假如缩放超过最大缩放,那么会反弹效果;假如是 NO,则到达最大或者最小的时候立即停止。

directionalLockEnabled

    默认是 NO,可以在垂直和水平方向同时运动。当值是 YES 时,假如一开始是垂直或者是水平运动,那么接下来会锁定另外一个方向的滚动。 假如一开始是对角方向滚动,则不会禁止某个方向

indicatorStyle

    滚动条的样式,基本只是设置颜色。总共3个颜色:默认、黑、白

scrollIndicatorInsets

    设置滚动条的位置

2.2 具体使用范例

使用一个ScrollView

// 创建一个UIScrollView

CGRectframe = CGRectMake( 0, 0, 200, 200);

scrollView = [[UIScrollView alloc] initWithFrame: frame];

// 添加子视图(框架可以超过scroll view的边界)

frame = CGRectMake( 0, 0, 500, 500);

myImageView = [[UIImageView alloc] initWithFrame: frame];

[scrollView addSubview: myImageView];

// 设置内容尺寸

scrollView.contentSize = CGSize(500,500);

2.3 扩展ScrollView的行为

        应用程序通常需要知道有关的滚图的事件:

    scrolloffset改变的时候

    拖动开始和结束

    减速的开始和结束

2.3.1 通过子类化扩展ScrollView的行为

        创建一个子类

        重写一些功能并改变行为

        关于这种方式的争议

        应用程序的逻辑和行为变成了视图本身的一部分,就像,你可能有一些定制的滚轴逻辑,,在那你只在意一个视图控制,但你想在不同地方重复使用你的滚轴视图,如果你必须为每个都子类化,你最后会有很多不同的滚轴视图子类以及在视图中的特定应用逻辑。

        编写很多子类是很沉闷的事情,你最后会有很多无法重复使用的单独视图,而MVC的视图部分的一个重点是视图是可以在不同的控制器和不同的模式之中重复使用的,如果我们把所有逻辑都放在视图中,它减少了可复用性。

       你的代码变得很牢固地配对在一起,它实际上变成了超类的一部分,你无法从UIScrollView中析取它,之后用其它东西代替,如果它在你控制器中且为控制器的一部分,在之后更容易改变它工作的方式和重新安排你应用程序的一些部分。

2.3.2 通过委托来扩展ScrollView的行为(常用的)

        委托是一个单独的对象,协议,定义了委托会实现的一系列功能的Objective-C协议,它创建了一系列很清晰的撤销点,在那里你能定制行为和外观。它在这些对象之间保持了松散的配对,视图本身与视图控制器或任何其它的控制器对象,委托不是滚轴视图的直接子类,它比起牢固配对的子类更加的松散。

2.4 开发技巧

2.4.1 计算当前页面数

- (void) scrollViewDidEndDecelerating: (UIScrollView*)scrollView {

    // 得到每页宽度

    CGFloat pageWidth = CGRectGetWidth(self.paggingScrollView.frame);

    // 根据当前的x坐标和页宽度计算出当前页数

    //此属性变化会调用Set方法

    self.currentPage = floor((scrollView.contentOffset.x - pageWidth / 2) / pageWidth) 1;

}

- (void) setCurrentPage: (NSInteger)currentPage {

    if (_currentPage==currentPage)

        return;

    _lastPage = _currentPage;

    _currentPage= currentPage;

    self.paggingNavbar.currentPage= currentPage;

    [self setupScrollToTop];

    [self callBackChangedPage];

}

2.4.2  指定Cell大小与间距
2.4.3 支持点击状态栏回到页面顶部

        scrollsToTop是UIScrollView的一个属性,主要用于点击设备的状态栏时,是scrollsToTop == YES的控件滚动返回至顶部。

        每一个默认的UIScrollView的实例,他的scrollsToTop属性默认为YES,所以要实现某一UIScrollView的实例点击设备状态栏返回顶部,则需要关闭其他的UIScrollView的实例的scrollsToTop属性为NO。很好理解:若多个scrollView响应返回顶部的事件,系统就不知道到底要将那个scrollView返回顶部了,因此也就不做任何操作了。

3 参考资料

Scroll View Programming Guide for iOS笔记

http://blog.sina.com.cn/s/blog_67419c420100phyf.html

第二、UIScrollView的使用大全

http://blog.csdn.net/ch_soft/article/details/6947695

[置顶] UIScrollView用法

http://blog.csdn.net/mengtnt/article/details/6723245

UIScrollView原理详解

http://blog.csdn.net/likendsl/article/details/7592867

TwitterPaggingViewer——类似Twitter,将滑动视图的UIPageControl(就是记录当前页面的一串小点)放到导航栏

http://code4app.com/ios/TwitterPaggingViewer/53a7ed4a933bf0794c8b48f9

UICollectionViewLayout

http://blog.csdn.net/majiakun1/article/details/17204921

ios开发——解决UICollectionView的cell间距与设置不符问题

http://www.bkjia.com/IOSjc/917782.html

IOS中scrollsToTop问题小结

http://blog.csdn.net/enuola/article/details/32331933

ios重写Cell后tabelView不能响应点击状态栏回到到顶部

http://www.cocoachina.com/bbs/read.php?tid-248386.html

深入理解iOS开发中的UIScrollView

http://mobile.51cto.com/hot-443341.htm

IOS学习笔记——iOS组件之UIScrollView详解

https://segmentfault.com/a/1190000002412930

IOS开发UI篇—UIScrollView控件介绍

http://www.cnblogs.com/wendingding/p/3754210.html

0 人点赞