码迷,mamicode.com
首页 > 其他好文 > 详细

UIScrollView

时间:2015-04-09 00:44:06      阅读:248      评论:0      收藏:0      [点我收藏+]

标签:

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

UIScrollView

标签:

原文地址:http://www.cnblogs.com/heri/p/4404793.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!