zoukankan      html  css  js  c++  java
  • UIScrollView

    UIScrollView(包括它的子类 UITableView 和 UICollectionView)是 iOS 开发中最常用的 UI 组件,大部分 App 的核心界面都是基于三者之一或三者的组合实现。

    UIScrollView 是 UIKit 中为数不多能响应滑动手势的 view,相比自己用 UIPanGestureRecognizer 实现一些基于滑动手势的效果,用 UIScrollView 的优势在于 bouncedecelerate 等特性可以让 App 的用户体验与 iOS 系统的用户体验保持一致。

    本文通过一些实例讲解 UIScrollView 的特性和实际使用中的经验。

    UIScrollView 和 Auto Layout

    关于 Auto Layout 的基本用法参考 Ray Wenderlich 上的教程Part 2)。

    UIScrollView 在 Auto Layout 是一个很特殊的 view,对于 UIScrollView 的 subview 来说,它的 leading/trailing/top/bottom space 是相对于 UIScrollView 的 contentSize 而不是 bounds 来确定的,所以当你尝试用 UIScrollView 和它 subview 的 leading/trailing/top/bottom 来互相决定大小的时候,就会出现「Has ambiguous scrollable content width/height」的 warning。

    正确的是用 UIScrollView 外部的 view 或 UIScrollView 本身的 width/height 确定 subview 的尺寸,进而确定 contentSize。因为 UIScrollView 本身的 leading/trailing/top/bottom 变得不好用,所以我习惯的做法是在 UIScrollView 和它原来的 subviews 之间增加一个 content view,这样做的好处有:

    • 不会在 storyboard 里留下 error/warning

    • 为 subview 提供 leading/trailing/top/bottom,方便 subview 的布局

    • 通过调整 content view 的 size(可以是 constraint 的 IBOutlet)来调整 contentSize

    • 不需要 hard code 与屏幕尺寸相关的代码

    • 更好地支持 rotation

    Sample 中的 AutoLayout 演示了 UIScrollView + Auto Layout 的例子。

    UIScrollViewDelegate

    UIScrollViewDelegate 是 UIScrollView 的 delegate protocol,UIScrollView 有意思的功能都是通过它的 delegate 方法实现的。了解这些方法被触发的条件及调用的顺序对于使用 UIScrollView 是很有必要的,本文主要讲拖动相关的效果,所以 zoom 相关的方法跳过不提,拖动相关的 delegate 方法按调用顺序分别是:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView

    这个方法在任何方式触发 contentOffset 变化的时候都会被调用(包括用户拖动,减速过程,直接通过代码设置等),可以用于监控 contentOffset 的变化,并根据当前的 contentOffset 对其他 view 做出随动调整。

    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView

    用户开始拖动 scroll view 的时候被调用。

    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset

    该方法从 iOS 5 引入,在 didEndDragging 前被调用,当 willEndDragging 方法中 velocity 为 CGPointZero(结束拖动时两个方向都没有速度)时,didEndDragging 中的 decelerate 为 NO,即没有减速过程,willBeginDecelerating 和 didEndDecelerating 也就不会被调用。反之,当 velocity 不为 CGPointZero 时,scroll view 会以 velocity 为初速度,减速直到 targetContentOffset。值得注意的是,这里的 targetContentOffset 是个指针,没错,你可以改变减速运动的目的地,这在一些效果的实现时十分有用,实例中会具体提到它的用法,并和其他实现方式作比较。

    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

    在用户结束拖动后被调用,decelerate 为 YES 时,结束拖动后会有减速过程。注,在 didEndDragging 之后,如果有减速过程,scroll view 的 dragging 并不会立即置为 NO,而是要等到减速结束之后,所以这个 dragging 属性的实际语义更接近 scrolling。

     - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView

    减速动画开始前被调用。

    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

    减速动画结束时被调用,这里有一种特殊情况:当一次减速动画尚未结束的时候再次 drag scroll view,didEndDecelerating 不会被调用,并且这时 scroll view 的 dragging 和 decelerating 属性都是 YES。新的 dragging 如果有加速度,那么 willBeginDecelerating 会再一次被调用,然后才是 didEndDecelerating;如果没有加速度,虽然 willBeginDecelerating 不会被调用,但前一次留下的 didEndDecelerating 会被调用,所以连续快速滚动一个 scroll view 时,delegate 方法被调用的顺序(不含 didScroll)可能是这样的:

    scrollViewWillBeginDragging:  
    scrollViewWillEndDragging: withVelocity: targetContentOffset:  
    scrollViewDidEndDragging: willDecelerate:  
    scrollViewWillBeginDecelerating:  
    scrollViewWillBeginDragging:  
    scrollViewWillEndDragging: withVelocity: targetContentOffset:  
    scrollViewDidEndDragging: willDecelerate:  
    scrollViewWillBeginDecelerating:  
    ...
    scrollViewWillBeginDragging:  
    scrollViewWillEndDragging: withVelocity: targetContentOffset:  
    scrollViewDidEndDragging: willDecelerate:  
    scrollViewWillBeginDecelerating:  
    scrollViewDidEndDecelerating:

    虽然很少有因为这个导致的 bug,但是你需要知道这种很常见的用户操作会导致的中间状态。例如你尝试在 UITableViewDataSource 的 tableView:cellForRowAtIndexPath: 方法中基于 tableView 的 dragging 和 decelerating 属性判断是在用户拖拽还是减速过程中的话可能会误判(见例 1)。

    Sample 中的 Delegate 简单输出了一些 Log,你可以快速了解这些方法的调用顺序。

    实例

    下面通过一些实例,更详细地演示和描述以上各 delegate 方法的用途。

    1. Table View 中图片加载逻辑的优化

    虽然这种优化方式在现在的机能和网络环境下可能看似不那么必要,但在我最初看到这个方法是的 09 年(印象中是 Tweetie 作者在 08 年写的 Blog,可能有误),遥想 iPhone 3G/3GS 的机能,这个方法为多图的 table view 的性能带来很大的提升,也成了我的秘密武器。而现在,在移动网络环境下,你依然值得这么做来为用户节省流量。

    先说一下原文的思路:

    • 当用户手动 drag table view 的时候,会加载 cell 中的图片;

    • 在用户快速滑动的减速过程中,不加载过程中 cell 中的图片(但文字信息还是会被加载,只是减少减速过程中的网络开销和图片加载的开销);

    • 在减速结束后,加载所有可见 cell 的图片(如果需要的话);

    问题 1:

    前面提到,刚开始拖动的时候,dragging 为 YES,decelerating 为 NO;decelerate 过程中,dragging 和 decelerating 都为 YES;decelerate 未结束时开始下一次拖动,dragging 和 decelerating 依然都为 YES。所以无法简单通过 table view 的 dragging 和 decelerating 判断是在用户拖动还是减速过程。

    解决这个问题很简单,添加一个变量如 userDragging,在 willBeginDragging 中设为 YES,didEndDragging 中设为 NO。那么 tableView: cellForRowAtIndexPath: 方法中,是否 load 图片的逻辑就是:

    if (!self.userDragging && tableView.decelerating) {  
        cell.imageView.image = nil;
    else {
        // code for loading image from network or disk
    }

    问题 2:

    这么做的话,decelerate 结束后,屏幕上的 cell 都是不带图片的,解决这个问题也不难,你需要一个形如 loadImageForVisibleCells 的方法,加载可见 cell 的图片:

    - (void)loadImageForVisibleCells
    {
        NSArray *cells = [self.tableView visibleCells];
        for (GLImageCell *cell in cells) {
            NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
            [self setupCell:cell withIndexPath:indexPath];
        }
    }

    问题 3:

    这个问题可能不容易被发现,在减速过程中如果用户开始新的拖动,当前屏幕的 cell 并不会被加载(前文提到的调用顺序问题导致),而且问题 1 的方案并不能解决问题 3,因为这些 cell 已经在屏上,不会再次经过 cellForRowAtIndexPath 方法。虽然不容易发现,但解决很简单,只需要在 scrollViewWillBeginDragging: 方法里也调用一次 loadImageForVisibleCells 即可。

    再优化

    上述方法在那个年代的确提升了 table view 的 performance,但是你会发现在减速过程最后最慢的那零点几秒时间,其实还是会让人等得有些心急,尤其如果你的 App 只有图片没有文字。在 iOS 5 引入了 scrollViewWillEndDragging: withVelocity: targetContentOffset: 方法后,配合 SDWebImage,我尝试再优化了一下这个方法以提升用户体验:

    • 如果内存中有图片的缓存,减速过程中也会加载该图片

    • 如果图片属于 targetContentOffset 能看到的 cell,正常加载,这样一来,快速滚动的最后一屏出来的的过程中,用户就能看到目标区域的图片逐渐加载

    • 你可以尝试用类似 fade in 或者 flip 的效果缓解生硬的突然出现(尤其是像本例这样只有图片的 App)

    核心代码:

    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
    {
        self.targetRect = nil;
        [self loadImageForVisibleCells];
    }
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
    {
        CGRect targetRect = CGRectMake(targetContentOffset->x, targetContentOffset->y, scrollView.frame.size.width, scrollView.frame.size.height);
        self.targetRect = [NSValue valueWithCGRect:targetRect];
    }
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
    {
        self.targetRect = nil;
        [self loadImageForVisibleCells];
    }

    是否需要加载图片的逻辑:

    BOOL shouldLoadImage = YES;  
    if (self.targetRect && !CGRectIntersectsRect([self.targetRect CGRectValue], cellFrame)) {  
        SDImageCache *cache = [manager imageCache];
        NSString *key = [manager cacheKeyForURL:targetURL];
        if (![cache imageFromMemoryCacheForKey:key]) {
            shouldLoadImage = NO;
        }
    }
    if (shouldLoadImage) {  
        // load image
    }

    更值得高兴的是,通过判断是否 nil,targetRect 同时起到了原来 userDragging 的作用。本例完整的代码见 Sample 中的 LazyLoad

    参考:http://www.cocoachina.com/ios/20141216/10645.html

  • 相关阅读:
    [Sass学习]Sass的安装和使用
    [CSS学习] padding属性讲解
    [CSS学习] line-height属性讲解
    IOS学习之路——Swift语言(2)——基本类型与函数
    IOS学习之路——Swift语言(1)——基本类型、运算符与逻辑控制语句
    上海 day23 -- 面向对象三大特征---多态 和 内置魔法函数
    上海 day22 -- 面向对象三大特征---- 封装
    上海 day21 -- 面向对象三大特征----继承
    上海 day20 -- 面向对象基础
    上海 day18~19 ATM+购物车(待更新)
  • 原文地址:https://www.cnblogs.com/heri/p/4404793.html
Copyright © 2011-2022 走看看