我是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. web12 使用map型的request、session、application

    电影网站:www.aikan66.com 项目网站:www.aikan66.com 游戏网站:www.aikan66.com 图片网站:www.aikan66.com 书籍网站:www.aikan66 ...

  2. 关于Target=" "的一些属性

    总结目前所知的Target=""的几个属性,对其它博客进行整理和归纳. Target="_blank":浏览器总在一个新打开.未命名的窗口中载入目标文档. 说通 ...

  3. 利用session创建的cookies是这样的

    版权声明:本文为博主原创文章,未经博主允许不得转载.

  4. 剑指offer:旋转数组的最小数字

    题目描述: 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素. 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个 ...

  5. linux上传的命令

    pscp D:\apache-tomcat-8.0.38\webapps\GameDataServer.zip root@112.74.32.215:/usr/local/tools/tomcat/a ...

  6. c++中的函数重载

    函数多态也称为函数重载. (1)函数重载指的是可以有多个同名的函数,因此对名称进行了重载. (2)函数重载的关键在于函数的参数列表,也称为函数特征标.如果两个函数的参数数目和参数类型相同,同时参数的排 ...

  7. wait 和 sleep 区别

    /* wait 和 sleep 区别? 1,wait可以指定时间也可以不指定. sleep必须指定时间. 2,在同步中时,对cpu的执行权和锁的处理不同. wait:释放执行权,释放锁. sleep: ...

  8. 360Vedio To NFOV Vedio

    Deep 360 Pilot Learning a Deep Agent for Piloting through 360° Sports Videos 源码.数据集和视频演示 ego-centric ...

  9. Beta阶段DAY2

    一.提供当天站立式会议照片一张 二.每个人的工作 1.讨论项目每个成员的昨天进展 刘阳航:删除多余按钮,调整界面. 林庭亦:删除麻烦的颜色设置. 郑子熙:添加新增按钮. 陈文俊:重新规划面板及功能. ...

  10. Hive如何加载和导入HBase的数据

    当我们用HBase 存储实时数据的时候, 如果要做一些数据分析方面的操作, 就比较困难了, 要写MapReduce Job. Hive 主要是用来做数据分析的数据仓库,支持标准SQL 查询, 做数据分 ...