Cocoa Touch(三):图形界面UIKit、Core Animation、Core Graphics

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启动画面图片

详见 

https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/IconMatrix.html

程序中需要注意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实例。

原文地址:https://www.cnblogs.com/xinchrome/p/4898409.html