重点参考链接:
View Programming Guide for iOS
Table View Programming Guide for iOS
1 UIWindow
1.1 UIWindow简介
UIWindow是一种特殊的UIView,通常在一个app中只会有一个UIWindow。iOS程序启动完毕后,创建的第一个视图控件就是UIWindow,接着创建控制器的view,最后将控制器的view添加到UIWindow上,于是控制器的view就显示在屏幕上了。一个iOS程序之所以能显示到屏幕上,完全是因为它有UIWindow。也就说,没有UIWindow,就看不见任何UI界面。
补充:UIWindow是创建的第一个视图控件(创建的第一个对象是UIapplication)如下图:
添加
先创建UIwindow,再创建控制器,创建控制器的view,然后将控制器的view添加到UIWindow上。
文档中关于该部分的解释:
1.2 UIWindow的创建过程
1.简单说明
创建一个空的项目,就可以看到UIWindow是怎么出来的了。在程序启动完毕之后就会调用一次,创建过程如下:
提示:应用程序启动之后,先创建Application,再创建它的代理,之后创建UIwindow。UIWindow继承自UIview。
2.把view添加到uiwindow
创建一个控制器,把view添加到uiwindow上面(有两种方式)
(1)直接将控制器的view添加到UIWindow中,并不理会它对应的控制器
[self.window addsubview: vc.view];
(2)设置uiwindow的根控制器,自动将rootviewcontroller的view添加到window中,负责管理rootviewcontroller的生命周期
[self.window.rootviewcontroller=vc];
两个方法的区别:
以后的开发中,建议使用(2).因为方法(1)存在一些问题,比如说控制器上面可能由按钮,需要监听按钮的点击事件,如果是1,那么按钮的事件应该由控制器来进行管理。但控制器是一个局部变量,控制器此时已经不存在了,但是控制器的view还在,此时有可能会报错。注意:方法执行完,这个控制器就已经不存在了。
问题描述1:当view发生一些事件的时候,通知控制器,但是控制器已经销毁了,所以可能出现未知的错误。
问题描述2:添加一个开关按钮,让屏幕360度旋转(两者的效果不一样)。当发生屏幕旋转事件的时候,UIapplication对象会将旋转事件传递给uiwindow,uiwindow又会将旋转事件传递给它的根控制器,由根控制器决定是否需要旋转UIapplication->uiwindow->根控制器(第一种方式没有根控制器,所以不能跟着旋转)。
提示:不通过控制器的view也可以做开发,但是在实际开发中,不要这么做,不要直接把view添加到UIWindow上面去。因为,难以管理。
3.在有storyboard的项目中,UIWindow是如何创建的?
为什么创建一个storyboard,没有看到创建uiwindow的过程?
它其实是把创建UIWindow的过程给屏蔽起来了。可以把代理的UIWindow的属性的值打印出来NSLog(@“window=%p”,self.window);打印出来确实是有值的,说明确实创建了UIWindow.不仅创建了UIWindow,默认还创建了UIWindow对应的控制器,也可以打印进行查看。NSLog(@“%@“,self.window.rootviewcontroller);
有storyboard的项目中的创建过程:
当用户点击应用程序图标的时候,先执行Main函数,执行UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给application(看项目配置文件info.plist里面的storyboard的name,根据这个name找到对应的storyboard),开启一个事件循环,当程序加载完毕,他会调用代理的didFinishLaunchingWithOptions:方法。在调用didFinishLaunchingWithOptions:方法之前,会加载storyboard,在加载的时候创建一个window,接下来会创建箭头所指向的控制器,把该控制器设置为UIWindow的根控制器,接下来再将window显示出来,即看到了运行后显示的界面。(提示:关于这部分可以查看story的初始化的文档)
1.3 如何获取window?
1.主窗口和次窗口
[self.window makekeyandvisible]让窗口成为主窗口,并且显示出来。有这个方法,才能把信息显示到屏幕上。
因为Window有makekeyandvisible这个方法,可以让这个Window凭空的显示出来,而其他的view没有这个方法,所以它只能依赖于Window,Window显示出来后,view才依附在Window上显示出来。
[self.window make keywindow]; //让uiwindow成为主窗口,但不显示。
2.获取UIwindow
(1)[UIApplication sharedApplication].windows
在本应用中打开的UIWindow列表,这样就可以接触应用中的任何一个UIView对象(平时输入文字弹出的键盘,就处在一个新的UIWindow中)。
(2)[UIApplication sharedApplication].keyWindow(获取应用程序的主窗口)用来接收键盘以及非触摸类的消息事件的UIWindow,而且程序中每个时刻只能有一个UIWindow是keyWindow。
提示:如果某个UIWindow内部的文本框不能输入文字,可能是因为这个UIWindow不是keyWindow。
(3)view.window获得某个UIView所在的UIWindow。
1.4 四大对象的关系图
1.5 主窗口和次窗口说明
代码:
// 程序启动完毕之后就会调用一次
- (BOOL) application: (UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
// 1.创建UIWindow
self.window = [[UIWindow alloc]initWithFrame: [[UIScreen mainScreen] bounds]];
// 设置UIWindow的背景颜色
self.window.backgroundColor = [UIColor redColor];
// 让UIWindow显示出来(让窗口成为主窗口 并且显示出来)
//一个应用程序只能有一个主窗口
[self.window makeKeyAndVisible];
// 让UIWindow成为主窗口
// [self.window makeKeyWindow];
// 2.再创建一个窗口
UIWindow *w2 = [[UIWindow alloc] initWithFrame: CGRectMake(100,100,200,200)];
w2.backgroundColor = [UIColor yellowColor];
[w2 makeKeyAndVisible];
self.w2 = w2;
// 3.创建两个文本输入框
// 3.1将文本输入框添加到window中
UITextField *tx1 = [[UITextField alloc] initWithFrame: CGRectMake(10,10,200,40)];
tx1.borderStyle = UITextBorderStyleRoundedRect;
[self.window addSubview: tx1];
// 3.2将文本输入框添加到w2中
UITextField *tx2 = [[UITextField alloc] initWithFrame: CGRectMake(10,10,100,40)];
tx2.borderStyle = UITextBorderStyleRoundedRect;
[self.w2 addSubview: tx2];
// 获取应用程序的主窗口
NSLog(@"%@", [UIApplication sharedApplication].keyWindow);
return YES;
}
代码说明:
再创建一个窗口(主窗口和次窗口的区别)
局部变量,需要定义一个Window属性来保存变量。
window的属性定义为strong,就是为了让其不销毁。
一个应用程序只能有一个主窗口,程序中创建了两个Window,那么谁是主窗口?后面的窗口能覆盖前面的窗口。
提示:如果UItextfield不显示,可以考虑设置它的样式,因为其创建默认是虚线的,没有边框。
在ios7里边,主窗口和次窗口是没有区别的。
在ios7以前中有区别:哪个是主窗口,后面设置为主窗口会把之前设置的覆盖掉。(只有主窗口才能响应键盘的输入事件,如果不能输入内容,可以查看是否是显示在主窗口上,不在主窗口上的不能响应。)
1.6 补充说明
在有storyboard中的创建过程:
先执行Main函数,执行UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给application,根据项目配置文件info.plist里面的storyboard的name,找到对应的storyboard,接下来创建一个window,之后创建它的初始化控制器(就是箭头所指向的控制器),自动把该控制器设置为UIWindow的根控制器,接下来再将window显示出来,即看到了运行后显示的界面。
注意这个控制器属性面板上的“初始化控制器属性”。
在没有storyboard中的创建过程:
先执行Main函数,执行UIApplicationMain(),根据其第三个和第四个参数创建Application,创建代理,并且把代理设置给application,开启一个事件循环,当程序加载完毕,他会调用代理的didFinishLaunchingWithOptions:方法。在该方法中,会创建一个Window,然后创建一个控制器,并把该控制器设置为UIWindow的根控制器,接下来再将window显示出来,即看到了运行后显示的界面。
2 UIView接口说明
2.1 API接口说明
addSubview:
添加一个子视图到接收者并让它在最上面显示出来。
- (void) addSubview: (UIView *)view
讨论
这方法同样设置了接收者为下一个视图响应对象。接收者保留视图。如果你使用removeFromSuperview方法用来把视图移除他的显示列表,那么视图将会被释放。如果你想要在视图移除显示列表後保留并使用这个视图(如果,举个例子,你想要交换一些视图的位置。),你不许保留那个视图在他调用removeFromSuperview前。
bringSubviewToFront:
把指定的子视图移动到顶层
- (void)bringSubviewToFront:(UIView *)view
参数view 需要移到顶层的视图
convertPoint:fromView:
把一个点从一个坐标系转换到接收者的坐标系
- (CGPoint) convertPoint: (CGPoint)point fromView: (UIView*)view
参数 point
一个视图中坐标系上的点
view
一个视图包含了点和他自身坐标系。如果是图是nil,那么这个方法将尝试转换基于窗口的坐标系。否则视图和那个接收者必须属于同一个UIWindow对象。
返回值
一个转换到接收者坐标系的点
convertPoint:toView:
转换一个点从接收者坐标系到给定的视图坐标系
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView*)view
参数 point
一个在调用者坐标系中的点
view
一个包含了需要被转换的点的视图。如果视图是nil,那么这个方法将会转换成基于窗口的坐标。否则视图和接收者都要属于同一个UIWindow对象。
返回值
基于视图的坐标系转换过的点
convertRect:fromView:
转换一个矩形从其他视图坐标系到接收者坐标系。
- (CGRect)convertRect:(CGRect)rect fromView:(UIView*)view
参数 rect
一个在视图坐标系中的矩形
view
一个视图内部有矩形在他的坐标系中。如果视图是nil,那么这个方法将会基于窗口来转换。否则视图和接收者必须都属于同一个UIWindow对象
返回值
The converted rectangle 转换过的矩形
convertRect:toView:
转换接收者坐标系中的矩形到其他视图
- (CGRect)convertRect:(CGRect)rect toView:(UIView *)view
参数 rect
一个在接收者坐标系中的矩形
view
要转换过去的目标视图对象。如果这个是视图是nil,这个方法将会基于窗口坐标系来转换。否者视图和接收者必须属于同一个UIwindow对象
返回值
一个转换过的矩形
didAddSubview:
告诉视图当子视图已经添加
- (void) didAddSubview: (UIView *)subview
参数 subview
被添加做子视图的视图对象
讨论
被子类重写用来执行额外的命令当子视图添加到接收者。这个方法被addSubview调用
didMoveToSuperview
通知接收者父视图已经改变(nil是允许的)
- (void)didMoveToSuperview
讨论
默认不做任何事情;子类可以重写这方法来作为特定的实现
didMoveToWindow
通知接收者它一斤给添加到窗口中
- (void)didMoveToWindow
讨论
默认实现不做任何事情;子类可以重写这个方法来做特殊的实现
窗口的属性有可能是nil当这个方法调用的时候,这表明接收者并不属于当然任何一个窗口。这个只发生在接收者从它的父视图上移除或者接收者添加到父视图中而不是添加到window中。重写这个方法可以用来选择忽略一些他们不关心的对象
drawRect:
在接收者视图中绘制矩形
- (void)drawRect:(CGRect)rect
参数 rect
一个定义的需要绘制的矩形
讨论
子类重写这个方法如果他们确实要绘制他们自定义的视图。如果子类是其他视图的容器那么它不需要重写这个方法。默认的实现不做任何事情。如果你自定义的视图是一个UIView子类,你不需要去调用它的父类实现。注意如果它的父类实现绘制并且不透明属性为YES那么每一个子类都需要填充矩形。
当这个方法被调用,接收者可以假定他的帧在坐标上已经转换,边界矩形已经应用;所有他要做的就是绘制自定义的方法。使用UIGraphicsGetCurrentContext方法去获取当前图形内容用来绘制,坐标原点在左上角。不要保留图片内容当他可以被drawRect:这个方法调用。
exchangeSubviewAtIndex:withSubviewAtIndex:
交换接收者的子视图和给定的索引视图
- (void)exchangeSubviewAtIndex:(NSInteger)index1withSubviewAtIndex:(NSInteger)index2
参数 index1
一个需要取代索引2的子视图
index2
一个需要取代索引1的子视图
hitTest:withEvent:
返回接收者视图层次中最远的派生(包括它本身)的特定的点。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent*)event
参数 point
接收者坐标系中的点
event
触发这个方法的事件或者是如果这个方法被预调用就返回nil
返回值
一个视图对象最远的派生点。如果这个点位于接收者之外就返回nil
讨论
这个方法贯穿视图的层次发送pointInside:withEvent:消息到每一个子视图用来决定那个子视图需要接收触摸事件。如果pointInside:withEvent: 返回YES,那么视图的层次全部贯穿;否则视图层次的分支是被否定的。你不太需要调用这个方法,但是你需要重写它用来隐藏子视图的触摸事件。
如果视图是隐藏的,禁止用户交互的或者透明值小于01那么这个方法不可用
initWithFrame:
初始化并返回一个新的拥有特定帧矩形的视图对象
- (id)initWithFrame:(CGRect)aRect
参数 aRect
一个帧矩形用来创建视图对象。原始的帧在它的父视图的坐标系中。设置这个属性用来改变中心和边界属性。
返回值
一个初始化的视图对象,如果没有被创建那就返回nil
讨论
一个新的视图对象必须添加到视图链中才能使用。这个方法为UIView类指出初始化对象。
insertSubview:aboveSubview:
在视图层次顶层插入一个视图
- (void)insertSubview:(UIView *)view aboveSubview:(UIView*)siblingSubview
参数 view
一个插入被用来放在顶层的视图。它将会从父视图中移除如果它不是相邻视图
siblingSubview
一个相邻视图用来放在插入视图的後面
insertSubview:atIndex:
插入视图到指定的索引
- (void)insertSubview:(UIView *)viewatIndex:(NSInteger)index
参数 view
插入的视图,这个值不能是nil
index
子视图索引从0开始并且不能大于子视图的数量
insertSubview:belowSubview:
插入视图到显示链的底层
- (void)insertSubview:(UIView *)view belowSubview:(UIView*)siblingSubview
参数 view
一个需要插入到其他视图底部的视图。它将从它的父视图移除如果它不与相邻视图的相邻
siblingSubview
一个相邻视图将会在插入的视图之上
isDescendantOfView:
返回一个布尔值指出接收者是否是给定视图的子视图或者指向那个视图
- (BOOL)isDescendantOfView:(UIView *)view
参数 view
一个视图用来测试子视图在视图层次中的关系
返回值
如果接收者是视图的子视图就返回YES,或者视图就是接收者;否则就是NO
layoutIfNeeded
排列子视图如果需要的话
- (void)layoutIfNeeded
讨论
使用这个方法来关注子视图的排列在绘制前
layoutSubviews
排列子视图
- (void)layoutSubviews
讨论
当layoutIfNeeded被调用是子类用来重写这个方法来排列子视图。默认实现这个方法不做任何事情。
pointInside:withEvent:
返回一个布尔值指出接收者是否包含特定的点
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
参数 point
一个在接收者坐标系内的点
event
这个方法的目标事件或者如果这个方法被预调用返回nil
返回值
如果点在接收者边界内返回YES,否则返回NO
removeFromSuperview
把接收者从它的父视图或者窗口移除,并在响应链中移除。
- (void)removeFromSuperview
讨论
接收者同时释放;如果你计划重用它,要确定在发送消息前保持它并在添加到其他UIView对象作为子视图後移除。 不要在显示的时候调用
sendSubviewToBack:
移动指定的子视图到它相邻视图的後面
- (void)sendSubviewToBack:(UIView *)view
参数 view
一个子视图用来移动到它後面去
setNeedsDisplay
控制接收者的边界矩形被标记为需要显示
-(void)setNeedsDisplay
讨论
默认情况下,视图几何图形的改变自动重绘而不需要调用drawRect:方法。因此,你需要去请求视图重绘当视图的数据或者状态改变的时候。从这个意义上来说,向视图发送setNeedsDisplay消息。任何UIView对象标记为需要显示後将会在应用程序循环中自动重新绘制。
setNeedsDisplayInRect:
标记接收者中的特定的矩形区域为需要显示,或者添加接收者现有的其他无效区域
- (void)setNeedsDisplayInRect:(CGRect)invalidRect
参数 invalidRect
标记接收者的矩形区域为无效的;他需要在接收者坐标系中定义。
讨论
默认情况下,视图几何图形的改变自动重绘而不需要调用drawRect:方法。因此,你需要去请求视图重绘当视图的数据或者状态改变的时候。使用这个方法或者用setNeedsDisplay方法来标记视图需要显示的地方。
setNeedsLayout
设置当子视图显示的时候需要重新排列
- (void)setNeedsLayout
讨论
如果你调用这个方法在下一个显示方法之间,那么layoutIfNeeded排列子视图;否则将不会做任何事情
sizeThatFits:
计算并返回一个最好的适应接收者子视图的大小
- (CGSize)sizeThatFits:(CGSize)size
参数 size
接收者首选的尺寸
返回值
一个新的大小用来适应接收者子视图
讨论
默认的实现返回大小参数 子类重写这个方法用来返回特定视图的大小。举个粒子,UISwitch返回一个修正过的大小,UIImageView返回图片的大小 这个方法并没有改变接收者的大小
sizeToFit
调整大小并移动接收者视图大小所以他包含了他的子视图
- (void)sizeToFit
讨论
这个方法使用sizeThatFits:方法来决定大小。子类需要重写sizeThatFits:用来计算正确的尺寸大小。默认的实现不做任何事情。
viewWithTag:
返回视图的特定的标签
- (UIView *)viewWithTag:(NSInteger)tag
参数 tag
一个用来在视图中搜索的标签
返回值
视图在接收者层次中符合的标签,接收者也包含在搜索中。
willMoveToSuperview:
通知接收者他的父视图将会改变到特定的父视图(也有可能是nil)
- (void)willMoveToSuperview:(UIView *)newSuperview
参数 newSuperview
新的视图对象将会是接收者新的父视图
讨论
子类可以重写这个方法来做一些特定的行为
willMoveToWindow:
通知接收者它已经被添加到特定的窗口对戏那个的视图层次中(也有可能是nil)
- (void)willMoveToWindow:(UIWindow *)newWindow
参数 newWindow
一个窗口对象将会成为接收者新的视图层次的根视图
讨论
子类可以重写这个方法来提供一些特定的必要实现
willRemoveSubview:
由子类重写用来在子视图从接收者视图中移除前执行一些特定的方法。
- (void)willRemoveSubview:(UIView *)subview
参数 subview
子视图将会被移除
讨论
这个方法被调用当子视图接收到removeFromSuperview消息或者子视图从接收者视图层次中移除因为它要被添加到其他视图了
3 UIView动画
3.1 概述
UIView视图的动画功能,可以使在更新或切换视图时有放缓节奏、产生流畅的动画效果,进而改善用户体验。UIView可以产生动画效果的变化包括:
• 位置变化:在屏幕上移动视图。
• 大小变化:改变视图框架(frame)和边界。
• 拉伸变化:改变视图内容的延展区域。
• 改变透明度:改变视图的alpha值。
• 改变状态:隐藏或显示状态。
• 改变视图层次顺序:视图哪个前哪个后。
• 旋转:即任何应用到视图上的仿射变换(transform)。
UIKit直接将动画集成到UIView类中,实现简单动画的创建过程。UIView类定义了几个内在支持动画的属性声明,当这些属性发生改变时,视图为其变化过程提供内建的动画支持。
执行动画所需要的工作由UIView类自动完成,但仍要在希望执行动画时通知视图,为此需要将改变属性的代码包装到一个代码块中。
3.2 UIView动画简单创建方法
- (void) buttonPressed
{
// 交换本视图控制器中2个view位置
[self.view exchangeSubviewAtIndex: 0 withSubviewAtIndex: 1];
//UIView开始动画,第一个参数是动画的标识,第二个参数附加的应用程序信息用来传递给动画代理消息
[UIView beginAnimations: @"View Flip" context: nil];
//动画持续时间
[UIView setAnimationDuration: 1.25];
//设置动画的回调函数,设置后可以使用回调方法
[UIView setAnimationDelegate: self];
//设置动画曲线,控制动画速度
[UIView setAnimationCurve: UIViewAnimationCurveEaseInOut];
//设置动画方式,并指出动画发生的位置
[UIView setAnimationTransition: UIViewAnimationTransitionCurlUp forView: self.view cache: YES];
//提交UIView动画
[UIView commitAnimations];
}
- (void) viewDidLoad
{
[super viewDidLoad];
//主要功能通过UIView动画完成2个试图控制器的切换
self.blueController = [[BlueViewController alloc] initWithNibName: nil bundle: nil];
//设置导航控制器view的大小占整个屏幕
[self.blueController.view setFrame: CGRectMake(0, 0, self.view.frame.size.width , self.view.frame.size.height)];
self.yellowController = [[YellowController alloc] initWithNibName: nil bundle: nil ];
[self.yellowController.view setFrame: CGRectMake(0, 0, self.view.frame.size.width , self.view.frame.size.height)];
//将2个控制器view插入到目前导航控制器视图上,yellowController后插入,显示在最前面
[self.view insertSubview: self.blueController.view atIndex: 0];
[self.view insertSubview: self.yellowController.view atIndex: 1];
//创建导航控制器右按钮,按钮名字叫next
//添加buttonPressed 事件
self.rightBarItem = [[UIBarButtonItem alloc] initWithTitle: @"next" style: UIBarButtonItemStylePlain target: self action: @selector(buttonPressed)];
//将按钮添加到导航控制器默认右按钮上
self.navigationItem.rightBarButtonItem = self.rightBarItem;
}
有个问题:如果动画不放在按钮事件中,直接放到viewDidLoad里,程序首先执行这个controller,这时动画是不会显示的。
原因:出现这个问题是因为开机时候系统有个动画,系统动画和这个动画重复了。
解决方案:
1、将动画写在按钮事件中
2、利用定时器。
转:UIView动画更具体讲解;http://wsqwsq000.iteye.com/blog/1189183
3.3 创建UIView动画(块)——(指过渡效果的动画)
3.3.1 一.基本方式:使用UIView类的UIViewAnimation扩展
UIView动画是成块运行的。发出beginAnimations:context:请求标志着动画块的开始;commitAnimations标志着动画块的结束。把这两个类方法发送给UIView而不是发送给单独的视图。在这两个调用之间的可定义动画的展现方式并更新视图。函数说明:
//开始准备动画
+ (void) beginAnimations: (NSString *)animationID context: (void *)context;
//运行动画
+ (void) commitAnimations;
具体二段动画代码:
[UIView beginAnimations: nil context: nil];
//setAnimationCurve来定义动画加速或减速方式
[UIView setAnimaitonCurve: UIViewAnimationCurveLinear];
[UIView setAnimationDuration: 2.7]; //动画时长
[UIView setAnimationTransition: transition forView: self.view cache: YES];
// operation>>>
[self.view exchangeSubviewAtIndex: 0 withSubviewAtIndex: 1];
//end<<<<<
[UIView commitAnimations];
CGContextRef context = UIGraphicsGetCurrentContext(); //返回当前视图堆栈顶部的图形上下文
[UIView beginAnimations: nil context: context];
[UIView setAnimaitonCurve: UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration: 1.0];
// View changes go here
[contextView setAlpha: 0.0f];
[UIView commitAnimations];
其中transition取值范围:UIView类本身提供四种过渡效果
UIViewAnimationTransitionNone 正常
UIViewAnimationTransitionFlipFromLeft 从左向右翻
UIViewAnimationTransitionFlipFromRight 从右向左翻
UIViewAnimationTransitionCurlUp 从下向上卷
UIViewAnimationTransitionCurlDown 从上向下卷
3.3.2 二.block方式:使用UIView类的UIViewAnimation WithBlocks扩展
要用到的函数有:
+ (void) animateWithDuration: (NSTimeInterval)duration delay: (NSTimeInterval)delay options:(UIViewAnimationOptions)options animations: (void (^)(void))animations completion: (void (^)(BOOL finished))completion
__OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
//间隔,延迟,动画参数(好像没用?),界面更改块,结束块
+ (void) animateWithDuration: (NSTimeInterval)duration animations: (void (^)(void))animations
completion: (void (^)(BOOL finished))completion
__OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
// delay = 0.0, options = 0
+ (void) animateWithDuration: (NSTimeInterval)duration animations: (void (^)(void))animations
__OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
// delay = 0.0, options = 0, completion = NULL
+ (void) transitionWithView: (UIView *)view duration: (NSTimeInterval)duration options:(UIViewAnimationOptions)options animations: (void (^)(void))animations
completion: (void (^)(BOOL finished))completion
__OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
+ (void)transitionFromView: (UIView*) fromView toView: (UIView *)toView duration: (NSTimeInterval)duration options: (UIViewAnimationOptions)options completion: (void (^)(BOOL finished))completion
__OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);
// toView added to fromView.superview, fromView removed from its superview界面替换,这里的options参数有效
3.3.3 三.core方式:使用CATransition类
iPhone还支持Core Animation作为其QuartzCore架构的一部分,CA API为iPhone应用程序提供了高度灵活的动画解决方案。但是须知:CATransition只针对图层,不针对视图。图层是Core Animation与每个UIView产生联系的工作层面。使用Core Animation时,应该将CATransition应用到视图的默认图层([myView layer])而不是视图本身。
使用CATransition类实现动画,只需要建立一个Core Animation对象,设置它的参数,然后把这个带参数的过渡添加到图层即可。
使用要引入QuartzCore.framework 代码
#import <QuartzCore/QuartzCore.h>
示例代码:
CATransition *transition = [CATransition animation];
transition.duration = 0.7;
transition.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionMoveIn;
//{kCATransitionMoveIn, kCATransitionPush, kCATransitionReveal, kCATransitionFade};
//更多私有{@"cube", @"suckEffect", @"oglFlip", @"rippleEffect", @"pageCurl", @"pageUnCurl", @"cameraIrisHollowOpen", @"cameraIrisHollowClose"};
transition.subtype = kCATransitionFromLeft;
//{kCATransitionFromLeft,kCATransitionFromRight, kCATransitionFromTop, kCATransitionFromBottom};
transition.delegate = self;
[self.view.layer addAnimation: transition forKey: nil];
// 要做的
[self.view exchangeSubviewAtIndex: 1 withSubviewAtIndex: 0];
CATransition动画使用了类型type和子类型subtype两个概念。type属性指定了过渡的种类(淡化、推挤、揭开、覆盖)。subtype设置了过渡的方向(从上、下、左、右)。另外,CATransition私有的动画类型有(立方体、吸收、翻转、波纹、翻页、反翻页、镜头开、镜头关)。
3.4 API简介
areAnimationsEnabled
返回一个布尔值表示动画是否结束。
+ (BOOL)areAnimationsEnabled
返回值
如果动画结束返回YES,否则NO。
beginAnimations:context:
开始一个动画块
+ (void)beginAnimations:(NSString *)animationID context:(void *)context
参数
animationID
动画块内部应用程序标识用来传递给动画代理消息-这个选择器运用setAnimationWillStartSelector: 和setAnimationDidStopSelector:方法来设置。
context
附加的应用程序信息用来传递给动画代理消息-这个选择器使用setAnimationWillStartSelector: 和setAnimationDidStopSelector:方法。
讨论
这个值改变是因为设置了一些需要在动画块中产生动画的属性。动画块可以被嵌套。如果在没有在动画块中调用那么setAnimation类方法将什么都不做。使用 beginAnimations:context:来开始一个动画块并用commitAnimations类方法来结束一个动画块。
commitAnimations
结束一个动画块并开始当他在动画块外时。
+ (void)commitAnimations
讨论
如果当前的动画块是最外层的动画块,当应用程序返回到循环运行时开始动画块。动画在一个独立的线程中所有应用程序不会中断。使用这个方法,多个动画可以被实现。查看setAnimationBeginsFromCurrentState:来了解如果开始一个动画当另外一个动画在播放的时候。
layerClass
返回类用来创建这一个本类的layer实例对象。
+ (Class)layerClass
返回值
一个用来创建视图layer的类
讨论
重写子类来指定一个自定义类用来显示。当在创建视图layer时候调用。默认的值是CALayer类对象。
setAnimationBeginsFromCurrentState:
设置动画从当前状态开始播放。
+ (void)setAnimationBeginsFromCurrentState:(BOOL)fromCurrentState
参数
fromCurrentState
YES如果动画需要从他们当前状态开始播放。否则为NO。
讨论
如果设置为YES那么当动画在运行过程中,当前视图的位置将会作为新的动画的开始状态。如果设置为NO,当前动画结束前新动画将使用视图最後状态的位置作 为开始状态。这个方法将不会做任何事情如果动画没有运行或者没有在动画块外调用。使用beginAnimations:context:类方法来开始并用 commitAnimations类方法来结束动画块。默认值是NO。
setAnimationCurve:
设置动画块中的动画属性变化的曲线。
+ (void)setAnimationCurve:(UIViewAnimationCurve)curve
讨论
动画曲线是动画运行过程中相对的速度。如果在动画块外调用这个方法将会无效。使用 beginAnimations:context:类方法来开始动画块并用commitAnimations来结束动画块。默认动画曲线的值是UIViewAnimationCurveEaseInOut。
setAnimationDelay:
在动画块中设置动画的延迟属性(以秒为单位)
+ (void)setAnimationDelay:(NSTimeInterval)delay
讨论
这个方法在动画块外调用无效。使用beginAnimations:context: 类方法开始一个动画块并用commitAnimations类方法结束动画块。默认的动画延迟是0.0秒。
setAnimationDelegate:
设置动画消息的代理。
+ (void)setAnimationDelegate:(id)delegate
参数
delegate
你可以用setAnimationWillStartSelector:和setAnimationDidStopSelector: 方法来设置接收代理消息的对象。
讨论
这个方法在动画块外没有任何效果。使用beginAnimations:context:类方法开始一个动画块并用commitAnimations类方法结束一个动画块。默认值是nil
setAnimationDidStopSelector:
设置消息给动画代理当动画停止的时候。
+ (void)setAnimationDidStopSelector:(SEL)selector
参数
selector
当动画结束的时候发送给动画代理。默认值是NULL。这个选择者须有下面方法的签名:animationFinished:(NSString *)animationID finished: (BOOL)finished context: (void *)context。
animationID
一个应用程序提供的标识符。和传给beginAnimations:context: 相同的参数。这个参数可以为空。
finished
如果动画在停止前完成那返回YES;否则就是NO。
context
一个可选的应用程序内容提供者。和beginAnimations:context: 方法相同的参数。可以为空。
讨论
这个方法在动画块外没有任何效果。使用beginAnimations:context: 类方法来开始一个动画块并用commitAnimations类方法结束。默认值是NULL。
setAnimationDuration:
设置动画块中的动画持续时间(用秒)
+ (void)setAnimationDuration:(NSTimeInterval)duration
参数
duration
一段动画持续的时间。
讨论
这个方法在动画块外没有效果。使用beginAnimations:context: 类方法来开始一个动画块并用commitAnimations类方法来结束一个动画块。默认值是0.2。
setAnimationRepeatAutoreverses:
设置动画块中的动画效果是否自动重复播放。
+ (void)setAnimationRepeatAutoreverses:(BOOL)repeatAutoreverses
参数
repeatAutoreverses
如果动画自动重复就是YES否则就是NO。
讨论
自动重复是当动画向前播放结束後再重头开始播放。使用setAnimationRepeatCount: 类方法来指定动画自动重播的时间。如果重复数为0或者在动画块外那将没有任何效果。使用beginAnimations:context:类方法来开始一个动画块并用commitAnimations方法来结束一个动画块。默认值是NO。
setAnimationRepeatCount:
设置动画在动画模块中的重复次数
+ (void)setAnimationRepeatCount:(float)repeatCount
参数
repeatCount
动画重复的次数,这个值可以是分数。
讨论
这个属性在动画块外没有任何作用。使用beginAnimations:context:类方法来开始一个动画块并用commitAnimations类方法来结束。默认动画不循环。
setAnimationsEnabled:
设置是否激活动画
+ (void)setAnimationsEnabled:(BOOL)enabled
参数
enabled
如果是YES那就激活动画;否则就是NO
讨论
当动画参数没有被激活那么动画属性的改变将被忽略。默认动画是被激活的。
setAnimationStartDate:
设置在动画块内部动画属性改变的开始时间
+ (void)setAnimationStartDate:(NSDate *)startTime
参数
startTime
一个开始动画的时间
讨论
使用beginAnimations:context:类方法来开始一个动画块并用commitAnimations类方法来结束动画块。默认的开始时间值由CFAbsoluteTimeGetCurrent方法来返回。
setAnimationTransition:forView:cache:
在动画块中为视图设置过渡
+ (void)setAnimationTransition:(UIViewAnimationTransition)transitionforView:(UIView *)view cache:(BOOL)cache
参数
transition
把一个过渡效果应用到视图中。可能的值定义在UIViewAnimationTransition中。
view
需要过渡的视图对象。
cache
如果是YES,那么在开始和结束图片视图渲染一次并在动画中创建帧;否则,视图将会在每一帧都渲染。例如缓存,你不需要在视图转变中不停的更新,你只需要等到转换完成再去更新视图。
讨论
如果你想要在转变过程中改变视图的外貌。举个例子,文件从一个视图到另一个视图,然後使用一个UIView子类的容器视图,如下:
1.Begin an animation block.
2.Set the transition on the container view.
3.Remove the subview from the container view.
4.Add the new subview to the container view.
5.Commit the animation block.
1.开始一个动画块。 2.在容器视图中设置转换。 3.在容器视图中移除子视图。 4.在容器视图中添加子视图。 5.结束动画块。
setAnimationWillStartSelector:
当动画开始时发送一条消息到动画代理
+ (void)setAnimationWillStartSelector:(SEL)selector
参数
selector
在动画开始前向动画代理发送消息。默认值是NULL。这个selector必须由和beginAnimations:context:方法相同的参数,一个任选的程序标识和内容。这些参数都可以是nil。
讨论
这个方法在动画块外没有任何作用。使用beginAnimations:context:类方法来开始一个动画块并用commitAnimations类方法来结束。
3.5 简单动画效果示例
3.5.1 简单移动
imageView.transform = CGAffineTransformIdentity;
imageView.frame = CGRectMake(0, 100, 320, 320);
[UIView beginAnimations: @"clearmemory" context: imageView];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector: @selector(enablebutton)];
imageView.frame = CGRectMake(34, 0, 320, 320);
[UIView commitAnimations];
3.5.2 动画曲线
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 1];
[UIView setAnimationDelegate: self];
[UIView setAnimationCurve: UIViewAnimationCurveEaseIn];
// UIViewAnimationCurveEaseInOut, // slow at beginning and end
//UIViewAnimationCurveEaseIn, // slow at beginning
//UIViewAnimationCurveEaseOut, // slow at end
//UIViewAnimationCurveLinear
//恒定速度
[UIView setAnimationDidStopSelector: @selector(enablebutton:)];
imageView.frame = CGRectMake(22, 0, 320, 320);
[UIView commitAnimations];
3.5.3 反向重复
[UIView beginAnimations: @"animation3" context: imageView1];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
[UIView setAnimationDuration: 1.0];
[UIView setAnimationRepeatAutoreverses: YES];
[UIView setAnimationRepeatCount: 10];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector: @selector(enablebutton:)];
imageView1.alpha=0;
[UIView commitAnimations];
3.5.4 延时,缓入,缓出
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDelay: 0.5];
[UIView setAnimationDuration: 1.5];
[UIView setAnimationCurve: UIViewAnimationCurveEaseIn];
[UIView setAnimationRepeatAutoreverses: YES];
[UIView setAnimationRepeatCount: 2];
[UIView setAnimationDelegate: self];
[UIViewsetAnimationDidStopSelector: @selector(enablebutton:)];
imageView.frame = CGRectMake(120, 0, 200, 200);
[UIView commitAnimations];
//缓出
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 1];
[UIView setAnimationCurve: UIViewAnimationCurveEaseOut];
[UIView setAnimationDelegate: self];
[UIViewsetAnimationDidStopSelector: @selector(enablebutton:)];
imageView.frame = CGRectMake(235, 144, 200 , 200);
[UIView commitAnimations];
3.5.5 放大
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 1];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
[UIView setAnimationDelegate: self];
[UIViewsetAnimationDidStopSelector: @selector(enablebutton:)];
imageView.transform = CGAffineTransformMakeScale(2, 2);
[UIView commitAnimations];
//旋转放大这里用到179.9是因为你不管前面加-+都是逆时针
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 1];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector: @selector(enablebutton:)];
CGAffineTransform tranform1 = CGAffineTransformMakeScale(1,1);
CGAffineTransformtranform3 = CGAffineTransformMakeTranslation(200, 200);
CGAffineTransformtranform2 = CGAffineTransformMakeRotation(179.9*M_PI/180.0);
imageView.transform = CGAffineTransformConcat(tranform1,tranform3);
[UIView commitAnimations];
3.5.6 平移旋转
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 1];
[UIView setAnimationCurve: UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelegate: self];
[UIViewsetAnimationDidStopSelector: @selector(enablebutton:)];
CGAffineTransform tranform1 = CGAffineTransformMakeTranslation(-200, 0);
// CGAffineTransform tranform2 = CGAffineTransformMakeRotation(179.9*M_PI/180.0);
imageView.transform = CGAffineTransformRotate(tranform1,359.9*M_PI/180.0);
[UIView commitAnimations];
3.5.7 翻转
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 1];
[UIView setAnimationCurve: UIViewAnimationCurveEaseIn];
[UIViewsetAnimationTransition: UIViewAnimationTransitionFlipFromRightforView: self.view cache: YES];
//UIViewAnimationTransitionFlipFromLeft 从左往右翻
//UIViewAnimationTransitionFlipFromRight从右往左翻
//UIViewAnimationTransitionCurlUp 从上往下翻
//UIViewAnimationTransitionCurlDown 从下往上翻
[UIView setAnimationDelegate: self];
[UIView setAnimationDidStopSelector: @selector(enablebutton:)];
imageView.hidden = YES;
imageView1.hidden = NO;
[UIView commitAnimations];
3.5.8 淡入淡出
CATransition *animation = [CATransition animation];
animation.duration = 0.75f; //动画时长
animation.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
animation.delegate = self;
animation.type = kCATransitionFade; //过度效果
//kCATransitionFade 淡入淡出
//kCATransitionMoveIn 移入移出
//kCATransitionPush 压入推出
//kCATransitionReveal 覆盖移除
animation.subtype = kCATransitionFromLeft;
//kCATransitionFromRight 从左
//kCATransitionFromLeft 从右
//kCATransitionFromTop 从上
//kCATransitionFromBottom 从下
[self.view.layer addAnimation: animation forKey: @"animation"];
imageView.hidden = YES;
imageView1.hidden = NO;
- (void) enablebutton: (id)sender
{
imageView.transform = CGAffineTransformIdentity;
imageView.frame = CGRectMake(0, 0, 200, 200);
//btn.enabled = NO;
}
3.5.9 点击缩略图到大图的过场动画
CGRect frame = [smallImgView.superview convertRect: smallImgView.frame toView: self.window];
//后续有入场动画
_coverView = [[UIScrollView alloc] initWithFrame: frame];
//增加入场动画
[UIView beginAnimations: nil context: nil];
[UIView setAnimationDuration: 0.25];
_coverView.frame = CGRectMake(0, 0, ESScrnW, ESScrnH);
[UIView commitAnimations];
4 UIView重绘机制
4.1 DrawRect机制
4.1.1 简介
iOS的绘图操作是在UIView类的drawRect方法中完成的,所以如果我们要想在一个UIView中绘图,需要写一个扩展UIView 的类,并重写drawRect方法,在这里进行绘图操作,程序会自动调用此方法进行绘图。
重绘操作仍然在drawRect方法中完成,但是苹果不建议直接调用drawRect方法,当然如果你强直直接调用此方法,当然是没有效果的。苹果要求我们调用UIView类中的setNeedsDisplay方法,则程序会自动调用drawRect方法进行重绘。(调用setNeedsDisplay会自动调用drawRect)
在UIView中,重写drawRect: (CGRect)aRect方法,可以自己定义想要画的图案.且此方法一般情况下只会画一次.也就是说这个drawRect方法一般情况下只会被掉用一次. 当某些情况下想要手动重画这个View,只需要掉用[self setNeedsDisplay]方法即可.
drawRect是在Controller->loadView,
Controller->viewDidLoad 两方法之后调用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
1.如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。
2.该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3.通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4.直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0.
以上1,2推荐;而3,4不提倡。
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者setNeedsDisplayInRect ,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似鱼drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕。
4.2 使用技巧
4.2.1 UITableViewCell中使用DrawRect
不能在UITableViewCell中直接使用DrawRect方法,应该在ContentView中增加一个子View,然后在这个字View中使用DrawRect方法。
4.2.2 (未解决)DrawRect如何清空之前内容
4.2.3 DrawRect之后注意用hitTest:withEvent:方法处理事件接收
//用户触摸时第一时间加载内容
- (UIView*) hitTest: (CGPoint)point withEvent: (UIEvent*)event{
UIView *result = [super hitTest: point withEvent: event];
CGPoint buttonPoint = [_subjectButton convertPoint: point fromView: self];
if ([_subjectButton pointInside: buttonPoint withEvent: event]){
return _subjectButton;
}
return result;
}
4.3 参考链接
CGContextRef用法
http://blog.csdn.net/wmqi10/article/details/8730352
iOS重绘机制drawRect
http://blog.csdn.net/fww330666557/article/details/8647608
iOS CGContextRef画图小结
http://blog.sina.com.cn/s/blog_9693f61a0101deko.html
iOS使用CGContextRef绘制各种图形
http://www.devstore.cn/essay/essayInfo/116.html
(good)IOS用CGContextRef画各种图形(文字、圆、直线、弧线、矩形、扇形、椭圆、三角形、圆角矩形、贝塞尔曲线、图片)
http://blog.csdn.net/rhljiayou/article/details/9919713
CGContextDrawImage画图图形相反 镜像
http://blog.csdn.net/koupoo/article/details/8670024
drawRect:和 - (void)setNeedsDisplay 的一些理解
http://www.cnblogs.com/pengyingh/articles/2383688.html
iOS画图 以及清空
http://blog.csdn.net/woshidaniu/article/details/46683409
drawRect自动清除屏幕
http://www.cocoachina.com/bbs/read.php?tid=7085
主题 : drawRect中,如何清除之前所绘制的内容
http://www.cocoachina.com/bbs/read.php?tid=28302&page=e&#a
drawRect中,如何清除之前所绘制的内容
http://www.cocoachina.com/bbs/read.php?tid=28302
5 事件分发机制
iOS中的事件大概分为三种,分别是Milti-Touch Events, Motion Events和RemoteControl Events(events for controlling multimedia)。
5.1 hitTest
参考文档:iOS事件分发机制(一)hit-Testing
http://suenblog.duapp.com/blog/100031/iOS事件分发机制(一)%20hit-Testing
5.1.1 事件检测原理
每当我们点击了一下iOS设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前active的app(官方原文说:Then it places the event object in the active app’s event queue.)
告知当前活动的app有事件之后,UIApplication单例就会从事件队列中去取最新的事件,然后分发给能够处理该事件的对象。UIApplication获取到Event之后,Application就纠结于到底要把这个事件传递给谁,这时候就要依靠HitTest来决定了。
iOS中,hit-Testing的作用就是找出这个触摸点下面的View是什么,HitTest会检测这个点击的点是不是发生在这个View上,如果是的话,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果整了一圈没找到能够处理的view,则返回自身。来一个简单的图说明一下:
假设我们现在点击到了图中的E,hit-testing将进行如下步骤的检测(不包含重写hit-test并且返回非默认View的情况):
1、触摸点在ViewA内,所以检查ViewA的Subview B、C;
2、触摸点不在ViewB内,触摸点在ViewC内部,所以检查ViewC的Subview D、E;
3、触摸点不在ViewD内,触摸点发生在ViewE内部,并且ViewE没有subview,所以ViewE属于ViewA中包含这个点的最小单位,所以ViewE变成了该次触摸事件的hit-Test View;
5.1.2 注意点
1、默认的hit-testing顺序是按照UIView中Subviews的逆顺序;
2、如果View的同级别Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil, 再检查底部的Subview;
3、Hit-Test也是比较聪明的,检测过程中有这么一点,就是说如果点击没有发生在某View中,那么该事件就不可能发生在View的Subview中,所以检测过程中发现该事件不在ViewB内,也直接就不会检测在不在ViewF内。也就是说,如果你的Subview设置了clipsToBounds=NO,实际显示区域可能超出了superView的frame,你点击超出的部分,是不会处理你的事件的,就是这么任性!
5.1.3 事件检测实现
Hit-Test的检查机制如上所示,当确定了Hit-TestView时,如果当前的application没有忽略触摸事件(UIApplication:isIgnoringInteractionEvents),则application就会去分发事件(sendEvent:->keywindow:sendEvent:)。
UIView中提供两个方法用来确定hit-testingView,如下所示
- (UIView*) hitTest: (CGPoint)point withEvent: (UIEvent*)event;
// recursively calls -pointInside:withEvent:. point is inthe receiver's coordinate system
-(BOOL)pointInside: (CGPoint)point withEvent: (UIEvent *)event;
// default returns YES if point is in bounds
当一个View收到hitTest消息时,会调用自己的pointInside:withEvent:方法,如果pointInside返回YES,则表明触摸事件发生在我自己内部,则会遍历自己的所有Subview去寻找最小单位(没有任何子view)的UIView,如果当前View.userInteractionEnabled= NO, enabled=NO(UIControl),或者alpha<=0.01, hidden等情况的时候,hitTest就不会调用自己的pointInside了,直接返回nil,然后系统就回去遍历兄弟节点。简而言之,可以写成这样
[st_hitTest: withEvent:]
- (UIView*) hitTest: (CGPoint)point withEvent: (UIEvent*)event{
if (self.alpha<=0.01 || !self.userInteractionEnabled || self.hidden) {
return nil;
}
BOOL inside = [self pointInside: point withEvent: event];
UIView *hitView = nil;
if (inside) {
NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator];
for(UIView*subview in enumerator) {
hitView = [subview hitTest: point withEvent: event];
if (hitView) {
break;
}
}
if(!hitView) {
hitView = self;
}
return hitView;
}
else{
return nil;
}
}
hit-Test是事件分发的第一步,就算你的app忽略了事件,也会发生hit-Test。确定了hit-TestView之后,才会开始进行下一步的事件分发。
我们可以利用hit-Test做一些事情,比如我们点击了ViewA,我们想让ViewB响应,这个时候,我们只需要重写View's hitTest方法,返回ViewB就可以了,虽然可能用不到,但是偶尔还是会用到的。大概代码如下:
[STPView]
@interface STPView: UIView
@end
@implementation STPView
- (instancetype) initWithFrame: (CGRect)frame{
self = [super initWithFrame: frame];
if (self) {
UIButton *button = [UIButton buttonWithType: UIButtonTypeCustom];
button.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame)/2);
button.tag = 10001;
button.backgroundColor = [UIColor grayColor];
[button setTitle: @"Button1" forState: UIControlStateNormal];
[self addSubview: button];
[button addTarget: self action: @selector(_buttonActionFired:) forControlEvents: UIControlEventTouchDown];
UIButton *button2 = [UIButton buttonWithType: UIButtonTypeCustom];
button2.frame = CGRectMake(0, CGRectGetHeight(frame)/2, CGRectGetWidth(frame), CGRectGetHeight(frame)/2);
button2.tag = 10002;
button2.backgroundColor = [UIColor darkGrayColor];
[button2 setTitle: @"Button2" forState: UIControlStateNormal];
[self addSubview: button2];
[button2 addTarget: self action: @selector(_buttonActionFired:) forControlEvents: UIControlEventTouchDown];
}
return self;
}
- (void) _buttonActionFired: (UIButton*)button{
NSLog(@"=====Button Titled %@ ActionFired ", [button titleForState:UIControlStateNormal]);
}
- (UIView*)hitTest: (CGPoint)point withEvent: (UIEvent*)event{
UIView *hitView = [super hitTest: point withEvent: event];
if (hitView == [self viewWithTag: 10001]) {
return [self viewWithTag: 10002];
}
return hitView;
}
@end
5.1.4 利用catalog实现hitTest
来自STKit,这个category的目的就是方便的编写hitTest方法,由于hitTest方法是override,而不是delegate,所以使用默认的实现方式就比较麻烦。Category如下
[UIView+HitTest.h]
/**
* @abstract hitTestBlock
*
* @param其余参数 参考UIView
hitTest:withEvent:
* @param returnSuper是否返回Super的值。
*如果*returnSuper=YES,则代表会返回 super hitTest:withEvent:, 否则则按照block的返回值(即使是nil)
*
* @discussion切记,千万不要在这个block中调用self
hitTest:withPoint,否则则会造成递归调用。
*这个方法就是hitTest:withEvent的一个代替。
*/
typedef UIView *(^STHitTestViewBlock)(CGPoint point, UIEvent*event, BOOL*returnSuper);
typedef BOOL(^STPointInsideBlock)(CGPoint point, UIEvent*event, BOOL*returnSuper);
@interface UIView (STHitTest)
/// althought this is strong ,but i deal it with copy
@property (nonatomic, strong) STHitTestViewBlock hitTestBlock;
@property (nonatomic, strong) STPointInsideBlock pointInsideBlock;
@end
[UIView+HitTest.m]
@implementation UIView (STHitTest)
const staticNSString *STHitTestViewBlockKey = @"STHitTestViewBlockKey";
const staticNSString *STPointInsideBlockKey = @"STPointInsideBlockKey";
+ (void) load{
method_exchangeImplementations(class_getInstanceMethod(self, @selector(hitTest:withEvent:)), class_getInstanceMethod(self, @selector(st_hitTest:withEvent:)));
method_exchangeImplementations(class_getInstanceMethod(self, @selector(pointInside:withEvent:)),class_getInstanceMethod(self, @selector(st_pointInside:withEvent:)));
}
- (UIView*)st_hitTest:(CGPoint)point withEvent:(UIEvent*)event{
NSMutableString *spaces = [NSMutableString stringWithCapacity: 20];
UIView *superView = self.superview;
while (superView) {
[spaces appendString: @"----"];
superView = superView.superview;
}
NSLog(@"%@%@:[hitTest:withEvent:]", spaces, NSStringFromClass(self.class));
UIView *deliveredView = nil;
// 如果有hitTestBlock的实现,则调用block
if (self.hitTestBlock) {
BOOL returnSuper = NO;
deliveredView = self.hitTestBlock(point, event, &returnSuper);
if (returnSuper) {
deliveredView = [self st_hitTest: point withEvent: event];
}
}else{
deliveredView = [self st_hitTest: point withEvent: event];
}
// NSLog(@"%@%@:[hitTest:withEvent:] Result:%@", spaces, NSStringFromClass(self.class), NSStringFromClass(deliveredView.class));
return deliveredView;
}
- (BOOL) st_pointInside: (CGPoint)point withEvent: (UIEvent*)event{
NSMutableString *spaces = [NSMutableString stringWithCapacity: 20];
UIView *superView = self.superview;
while (superView) {
[spaces appendString: @"----"];
superView = superView.superview;
}
NSLog(@"%@%@:[pointInside:withEvent:]", spaces, NSStringFromClass(self.class));
BOOL pointInside = NO;
if (self.pointInsideBlock) {
BOOL returnSuper = NO;
pointInside = self.pointInsideBlock(point, event, &returnSuper);
if (returnSuper) {
pointInside = [self st_pointInside: point withEvent: event];
}
}
else{
pointInside = [self st_pointInside: point withEvent: event];
}
return pointInside;
}
- (void) setHitTestBlock: (STHitTestViewBlock)hitTestBlock{
objc_setAssociatedObject(self, (__bridge const void*)(STHitTestViewBlockKey),
hitTestBlock, OBJC_ASSOCIATION_COPY);
}
- (STHitTestViewBlock) hitTestBlock{
return objc_getAssociatedObject(self, (__bridge const void*)(STHitTestViewBlockKey));
}
- (void)setPointInsideBlock: (STPointInsideBlock)pointInsideBlock{
objc_setAssociatedObject(self, (__bridge const void*)(STPointInsideBlockKey),
pointInsideBlock, OBJC_ASSOCIATION_COPY);
}
- (STPointInsideBlock) pointInsideBlock{
return objc_getAssociatedObject(self, (__bridge const void*)(STPointInsideBlockKey));
}
@end
代码很简单,就是利用iOS的runtime能力,在hitTest执行之前,插入了一个方法。如果有看不懂的,可以参考我以前的博客 iOS面向切面编程
5.2 Responder Chain
参考文档:
iOS事件分发机制(二)The Responder Chain
http://suenblog.duapp.com/blog/100032/iOS事件分发机制(二)The%20Responder%20Chain
5.2.1 事件传递原理
响应链简单来说,就是一系列的相互关联的对象,从firstResponder开始,到application对象结束,如果firstResponder无法响应事件,则交给nextResponder来处理,直到结束为止。iOS中很多类型的事件分发,都依赖于响应链;在响应链中,所有对象的基类都是UIResponder,也就是说所有能响应事件的类都是UIResponder的子类,UIApplication/UIView/ UIViewController都是UIResponder的子类,这说明所有的Views,绝大部分Controllers(不用来管理View的Controller除外)都可以响应事件。
PS:CALayer不是UIResponder的子类,这说明CALayer无法响应事件,这也是UIView和CALayer的重要区别之一。
如果找到的hitTestView无法处理这个事件,事件就通过响应链往上传递(hitTestView算是最早的Responder),直到找到一个可以处理的Responder为止。
举个例子,如果触摸通过hitTest确定的是一个View,而这个View没有处理事件,则事件会发送给nextResponder去处理,通常是superView,有关nextResponder的事件传递过程,官方给出了一张很形象的图,如下所示:
PS:View处理事件的方式有手势或者重写touchesEvent方法或者利用系统封装好的组件(UIControls)。
图中所表示的正是nextResponder的查找过程,两种方式分别对应两种app的架构,左边的那种app架构比较简单,只有一个VC,右边的稍微复杂一些,但是寻找路线的原则是一样的,先解释一下,UIResponder本身是不会去存储或者设置nextResponder的,所谓的nextResponder都是子类去实现的(这里说的是UIView,UIViewController,UIApplication),关于nextResponder的值总结如下:
1、UIView的nextResponder是直接管理它的UIViewController(也就是VC.view.nextResponder=VC),如果当前View不是ViewController直接管理的View,则nextResponder是它的superView(view.nextResponder= view.superView)
2、UIViewController的nextResponder是它直接管理的View的superView (VC. nextResponder = VC.view.superView)
3、UIWindow的nextResponder是UIApplication
4、UIApplication的nextResponder是UIApplicationDelegate(官方文档说是nil)
我写了一段代码,打印当前UIResponder的所有nextResponder,大家可以拿去试一下,代码很简单,如下:
[STLogResponderChain]
void STLogResponderChain (UIResponder*responder) {
NSLog(@"------------------The Responder Chain------------------");
NSMutableString *spaces = [NSMutableString stringWithCapacity: 4];
while (responder) {
NSLog(@"%@%@", spaces, responder.class);
responder = responder.nextResponder;
[spaces appendString: @"----"];
}
}
然后我测试了一下,打印的日志如下图所示:
[Log]
UIButton
----STPView
--------UIView
------------STPFeedViewController
----------------UIView
--------------------UIView
------------------------_STWrapperViewController
----------------------------UIView
--------------------------------UIView
------------------------------------STNavigationController
----------------------------------------STPWindow
--------------------------------------------UIApplication
------------------------------------------------STPAppDelegate
这样比较清晰,大家也会直观的看到nextResponder的查找过程。
5.2.2 使用示例
接下来我们说正事了,假定我们现在有一个View是hitTestView,命名为STImageView,现在我们想让这个image处理一些事情,比如所有的图片点下之后加一个灰色的效果,我们就把事件分发给它。
在UIResponder中,提供以下几个方法,几个方法分别表示点击的不同状态,大家看名字就能明白差不多:
- (void)touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event;
- (void) touchesMoved: (NSSet *)touches withEvent: (UIEvent *)event;
- (void) touchesEnded: (NSSet *)touches withEvent: (UIEvent *)event;
- (void) touchesCancelled: (NSSet *)touches withEvent: (UIEvent *)event;
如果我们想让我们当前的Responder处理事件,我们则需要重写如下的几个方法。我们的需求是手指按下图片的时候加一个灰色的效果,松开的时候灰色消失。关于灰色的实现,我们暂定用一个View贴在ImageView上named maskView,然后用hidden来控制是否显示(上一篇文章有说过,所有hidden的View默认不接受任何事件)。
我们需要在touchesBegan方法里面self.maskView.hidden = NO;然后在 touchesEnded/Cancelled里面self.maskView.hidden = YES;就可以实现我们的效果了,原理很简单,我们的hitTestView在事件分发的时候去处理事件,仅此而已。这里注意一下:UIImageView的默认是不接受点击事件的,如果想要实现如上所示效果,需要设置userInteractionEnabled=YES;
说到这里,就有人产生了疑问,如果这么实现的话,那如果本身UIImageView还想让下面的View处理事件该怎么办?会不会把所有的事件拦截下来?这里就说到了另一个问题,UIResponder在知道需要处理事件的时候,还是有决定权的,比如我可以决定让整个响应链继续走下去,或者直接中断掉整个响应链。如果中断了响应链,那么所有在链上的nextResponder都不会得知有事件发生,iOS也提供了这个方法,其实很简单:
我们在重写TouchesEvents的时候,如果不想让响应链继续传递,就不调用super对应的实现就可以了,相反,有些时候你只需要做一个小改变,如上所示,但是你不想中断响应链,你就需要调用父类对应的实现。
这里有一点需要注意,一般来说,我们如果想要自己处理一些事件,我们需要重写如上所示的方法,如果我们想自己处理,就不需要调用super。调用super的目的就是为了把事件传递给nextResponder,并且如果我们在touchesBegan中没有调用super,则super不会响应其他的回掉(touchesMoved/touchesEnded),但是我们需要重写所有如上所示的方法来确保我们的一切正常。touchesBegan和touchesEnded/touchesCancelled一定是成对出现的,这点大家可以放心。
有关触摸事件在响应链上的分发,就差不多这么多东西,最重要的是大家可以看那几个touches方法,多做实验,就可以了解的更加深入。
5.2.3 其他要点
这里有一些补充,响应链能够处理很多东西,不仅仅是触摸事件。一般来说,如果我们需要一个对象去处理一个非触摸事件(摇一摇,RemoteControlEvents,调用系统的复制、粘贴框等),我们要确保该对象是UIResponder子类,如果我们要接收到事件的话,我们需要做两件事情:
1、重写canBecomeFirstResponder,并且返回YES
2、在需要的时候像该对象发送becomeFirstResponder消息。
我们有时候会遇到一些问题,比如我们重写了motionEvents,但是我们不能收到摇一摇的回调,或者我们的UIMenuController老是不弹出,我们就需要检查一下,我们是否满足了如上所示的条件,而且要确保becomeFirstResponder的发送时机正确。
当然,这个补充对于触摸事件无效,触摸事件的第一响应者是根据hitTest确定而来的,有点绕,需要仔细捋捋。
需要注意的是:
如果你自己想自定义一个非TouchEvent的事件,当需要继续传递事件的话,切记不要在实现内直接显示的调用nextResponder的对应方法,而是直接调用super对应的方法来让这个事件继续分发到响应链。到目前为止,事件的分发还没有结束,之后会有一篇文章介绍一个很重要的角色,手势。
最后,附上官方的文档
Guide for iOS
6 UIView开发技巧
6.1 常用技巧
6.1.1 使用半透明View与不透明SubView
半透明背景视图只能用此种方法设置颜色,否则subView也是半透明的。
blurView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.3];
6.1.2 [superlayoutSubviews]要发到layoutSubviews方法末尾位置
在自定义子View中使用layoutSubviews时应注意,[super layoutSubviews];最好放在方法默认最后执行,不然IOS7下面可能引起挂机。
6.1.3 内容自适应属性UIViewContentMode
UIImageView 的contentMode这个属性是用来设置图片的显示方式,如居中、居右,是否缩放等,有以下几个常量可供设定:
UIViewContentModeScaleToFill
UIViewContentModeScaleAspectFit
UIViewContentModeScaleAspectFill
UIViewContentModeRedraw
UIViewContentModeCenter
UIViewContentModeTop
UIViewContentModeBottom
UIViewContentModeLeft
UIViewContentModeRight
UIViewContentModeTopLeft
UIViewContentModeTopRight
UIViewContentModeBottomLeft
UIViewContentModeBottomRight
注意以上几个常量,凡是没有带Scale的,当图片尺寸超过 ImageView尺寸时,只有部分显示在ImageView中。UIViewContentModeScaleToFill属性会导致图片变形。UIViewContentModeScaleAspectFit会保证图片比例不变,而且全部显示在ImageView中,这意味着ImageView会有部分空白。UIViewContentModeScaleAspectFill也会证图片比例不变,但是是填充整个ImageView的,可能只有部分图片显示出来。
6.1.4 hitTest方法以及不规则区域内触摸事件处理方法
6.1.4.1 hitTest:withEvent:方法流程
iOS系统检测到手指触摸(Touch)操作时会将其放入当前活动Application的事件队列,UIApplication会从事件队列中取出触摸事件并传递给key window(当前接收用户事件的窗口)处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view。
window对象会在首先在view hierarchy的顶级view上调用hitTest:withEvent:,此方法会在视图层级结构中的每个视图上调用pointInside:withEvent:,如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是hit-test view。
hitTest:withEvent:方法的处理流程如下:
• 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
• 若返回NO,则hitTest:withEvent:返回nil;
• 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
• 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
• 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操(userInteractionEnabled=YES) 的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。
对于每个触摸操作都会有一个UITouch对象,UITouch对象用来表示一个触摸操作,即一个手指在屏幕上按下、移动、离开的整个过程。UITouch对象在触摸操作的过程中在不断变化,所以在使用UITouch对象时,不能直接retain,而需要使用其他手段存储UITouch的内部信息。UITouch对象有一个view属性,表示此触摸操作初始发生所在的视图,即上面检测到的hit-test view,此属性在UITouch的生命周期不再改变,即使触摸操作后续移动到其他视图之上。
【原】ios的hitTest方法以及不规则区域内触摸事件处理方法
http://www.cnblogs.com/wengzilin/p/4249847.html
hitTest:withEvent:方法流程
http://blog.csdn.net/jiajiayouba/article/details/23447145
6.1.4.2 使用hitTest自定义响应事件
the responder chain
在此例子中button,scrollview同为topView的子视图,但scrollview覆盖在button之上,这样在在button上的触摸操作返回的hit-test view为scrollview,button无法响应,可以修改topView的hitTest:withEvent:方法如下:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = [super hitTest:pointwithEvent:event];
CGPoint buttonPoint = [underButtonconvertPoint:point fromView:self];
if ([underButton pointInside:buttonPointwithEvent:event]) {
return underButton;
}
return result;
}
这样如果触摸点在button的范围内,返回hittestView为button,从button按钮可以响应点击事件。
2、Paging-enabled UIScrollView with Previews
关于这两个例子,可以看之前文章的说明,见Paging-enabled
6.1.5 通过UIView对象获取其所属UIViewController
通过UIView对象获取该对象所属的UIViewController可以使用UIResponder的nextResponder方法获得,UIView类继承于UIResponder,因此可以直接使用。
根据文档描述,如果View有view controller,则通过nextResponder方法返回,如果没有则返回superview。
下面是英文原文:
if the view has a view controller, it is returned by nextResponder.
If there is no view controller, the method will return the superview
相关代码如下:遍历该View的树形结构,获取到其所属的ViewController
- (UIViewController*)viewController {
for(UIView* next = [self superview]; next; next = next.superview) {
UIResponder* nextResponder = [next nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]]) {
return (UIViewController*)nextResponder;
}
}
return nil;
}
6.1.6 坐标体系转换
// 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView*)view;
// 将像素point从view中转换到当前视图中,返回在当前视图中的像素值
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView*)view;
// 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect
- (CGRect)convertRect:(CGRect)rect toView:(UIView*)view;
// 将rect从view中转换到当前视图中,返回在当前视图中的rect
- (CGRect)convertRect:(CGRect)rect fromView:(UIView*)view;
例把UITableViewCell中的subview(btn)的frame转换到 controllerA中
// controllerA 中有一个UITableView, UITableView里有多行UITableVieCell,cell上放有一个button
// 在controllerA中实现:
CGRect rc = [cell convertRect:cell.btn.frame toView:self.view];
或
CGRect rc = [self.view convertRect:cell.btn.frame fromView:cell];
// 此rc为btn在controllerA中的rect
或当已知btn时:
CGRect rc = [btn.superview convertRect:btn.frame toView:self.view];
或 CGRect rc = [self.view convertRect:btn.frame fromView:btn.superview];
7 参考链接
iOS开发UI篇—UIWindow简单介绍
http://www.cnblogs.com/wendingding/p/3770052.html
iOS动画总结----UIView动画
http://blog.csdn.net/huifeidexin_1/article/details/7597868
UIView动画(过渡效果)的学习笔记
http://www.cnblogs.com/lovecode/archive/2011/11/05/2237077.html
动画UIView animateWithDuration使用详解
http://blog.163.com/wzi_xiang/blog/static/65982961201211104227946/
UIView动画效果的四种调用方式
http://www.cnblogs.com/sell/archive/2013/02/09/2909522.html
(Good)iOS事件分发机制(一)hit-Testing
http://suenblog.duapp.com/blog/100031/iOS事件分发机制(一)%20hit-Testing
(Good)iOS事件分发机制(二)The Responder Chain
http://suenblog.duapp.com/blog/100032/iOS事件分发机制(二)The%20Responder%20Chain
hitTest:withEvent:方法流程
http://blog.csdn.net/jiajiayouba/article/details/23447145
iOS的UIView的hitTest的分析
http://blog.csdn.net/sanjunsheng/article/details/25080797
[IOS]hitTest的作用与用法【转】
http://blog.csdn.net/smking/article/details/9791365
iOS开发笔记--UIView中的坐标转换
http://blog.csdn.net/hopedark/article/details/18215083
IOS--UIView中的坐标转换
http://blog.sina.com.cn/s/blog_a573f7990102vgui.html
Responder Chain(ios事件传递)
http://blog.csdn.net/yongyinmg/article/details/19616527
UIView中常见的方法总结
http://www.cnblogs.com/pengyingh/articles/2379476.html
iOS应用获取最上层全屏Window的正确方法