美文网首页
深入理解 UIScrollView

深入理解 UIScrollView

作者: 死神一护 | 来源:发表于2019-07-31 11:39 被阅读0次

    前阵子在实现视差动画的时候,无意间看到了 Ole BegeMann 大神关于 UIScrollView 的文章,UnderStand UIScrollView,获益匪浅。不禁感叹如若当时初学 UIKit 时,就碰到这篇文章,对于新手来说,理解 bounds,contentSize,contentOffset 这些让人烦恼的属性一定简单很多。

    ​ 这篇文章简单易懂,对新手非常友好,遂决定对这篇文章进行翻译,给 Ole 大神发送了邮件👨‍💻,获取转发翻译授权。

    申请授权邮件

    下面的内容就是直接翻译自 Ole 大神的博客,如有翻译的不好的地方,请各位批评指正。🙏


    ​ 我是Mike Ash Let's Build 系列文章的忠实粉丝,在这个系列的文章中他通过从头开始创建某些框架或功能,从而解释这些 CoCoa 框架的工作原理。在这篇 Blog 中,我决定做一些和Mike Ash 类似的事情,通过一小段代码实现我的小小 scroll view。

    ​ 首先,让我们看一看 UIKit 中 coordinate systems 是怎样工作的。如果你只对 scroll view 的实现感兴趣的话,可以跳过下面这一段。

    Coordinate Systems

    ​ 每一个 view 定义了他自己的 coordinate system。如下图所示,X轴向右,Y轴向下。

    A UIView coordinate system.

    ​ 请注意,逻辑上所说的 coordinate system 并不关心他自己的宽和高。他是在四个方向上无限延伸的。(PS: 译者添加-也就是说在四个方向上可以无限给当前 View 添加 subView 来增添内容). 让我们在这个 coordinate system 中添加一些 subviews 来检验一下结果。下图中,每一个带颜色的块代表一个 subview:

    Adding subviews to the coordinate system.

    代码如下所示:

    UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 100, 100)];
    redView.backgroundColor = [UIColor colorWithRed:0.815 green:0.007
        blue:0.105 alpha:1];
    
    UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(150, 160, 150, 200)];
    greenView.backgroundColor = [UIColor colorWithRed:0.494 green:0.827
        blue:0.129 alpha:1];
    
    UIView *blueView = [[UIView alloc] initWithFrame:CGRectMake(40, 400, 200, 150)];
    blueView.backgroundColor = [UIColor colorWithRed:0.29 green:0.564
        blue:0.886 alpha:1];
    
    UIView *yellowView = [[UIView alloc] initWithFrame:CGRectMake(100, 600, 180, 150)];
    yellowView.backgroundColor = [UIColor colorWithRed:0.972 green:0.905
        blue:0.109 alpha:1];
    
    [mainView addSubview:redView];
    [mainView addSubview:greenView];
    [mainView addSubview:blueView];
    [mainView addSubview:yellowView];
    

    Bounds

    UIView 的官方文档中对于 bounds 这个属性解释如下:

    The bounds rectangle … describes the view’s location and size in its own coordinate system.

    一个 view 可以被当做是一个 window 窗口或者 viewport 视窗的矩形区域在他自己的 coordinate system定义的平面中。并且这个 view 的 bounds 表示这个矩形的位置和尺寸大小。

    ​ view 的 bounds 矩形的宽和高是320*480,origin 原点默认是 (0,0)。这个 view 就可以看成是一个在当前 coordinate system 平面中的视窗,用来展示整个平面的一小部分而已。在 bounds 矩形外面的部分仍旧在那里布局着,只不过我们看不到而已。

    A view provides a viewport into the plane defined by its coordinate system. The view’s bounds rectangle describe the position and size of the visible area.

    Frame

    ​ 接下来,我们改变 bounds 矩形的原点试试:

    CGRect bounds = mainView.bounds;
    bounds.origin = CGPointMake(0, 100);
    mainView.bounds = bounds;
    

    ​ bounds 矩形的原点变成了 (0,100),所以显示效果如下:

    `Modifying the origin of the bounds rectangle is equivalent to moving the viewport.`

    ​ 看起来视图向下移动了 100 个点,诚然,对于他自己的 coordinate system来说确实如此。这个视图在屏幕上的的真正位置(确切来说,或者是是在他的父视图上)仍然没有变,这个位置是由他的 frame 属性来决定的,frame 本身没有变:

    The frame rectangle … describes the view’s location and size in its superview’s coordinate system.

    由于这个视图的位置是固定的(从他自己的角度来说),把 coordinate system 平面看成是一片我们可以随意拖动的,透明的胶片,把 view 看成是一个固定的窗口,我们可以通过这个窗口看到下面胶片上的内容。改变 bounds’s 的原点,就相当于移动这个透明胶片,结果就是这个胶片上的其他内容从不可见,到可以通过这个视窗看到了:

    Modifying the origin of the bounds rectangle is equivalent to moving the coordinate system in the opposite direction while the view’s position remains fixed because its frame does not change.

    好了,这就是 UIScrollView 滑动时的真正原理。我们需要注意是,从用户的角度来看,好像是 view 的 subviews 在移动,其实这些 subviews 对于这个视图的的坐标系来说,没有改变(换句话说,这些 subviews 的 frame 没有变化)。

    Build UIScrollView

    一个 scroll view 不需要在滚动的时候频繁地更新他 subview的坐标。他只是更改了他自己的 bounds,仅此而已。明白了这个原理之后,实现一个简易的的 scroll view 就非常容易了。我们给 view 添加一个追踪用户 pan 手势的识别器,随时手势的滑动,转换并且更新 view 的 bounds就好:

    // CustomScrollView.h
    @import UIKit;
    
    @interface CustomScrollView : UIView
    
    @property (nonatomic) CGSize contentSize;
    
    @end
    
    // CustomScrollView.m
    #import "CustomScrollView.h"
    
    @implementation CustomScrollView
    
    - (id)initWithFrame:(CGRect)frame
    {
        self = [super initWithFrame:frame];
        if (self == nil) {
            return nil;
        }
        UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc]
            initWithTarget:self action:@selector(handlePanGesture:)];
        [self addGestureRecognizer:gestureRecognizer];
        return self;
    }
    
    - (void)handlePanGesture:(UIPanGestureRecognizer *)gestureRecognizer
    {
        CGPoint translation = [gestureRecognizer translationInView:self];
        CGRect bounds = self.bounds;
    
        // Translate the view's bounds, but do not permit values that would violate contentSize
        CGFloat newBoundsOriginX = bounds.origin.x - translation.x;
        CGFloat minBoundsOriginX = 0.0;
        CGFloat maxBoundsOriginX = self.contentSize.width - bounds.size.width;
        bounds.origin.x = fmax(minBoundsOriginX, fmin(newBoundsOriginX, maxBoundsOriginX));
    
        CGFloat newBoundsOriginY = bounds.origin.y - translation.y;
        CGFloat minBoundsOriginY = 0.0;
        CGFloat maxBoundsOriginY = self.contentSize.height - bounds.size.height;
        bounds.origin.y = fmax(minBoundsOriginY, fmin(newBoundsOriginY, maxBoundsOriginY));
    
        self.bounds = bounds;
        [gestureRecognizer setTranslation:CGPointZero inView:self];
    }
    
    @end
    

    就像UIKit 中真正的 UIScollView 一样,我们自己构建的类也有一个 contentSize 属性用来从外部设置来定义滑动范围。当我们改变 bounds 的时候,我们需要保证这个 bounds 是一个没有超出滑动范围的有效值。

    最终实现结果如下:

    Our custom scroll view in action. Note that it lacks momentum scrolling, bouncing, and scroll indicators.

    总结

    感谢 UIKit 中内置的 coordinate system,让我们用不到30行代码实现了 UIScrollView 的基本原理。当然,对于真正的 UIScrollView 来说,还有很多其他特性,比如 带有惯性的 scrolling,反弹特性,滑动指示标,放大缩小,还有那些我们没有实现某个功能的代理方法。

    2014年5月2日更新:整个实现代码在 available on GitHub

    2014年5月8日更新:查看进阶的一些文章follow-up post来实现类似惯性滑动,弹性,摩擦停止等等特性。

    为此,写了一个Demo,并且添加了惯性滑动,边界Bounce等特性,Github链接 AppleUIScrollView

    相关文章

      网友评论

          本文标题:深入理解 UIScrollView

          本文链接:https://www.haomeiwen.com/subject/snszrctx.html