UIView的剖析!

前面说过UIViewController,但是UIView也是在MVC中非常重要的一层。正是因为UIView是Iphone下所有界面的基础,所以官方专门写了一个文档“ViewProgrammingGuideforiOS”。通过这个可以很好的了解UIView的功能。

先来看看官方API的解释:TheUIViewclassdefinesarectangularareaonthescreen

andtheinterfacesformanagingthecontentinthatarea.

Atruntime,aviewobjecthandlestherenderingofanycontentinitsarea

andalsohandlesanyinteractionswiththatcontent.(UIView在屏幕上定义了一个矩形区域和管理区域内容的接口。在运行时,一个视图对象控制该区域的渲染,同时也控制内容的交互。)。所以说UIView具有三个基本的功能,画图和动画,管理内容的布局,控制事件。正是因为UIView具有这些功能,它才能担当起MVC中视图层的作用。

UIView咋看起来很复杂,官方API中各种函数接口,要学过运用庖丁解牛的思想,逐个分析,因为再复杂的东西都是有简单的东西构成的。回到刚才提到的UIView的三个基本功能就可以容易的分离出UIView不同的功能是怎么组合起来的。首先看视图最基本的功能显示和动画,其实UIView的所有的绘图和动画的接口,都是可以用CALayer和CAAnimation实现的,也就是说苹果公司是不是把CoreAnimation的功能封装到了UIView中,这个文档中没有提到过,也没法断言。但是每一个UIView都会包含一个CALayer,并且CALayer里面可以加入各种动画。再次我们来看UIView管理布局的思想其实和CALayer也是非常的接近的。最后控制事件的功能,是因为UIView继承了UIResponder。经过上面的分析很容易就可以分解出UIView的本质。UIView就相当于一块白墙,这块白墙只是负责把加入到里面的东西显示出来而已。

图1

1.UIView中的CALayer

UIView的一些几何特性frame,bounds,center都可以在CALayer中找到替代的属性,所以如果明白了CALayer的特点,自然UIView的图层中如何显示的都会一目了然。

CALayer就是图层,图层的功能自然就有渲染图片,播放动画的功能。每当创建一个UIView的时候,系统会自动的创建一个CALayer,但是这个CALayer对象你不能改变,只能修改某些属性。所以通过修改CALayer,不仅可以修饰UIView的外观,还可以给UIView添加各种动画。CALayer属于CoreAnimation框架中的类,通过CoreAnimationProgrammingGuide就可以了解很多CALayer中的特点,假如掌握了这些特点,自然也就理解了UIView是如何显示和渲染的。

先来看下CoreAnimation框架中关于layer的解释:WhilethereareobvioussimilaritiesbetweenCoreAnimationlayersandCocoaviewsthebiggest

conceptualdivergenceisthatlayersdonotrenderdirectlytothescreen.

WhereNSViewandUIViewareclearlyviewobjectsinthemodel-view-controllerdesignpattern,

CoreAnimationlayersareactuallymodelobjects.Theyencapsulategeometry,timingandvisualproperties,

andtheyprovidethecontentthatisdisplayed,

buttheactualdisplayisnotthelayer’sresponsibility.

Eachvisiblelayertreeisbackedbytwocorrespondingtrees:apresentationtreeandarendtree(非常相似的cocoa视图和coreAnimation层最大的区别是coreAnimation不能直接渲染到屏幕上。UIView和NSView明显是MVC中的视图模型,animationlayer更像是模型对象。他们封装了几何,时间和一些可视的属性,并且提供了可以显示的内容,但是实际的显示并不是layer的职责。每一个层树的后台都有两个响应树:一个曾现树和一个渲染树)。所以很显然Layer封装了模型数据,每当更改layer中的某些模型数据中数据的属性时,曾现树都会做一个动画代替,之后由渲染树负责渲染图片。

既然AnimationLayer封装了对象模型中的几何性质,那么如何取得这些几何特性。一个方式是根据Layer中定义的属性,比如bounds,authorPoint,frame等等这些属性,其次,CoreAnimation扩展了键值对协议,这样就允许开发者通过get和set方法,方便的得到layer中的各种几何属性。下表是Transform的keypaths。例如转换动画的各种几何特性,大都可以通过此方法设定:

