我是Mike Ash的Let’s Build…系列文章的忠实粉丝,在这一系列文章中他从头设计Cocoa的控件来解释他们的工作原理。在这里我要做一点类似的事情,用几行代码来实现我自己的滚动试图。不过首先,让我们先来了解一下UIKit中的坐标系是怎么工作的。如果你只对滚动试图的代码实现感兴趣可以放心跳过下一小节。UIKit坐标系每一个View都定义了他自己的坐标系统。如下图所示,x轴指向右方,y轴指向下方:

注意这个逻辑坐标系并不关注包含在其中View的宽度和高度。整个坐标系没有边界向四周无限延伸.我们在坐标系中放置四个子View。每一次色块代表一个View:

添加View的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

Apple关于UIView的文档中是这样描述bounds属性的:

bounds矩形…描述了该视图在其自身坐标系中的位置和大小。

一个View可以被看作是定义在其所在坐标系平面上的一扇窗户或者说是一个矩形的可视区域。View的边界表明了这个矩形可视区域的位置和大小。

假设我们的View宽320像素,高480像素,原点在(0,0)。那么这个View就变成了整个坐标系平面的观察口,它展示的只是整个平面的一小部分。位于该View边界外的区域依然存在,只是被隐藏起来了。

一个View提供了其所在平面的一个观察口。View的bounds矩形描述了这个可是区域的位置和大小。

Frame

接下来我们来试着修改bounds的原点坐标:

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

当我们把bound原点设为(0,100)后,整个画面看起来就像这样:

修改bounds的原点就相当与在平面上移动这个可视区域。

看起来好像是这个View向下移动了100像素,在这个View自己的坐标系中这确实没错。不过这个View真正位于屏幕上的位置(更准确的说在其父View上的位置)其实没有改变,因为这是由View的frame属性决定的,它并没有改变:

frame矩形…定义了这个View在其父View坐标系中的位置和大小。

由于View的位置是相对固定的,你可以把整个坐标平面想象成我们可以上下拖动的透明幕布,把这个View想象成我们观察坐标平面的窗口。调整View的Bounds属性就相当于拖动这个幕布,那么下方的内容就能在我们View中被观察到:

Since the view’s position is fixed (from its own perspective), think of the coordinate system plane as a piece of transparent film we can drag around, and of the view as a fixed window we are looking through. Adjusting the bounds’s origin is equivalent to moving the transparent film such that another part of it becomes visible through the view:

修改bounds的原点坐标也相当于把整个坐标系向上拖动,因为View的frame没由变过,所以它相对于父View的位置没有变化过。

其实这就是UIScrollView滑动时所发生的事情。注意从一个用户的角度来看,他以为时这个View中的子View在移动,其实他们的在坐标系中位置(他们的frame)没有发生过变化。

打造你的UIScrollView

一个scroll view并不需要其中子View的坐标来使他们滚动。唯一要做的就是改变他的bounds属性。知道了这一点,实现一个简单的scroll view就没什么困难了。我们用一个gesture recognizer来识别用户的拖动操作,根据用户拖动的偏移量来改变bounds的原点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 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

和真正的UIScrollView一样,我们的类也有一个contentSize属性,你必须从外部来设置这个值来指定可以滚动的区域,当我们改变bounds的大小时我们要确保设置的值是有效的。

结果:

我们的scroll view已经能够工作了,不过还缺少动量滚动,反弹效果还有滚动提示符。

总结

感谢UIKit的坐标系统特性,使我们之花了30几行代码就能重现UIScrollView的精华,当然真正的UIScrollView要比我们所做的复杂的多,反弹效果,动量滚动,放大试图,还有代理方法,这些特性我们没有在这里涉及到。

更新 5/ 2, 2014: 本文的代码在https://github.com/ole/CustomScrollView。

更新 5/ 8, 2014:

1.坐标系并非无限延伸的。坐标系的范围由CGFloat的长度来决定,根据32位和64位系统有所不同,通常来讲这是一个很大的值。

2.事实上,除非你设置clipToBounds == YES,所有子View超出的部分其实仍然是可见的。只是View不会再去检测超出部分的触摸事件而已。

关于作者: 袁欣

 

