UIKit 视图树模型
1、视图树模型
计算机图形实际上是一个视图树模型,每个视图都有一个本地坐标系。
每个本地坐标系的组成部分是:原点在父坐标系中的位置,每个基在父坐标系中的位置,由此就可以根据向量的本地位置求出相对于父坐标系的位置,最终求出向量全局位置。
我们要分清全局坐标系,父坐标系,本地坐标系三种概念,分清基、向量的坐标、向量的位置三种概念。
2、控件监听事件
观察者模式在这里得到充分体现。
UIView和UIControll都可以监听用户事件,但是UIView实现监听事件要通过在子类中覆写- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event等方法,或者借助于UIGestureRecognizer。而UIControll类则为线程提供了一些方便的接口,比如addTarget方法。
监听普通事件:
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
监听手势事件:
UITapGestureRecognizer响应顺序是怎么样的
一个scrollview上有几个按钮
在scrollview上add 了一个单击事件
singletap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
[singletap setNumberOfTapsRequired:1];
[scrollview addGestureRecognizer:singletap];
这样点击按钮,不会响应按钮的事件,而会直接跳到handleSingleTap去了,原因在于单击事件应该先被直接单击的superview处理,如果没有处理才传递到subview去处理。
怎么才能让按钮响应单击事件?
使用UIGestureRecognizerDelegate的一个方法判断点击的是哪个view,确定是否响应事件。
singletap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; [singletap setNumberOfTapsRequired:1]; singletap.delegate = self; [scrollview addGestureRecognizer:singletap]; //让scrollview而不是上面的button来相应触摸事件 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if(touch.view != scrollview){ return NO; }else return YES; }
再一次举例
UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; [self.view addGestureRecognizer:singleFingerTap]; [singleFingerTap release]; - (void)handleSingleTap:(UITapGestureRecognizer *)recognizer { CGPoint location = [recognizer locationInView:[recognizer.view superview]]; //Do stuff here... }
3、添加视图
[[[UIApplication sharedApplication] keyWindow] addSubview:myNewView];
怎样让视图显示在最外层?这样就把视图显示在最外层,一个类似action sheet或者alert view的效果就是在window上添加一层灰色透明视图作为根节点,再添加想要的视图。
尝尝使用这个方法覆盖一个HUD风格的透明图形
becomeKeyWindow 和 – resignKeyWindow ,线程调用这两个方法,控制一个window实例是否成为用于转发用户消息的那个窗口
Core Animation 动画
1、实现动画
对于不需要和用户交互的动画,要实现动画只需要调用UIView的一个类方法即可,不必涉及CALayer和用户触摸事件
UIViewAnimationOptions options = UIViewAnimationCurveLinear | UIViewAnimationOptionAllowUserInteraction; [UIView animateWithDuration:0.2 delay:0.0 options:options animations:^ { highlightView.alpha = 1.0; } completion: ^(BOOL finished){
// 动画结束时的处理
}];
但是对于交互类型的动画,则需要手势识别器,实现视图跟随触摸点移动,或者覆写需要覆写touchesBegan&touchesMoved:配合Core Graphics中的各种函数实现自定义绘图。
2、CALayer树
线程会利用CALayer类(或者其子类)封装的代码来进行界面绘图。UIView继承自UIResponder,主要特点是可以告诉线程如何响应触摸事件,而CALayer是告诉线程如何进行实际的绘图。
UIView的实例中维护着一个CALayer实例的树模型,相当于CALayer树的管理器,访问UIView的跟绘图和跟坐标有关的属性,例如frame,bounds等等,实际上内部都是在访问它所包含的CALayer的相关属性。
UIView有个layer属性,可以返回它的主CALayer实例,UIView有一个layerClass方法,返回主layer所使用的类,UIView的子类,可以通过重载这个方法,来让UIView使用不同的CALayer来显示
- (class) layerClass { return ([CAEAGLLayer class]); }
UIView的CALayer类似UIView的子View树形结构,也可以向它的layer上添加子layer,来完成某些特殊的表示。例如下面的代码会在目标View上敷上一层黑色的透明薄膜。
grayCover = [[CALayer alloc] init]; grayCover.backgroundColor = [[[UIColor blackColor] colorWithAlphaComponent:0.2] CGColor]; [self.layer addSubLayer: grayCover];
UIView的layer树形在系统内部,被系统维护着三份copy
第一份,逻辑树,就是代码里可以操纵的,例如更改layer的属性等等就在这一份。
第二份,动画树,这是一个中间层,系统正在这一层上更改属性,进行各种渲染操作。
第三份,显示树,这棵树的内容是当前正被显示在屏幕上的内容。
这三棵树的逻辑结构都是一样的,区别只有各自的属性。
3、CATrainsition
一个CATrainsition类的实例可以引用一个动画过程。可以通过设置CATrainsition实例的属性,定义将要如何进行动画。
CATransition是CAAnimation的子类,通过静态方法animation获取可以引用动画过程的实例。
自定义在window上添加模态视图的全翻页效果:
CATransition *animation = [CATransition animation]; animation.duration = 1.0; animation.timingFunction = UIViewAnimationCurveEaseInOut; animation.type = @"pageCurl"; //animation.type = kCATransitionPush; animation.subtype = kCATransitionFromLeft; [self.view.window.layer addAnimation:animation forKey:nil]; [self presentViewController:myNextViewController animated:NO completion:nil];
修改导航控制器推入动画
CATransition *transition = [CATransition animation]; transition.duration = 1; transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; transition.type = kCATransitionPush; transition.subtype = kCATransitionFromTop; transition.delegate = self; [self.navigationController.view.layer addAnimation:transition forKey:nil]; self.navigationController.navigationBarHidden = NO; [self.navigationController pushViewController:viewController animated:NO];
修改导航控制器弹出的动画
CATransition *transition = [CATransition animation]; transition.duration =0.4; transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; transition.type = kCATransitionReveal; //transition.subtype = kCATransitionFromBottom; transition.delegate = self; [self.navigationController.view.layer addAnimation:transition forKey:nil]; self.navigationController.navigationBarHidden = NO; [self.navigationController popViewControllerAnimated:NO];
Core Graphics 绘图
iPhone的绘图操作是在UIView类的drawRect方法中完成的,所以如果我们要想在一个UIView中绘图,需要写一个扩展UIView 的类,并重写drawRect方法,在这里进行绘图操作,线程会自动调用此方法进行绘图。
比如,你想绘制一个方块,你需要写一个类来扩展UIView并在drawRect方法中填入如下代码:
- (void)drawRect:(CGRect)rect { // Drawing code. //获得处理的上下文 CGContextRef context = UIGraphicsGetCurrentContext(); //设置线条样式 CGContextSetLineCap(context, kCGLineCapSquare); //设置线条粗细宽度 CGContextSetLineWidth(context, 1.0); //设置颜色 CGContextSetRGBStrokeColor(context, 1.0, 0.0, 0.0, 1.0); //开始一个起始路径 CGContextBeginPath(context); //起始点设置为(0,0):注意这是上下文对应区域中的相对坐标, CGContextMoveToPoint(context, 0, 0); //设置下一个坐标点 CGContextAddLineToPoint(context, 100, 100); //设置下一个坐标点 CGContextAddLineToPoint(context, 0, 150); //设置下一个坐标点 CGContextAddLineToPoint(context, 50, 180); //连接上面定义的坐标点 CGContextStrokePath(context); }
不过有时不会直接调用在context上添加线段的addLine方法,逐点填写绝对坐标,而是先获得上下文context,在定义一个路径path,调用addLine方法定义好一个path的全部位置,然后把路径添加到上下文里
class MyView: UIView { var _path = CGPathCreateMutable() override func drawRect(rect: CGRect) { UIColor.redColor().set() var context = UIGraphicsGetCurrentContext() CGContextSetLineWidth(context, 5) CGContextAddPath(context, _path) CGContextStrokePath(context) } override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) { let p = (touches as NSSet).anyObject()?.locationInView(self) CGPathMoveToPoint(path, nil, p!.x, p!.y) } override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent) { let p = (touches as NSSet).anyObject()?.locationInView(self) CGPathAddLineToPoint(_path, nil, p!.x, p!.y) setNeedsDisplay() } func drawTwoLines(point:CGPoint, lineJoin:CGLineJoin) { let context = UIGraphicsGetCurrentContext() CGContextSetLineJoin(context, lineJoin) CGContextSetLineWidth(context, 5) CGContextMoveToPoint(context, point.x-100, point.y+50) CGContextAddLineToPoint(context, point.x, point.y) CGContextAddLineToPoint(context, point.x+100, point.y+50) CGContextStrokePath(context) } }
再说明一下重绘,重绘操作仍然在drawRect方法中完成,但是苹果不建议直接调用drawRect方法,如果你强直直接调用此方法,当然是没有效果的。在UIView中,重写drawRect: (CGRect) aRect方法可以自己定义想要画的图案,但是此方法一般情况下只会画一次,也就是说这个drawRect方法一般情况下只会被调用一次。drawRect是在Controller->loadView, Controller->viewDidLoad 两方法之后调用的.
苹果要求我们调用UIView类中的setNeedsDisplay方法,则程序会自动调用drawRect方法进行重绘,即[self setNeedsDisplay]方法即可。
注意
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。
2、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
3、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0.
4、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect ,让系统自动调该方法。
5、若使用calayer绘图,只能在drawInContext: 中绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
6、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕
Interface Builder
笔者倾向于使用纯代码方式来实现界面布局,这样更容易实现自定义UI控件和更容易维护修改。不过对于简单的界面,也可以使用苹果为我们提供的图形化的方式来构建。
1、如果通过加载nib文件的方式加载controller的视图,当线程调用 initWithNibName: 来初始化controller.view时,如果nibName可以找到,线程会根据nib文件初始化控制器,需要注意的是无论通过什么方式(除了story board)初始化控制器,最终都会调用UIViewController或者其子类中的initWithNibName方法,所以在这里初始化就够了。
2、如果通过story board生成控制器,那么会调用instantiateViewControllerWithIdentifier创建对象,然后用initWithCdoer初始化控制器。
- (id)initWithCoder:(NSCoder*)coder{ if (self =[super initWithcoder:coder]) { //初始化控制器的变量 } return self; }
3、使用interface builder实现控件监听事件,需要配合代码中的IBOutlet和IBAction关键字,并建立Interface Builder对象和代码的关联。
素材资源
要建立一个像那么回事儿的界面,不能不关注图片资源,虽然这看起来像是美工的活。
1、分辨率
iPhone4/4s:主屏幕像素:960x640,开发中屏幕尺寸为 480x320
iPhone5/5s:主屏幕像素:1136x640,开发中屏幕尺寸为 568x320
iPhone6/6s:主屏幕像素:1334x750,开发中屏幕尺寸为 667x375
iPhone6p:主屏幕像素:1920*1080,开发中屏幕尺寸为 736x414
一定要注意开发是的最小刻度值和并不等于实际像素的长度。
素材准备:
● Icon.png – 像素57×57 iPhone iOS 6应用图标
● Icon-120.png – 像素120 x 120 iPhone4显示屏 iOS 7应用图标
● Icon-Small.png – 像素29×29 iPhone 系统设置和搜索结果图标
● Icon-Small@2x.png – 像素58×58 iPhone Retina显示屏 系统设置和搜索结果图标
● Default.png 480x320 iPhone启动画面图片
● Default@2x.png 960x640 iPhone4、4s的启动画面图片
● Default-568h@2x.png 1136x640 iPhone5启动画面图片
详见
程序中需要注意bounds和nativeBounds的区别。iphone5的bounds.size.height是568.
实用界面设置
UIApplication(或者其子类)的对象主要告诉线程如何等待和处理事件。
这个类中定义了很多有用的代码,可供线程执行。
1.设置icon上的数字图标 [UIApplication sharedApplication].applicationIconBadgeNumber = 4; 2.设置摇动手势的时候,是否支持redo,undo操作 [UIApplication sharedApplication].applicationSupportsShakeToEdit =YES; 3.判断程序运行状态 if([UIApplication sharedApplication].applicationState ==UIApplicationStateInactive){ NSLog(@"程序在运行状态"); } 4.阻止屏幕变暗进入休眠状态 [UIApplication sharedApplication].idleTimerDisabled =YES; 慎重使用本功能,因为非常耗电。 5.显示联网状态 [UIApplication sharedApplication].networkActivityIndicatorVisible =YES; 6.在map上显示一个地址 NSString* addressText =@"1 Infinite Loop, Cupertino, CA 95014"; addressText = [addressText stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]; NSString* urlText = [NSString stringWithFormat:@"http://maps.google.com/maps?q=%@", addressText]; [[UIApplication sharedApplication]openURL:[NSURL URLWithString:urlText]]; 7.发送电子邮件 NSString *recipients =@"mailto:first@example.com?cc=second@example.com,third@example.com&subject=Hello from California!"; NSString *body =@"&body=It is raining in sunny California!"; NSString *email = [NSString stringWithFormat:@"%@%@", recipients, body]; email = [email stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; [[UIApplication sharedApplication]openURL:[NSURL URLWithString:email]]; 8.打电话到一个号码 [[UIApplication sharedApplication]openURL:[NSURL URLWithString:@"tel://8004664411"]]; 9.发送短信 [[UIApplication sharedApplication]openURL:[NSURL URLWithString:@"sms://466453"]]; 10.打开一个网址 [[UIApplication sharedApplication]openURL:[NSURL URLWithString:@"http://itunesconnect.apple.com"]]; AppDelegate类 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 当由于其它方法打开应用程序(如URL指定或者连接),通知委托启动完毕 - (void)applicationWillTerminate:(UIApplication *)application 通知委托,应用程序将在关闭 退出,请做一些清理工作。 - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application 通知委托,应用程序收到了为来自系统的内存不足警告。-(void)applicationSignificantTimeChange:(UIApplication *)application 通知委托系统时间发生改变(主要是指时间属性,而不是具体的时间值) 打开URL - (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url 打开指定的URL 控制状态栏方位变化 – application:willChangeStatusBarOrientation:duration: 设备方向将要发生改变 – application:didChangeStatusBarOrientation: 活动状态改变 - (void)applicationWillResignActive:(UIApplication *)application 通知委托应用程序将进入非活动状态,在此期间,应用程序不接收消息或事件。-(void)applicationDidBecomeActive:(UIApplication *)application 通知委托应用程序进入活动状态,请恢复数据 如果我们希望获得appdelegate实例,不必通过import类的方式,只需要: UIApplicationDelegate* delegate = [[UIApplication sharedApplication] delegate];
实用控件和方法
很多控件和方法都是直接在keywindow上添加视图实现,它们往往是以模态的方式呈现,适用于注册、登录等等场景。
1、UIViewController:
每个控制器实例都有下面两个方法,用于在keywindow上添加或删除一个模态视图,常用于登录界面、发微博界面等等。
– presentViewController:animated:completion: 弹出,出现一个新视图,可以带动画效果,完成后可以做相应的执行函数经常为nil
– dismissViewControllerAnimated:completion:退出一个新视图,可以带动画效果,完成后可以做相应的执行函数经常为nil
2、action sheet
actionSheet = [[UIActionSheet alloc] initWithTitle:title delegate:nil cancelButtonTitle:@"取消" destructiveButtonTitle:nil otherButtonTitles:@"相册", @"拍照",nil]; picker = [[UIPickerView alloc] initWithFrame:CGRectMake(0, 50, 320, 220)]; picker.delegate = self; picker.dataSource = self; picker.showsSelectionIndicator = YES; [actionSheet addSubview:picker]; [actionSheet showInView:self.view];
参数说明:
title:视图标题
delegate:设置代理
cancelButtonTitle:取消按钮的标题
destructiveButtonTitle:特殊标记的按钮的标题
otherButtonTitles:其他按钮的标题
动作回调方法为:
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex{ UIAlertView *alertView; switch (buttonIndex) { case 0: alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"点击相册事件" delegate:self cancelButtonTitle:nil otherButtonTitles: nil]; [alertView show]; break; case 1: alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"以点击拍照事件" delegate:self cancelButtonTitle:nil otherButtonTitles: nil]; [alertView show]; break; default: break; } }
3、alert view
在action sheet中,动作回调方法中就创建了alert view实例。