[myLayer setValue:[NSNumber numberWithInt:0] forKeyPath:@"transform.rotation.x"];

图2

虽然CALayer跟UIView十分相似,也可以通过分析CALayer的特点理解UIView的特性,但是毕竟苹果公司不是用CALayer来代替UIView的,否则苹果公司也不回设计一个UIView类了。就像官方文档解释的一样,CAlayer层树是cocoa视图继承树的同等物,它具备UIView的很多共同点,但是CoreAnimation没有提供一个方法展示在窗口。他们必须宿主到UIView中,并且UIView给他们提供响应的方法。所以UIReponder就是UIView的又一个大的特性。

2.UIView继承的UIResponder

UIResponder是所有事件响应的基石,官方也提供了一个重要的文档给开发者参考”EventHandlingGuideforiOS”。

事件(UIEvent)是发给应用程序,告知用户的行动的。在IOS中事件有三种事件:多点触摸事件,行动事件,远程控制事件。三种事件定义如下:

typedef enum { UIEventTypeTouches, UIEventTypeMotion, UIEventTypeRemoteControl, } UIEventType;

再来看下UIReponder中的事件传递过程,如下图所示:

图3

首先是被点击的该视图响应时间处理函数,如果没有响应函数就逐级的向上面传递,直到有响应处理函数,或者该消息被抛弃。至于苹果公司是如何让事件消息这样流动的,在下面的分析中,可以了解一些,至于深层的原理还的进一步挖掘。

这里重点看三个事件中的多点触摸事件,也就是UITouch事件,下图是UIEvent中封装的UITouch内容

图4

关于UIView的触摸响应事件中,这里有一个常常容易迷惑的方法hitTest:WithEvent。先来看官方的解释:ThismethodtraversestheviewhierarchybysendingthepointInside:withEvent:message

toeachsubviewtodetermine whichsubviewshouldreceiveatouchevent.

IfpointInside:withEvent:returnsYES,thenthesubview’shierarchyistraversed;
otherwise,itsbranchoftheviewhierarchyisignored.

Yourarelyneedtocallthismethodyourself,

butyoumightoverrideittohidetoucheventsfromsubviews.(通过发送PointInside:withEvent:消息给每一个子视图,这个方法遍历视图层树,来决定那个视图应该响应此事件。如果PointInside:withEvent:返回YES,然后子视图的继承树就会被遍历;否则,视图的继承树就会被忽略。你很少需要调用这个方法,仅仅需要重载这个方法去隐藏子视图的事件)。从官方的API上的解释,可以看出hitTest方法中,要先调用PointInside:withEvent:,看是否要遍历子视图。如果我们不想让某个视图响应事件,只需要重载PointInside:withEvent:方法,让此方法返回NO就行了。不过从这里,还是不能了解到hitTest:WithEvent的方法的用途。

下面再从”EventHandlingGuideforiOS”找答案,Yourcustomrespondercanusehit-testingtofindthesubvieworsublayerofitselfthatis"under”atouch,andthenhandletheeventappropriately。从中可以看出hitTest主要用途是用来寻找那个视图是被触摸了。看到这里对hitTest的调用过程还是一知半解。我们可以实际建立一个工程进行调试。建立一个MyView里面重载hitTest和pointInside方法:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ [super hitTest:point withEvent:event]; return self; } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{ NSLog(@"view pointInside"); return YES; }

然后在MyView中增加一个子视图MySecondView也重载这两个方法

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ [super hitTest:point withEvent:event]; return self; } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{ NSLog(@"second view pointInside"); return YES; }

这里注意[superhitTest:pointwithEvent:event];必须要包括,否则hitTest无法调用父类的方法,这样就没法使用PointInside:withEvent:进行判断,那么就没法进行子视图的遍历。当去掉这个语句的时候,触摸事件就不可能进到子视图中了,除非你在方法中直接返回子视图的对象。这样你在调试的过程中就会发现,每次你点击一个view都会先进入到这个view的父视图中的hitTest方法,然后调用super的hitTest方法之后就会查找pointInside是否返回YES如果是,则就把消息传递个子视图处理,子视图用同样的方法递归查找自己的子视图。所以从这里调试分析看,hitTest方法这种递归调用的方式就一目了然了。