UIScrollView原理的更多相关文章

  1. iOS开发-UIScrollView原理

    UIScrollView在开发中是不可避免,关于UIScrollView都有自己一定的理解.滚动视图有两个需要理解的属性,frame和bounds,frame是定义了视图在窗口的大小和位置,bound ...

  2. UIScrollView 原理详解

    转载此文章原因:web页面在ipad的app中总是有橡皮筋效果,使用iscroll虽然能解决橡皮筋想过,但是滚动层内的元素事件都无法触发.故同安卓和ios一样使用后台解决...红色的为解决方案.. S ...

  3. UIScrollView的属性总结

    contentSize是scrollview可以滚动的区域,比如frame = (0 ,0 ,320 ,480) contentSize = (320 ,960),代表你的scrollview可以上下 ...

  4. 第二、UIScrollView的使用大全

    UIScrollView UIPageControl 的使用 2011-11-19 16:48 4690人阅读 评论(0) 收藏 举报 imagescrollspringiphone // //    ...

  5. UIScrollView 的基本用法

    转自:http://unmi.cc/use-uiscrollview/ iPhone/iPad 中 UIScrollView 还是经常要用到的,这里作了一个使用它最简单的例子,一个 ScrollVie ...

  6. iOS基本UI控件总结

    包括以下几类: //继承自NSObject:(暂列为控件) UIColor *_color;    //颜色 UIImage *_image;    //图像 //继承自UIView:只能相应手势UI ...

  7. UIScrollView的缩放原理

    当用户在UIScrollView身上使用捏合手势时,UIScrollView会给代理发送一条消息,询问代理究竟要缩放自己内部的哪一个子控件(哪一块内容) 当用户在UIScrollView身上使用捏合手 ...

  8. iOS 下拉刷新-上拉加载原理

    前言 讲下拉刷新及上拉加载之前先给大家解释UIScrollView的几个属性 contentSize是UIScrollView可以滚动的区域. contentOfinset 苹果官方文档的解释是&qu ...

  9. UIScrollView的delaysContentTouches与canCencelContentTouches属性

    UIScrollView有一个BOOL类型的tracking属性,用来返回用户是否已经触及内容并打算开始滚动,我们从这个属性开始探究UIScrollView的工作原理: 当手指触摸到UIScrollV ...

随机推荐

  1. 预备作业02 : 体会做中学(Learning By Doing)

    1.你有什么技能比大多人(超过班级90%以上)更好? 我认为我是一个比较爱摄影和绘画的人,虽然说说不上技术精湛,但还是能拿出手的. 2.针对这个技能的获取你有什么成功的经验? 接触摄影和绘画都是因为喜 ...

  2. 20162325 金立清 S2 W3 C13

    20162325 2017-2018-2 <程序设计与数据结构>第3周学习总结 教材学习内容概要 查找是在一组项内找到指定目标或是确定目标不存在的过程 高效的查找使得比较的次数最少 Com ...

  3. C++:内存分区

    前言:最近正在学习有关static的知识,发觉对C++的内存分区不是很了解,上网查了很多资料,遂将这几天的学习笔记进行了简单整理,发表在这里 • 栈区(stack):主要用来存放函数的参数以及局部变量 ...

  4. 第二阶段Sprint冲刺会议3

     进展:讨论视频录制的具体功能,查看有关资料,开始着手编写有关代码.

  5. 编程之法section II: 2.2 和为定值的两个数

    ====数组篇==== 2.2 求和为定值的两个数: 题目描述:有n个整数,找出其中满足两数相加为target的两个数(如果有多组满足,只需要找出其中一组),要求时间复杂度尽可能低. 解法一: 思路: ...

  6. 如何解决abd.exe已停止工作

     打开电脑,右键点击属性会出现如下界面: 点击左边高级系统设置:将会出现如下界面: 点击环境变量,点编辑. 把环境变量中的 ANDROID_ADB_SERVER_PORT 改成1122以后还遇到这个问 ...

  7. Java 多生产者消费者问题

    /* 生产者,消费者.   多生产者,多消费者的问题. if判断标记,只有一次,会导致不该运行的线程运行了.出现了数据错误的情况. while判断标记,解决了线程获取执行权后,是否要运行!   not ...

  8. /etc/tolmcat/Server.xml 实例说明

      # 这是service类 <Service name="Catalina">   # 这是http连接器,响应用户请求 <Connector port=&qu ...

  9. js学习1

    js基础1: js组成: ECMAScript :解释器 .翻译 提供语言的基本功能 几乎没有兼容型问题 dom :document object model 有一些兼容型问题 bom :brower ...

  10. net user 修改密码的坑

    不多说 直接上图 自己偷懒修改 admin的密码.. 结果没注意 这个地方 能够输入全角字符. 造成密码 实质上是全角的 标点符号 ... 以后一定注意一些. 里面的坑..说多了 都是浪费时间 另外 ...