这个只是说了调试中吻合官方文档中解释的部分,但是还有一个问题,就是每个view中hitTest总要调用三个,这个查找了API和很多资料都没有找到解决的方法,然后google了以下在overflowstack中发现了有人这样解释:Thereareindeed3callstohitTest.Itisnotclearwhy,

butwecansurmisebythetimestampsontheevent

thatthefirsttwocallsaretodowithcompletingthepreviousgesture-

thosetimestampsarealwaysveryclosetowhenevertheprevioustouchhappened,

andwillbesomedistancefromthecurrenttime.(确实有3次调用hitTest,不清楚为什么,但是前两次调用时里面的UIEvent中的timestamps属性和上一次已经完成的手势有关。这些时间timestamps是如此的接近无论先前的触摸什么时候发生,并且和系统当前的时间有一定的间隔)。看到这里我想到了,”EventHandlingGuideforiOS”中曾经解释,如何区分单击和双击的区别,用的方法很简单,代码如下:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *aTouch = [touches anyObject]; if (aTouch.tapCount == 2) { [NSObject cancelPreviousPerformRequestsWithTarget:self]; } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { UITouch *theTouch = [touches anyObject]; if (theTouch.tapCount == 1) { NSDictionary *touchLoc = [NSDictionary dictionaryWithObject: [NSValue valueWithCGPoint:[theTouch locationInView:self]] forKey:@"location"]; [self performSelector:@selector(handleSingleTap:) withObject:touchLoc afterDelay:0.3]; } else if (theTouch.tapCount == 2) { // Double-tap: increase image size by 10%" CGRect myFrame = self.frame; myFrame.size.width += self.frame.size.width * 0.1; myFrame.size.height += self.frame.size.height * 0.1; myFrame.origin.x -= (self.frame.origin.x * 0.1) / 2.0; myFrame.origin.y -= (self.frame.origin.y * 0.1) / 2.0; [UIView beginAnimations:nil context:NULL]; [self setFrame:myFrame]; [UIView commitAnimations]; } } - (void)handleSingleTap:(NSDictionary *)touches { // Single-tap: decrease image size by 10%" CGRect myFrame = self.frame; myFrame.size.width -= self.frame.size.width * 0.1; myFrame.size.height -= self.frame.size.height * 0.1; myFrame.origin.x += (self.frame.origin.x * 0.1) / 2.0; myFrame.origin.y += (self.frame.origin.y * 0.1) / 2.0; [UIView beginAnimations:nil context:NULL]; [self setFrame:myFrame]; [UIView commitAnimations]; }

所以区别这两个手势的思想,就是判断tapcount如果发现touchEnd的时候tapcount是2就取消第一次执行的动作。但是这一点是否想过,苹果公司是如何判断tapcount的,比如说我在屏幕上按了下去,过了一分钟后松开,那么在touchEnd方法中捕捉到的touch事件和我点击一下屏幕就起来一样么?答案是不一样的,可以写程序亲自试验以下,按下去一分钟再松开,这里没必要一分钟了,就几秒也足够了,你会发现再touchEnd中tapCount为0,而点击一下松开的tapCount为1。还有一种情况就是双击,如果我双击间隔的时间超过大概4,5秒钟,再次侦测touchEnd中的tapCount就会发现是1,而正常的双击tapCount为2。这里和hitTest执行三次,并且前两次记录的时间是上一次触摸手势的时间,后一次才是本次触摸手势的时间,有没有关系,官方没有任何解释,这里也只能臆测。是不是用来区分上面所说的情况,也就是说根据这个事件timestamp来改变UITouch中tapCount的次数,还希望那位高手给予解释。所以上面提到的UIEvent,这个事件为何能向苹果官方解释的那样流动,这里也就可见一斑了。

最后推荐俩本iphone开发的书籍,非常适合初学者使用。一本是iphone开发秘籍,另外一本是Core.AnimationSimplified。我的空间里有这两本书的资源,不过都是英文原本的,有兴趣的可以看下(http://download.csdn.net/source/3347531http://download.csdn.net/user/mengtnt

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值