iOS 绘画学习

很多UIView的子类,例如UIButton或者UIlabel,都知道如何绘制自己;不过迟早,你都会想绘制一些自己想要的效果。你可以通过一些已有的类在代码中绘制一幅图片,然后在自己的界面上展示出来,例如UIImageVIew和UIButton。单纯一个UIView就是只与绘制有关,它给你了很大的空间来绘画;你的代码决定了这个视图怎么绘制自己,最终怎么在你界面上展示。

  UIImage和UIImageView

  iOS系统支持很多标准的图片格式:TIFF、JPEG、GIF、PNG等。当一张图片被包含在我们的app 包内,iOS系统特别地,会对PNG文件提供更加友好的支持,不只是因为系统会对它进行压缩处理,还有在不同分辨率下的对图片的选取和展示都做了很多工作,所以我们应该优先选择PNG格式图片。我们可以通过  imageNamed: 这个UIImage类提供的方法获取app包内的图片,这个方法会从两个地方寻找指定的图片:

  app包顶级目录

    系统会通过提供的图片名字,名字是大小写敏感的,以及包括图片的类型,在app包中寻找。如果没有提供类型,默认是png格式。

  Asset catalog 资源目录

    它会通过提供的名字,在这个资源目录中寻找匹配的图片集。如果名字带有文件后缀,就不会在这里查找,以便旧代  码中,如果把图片移动到这个目录仍然能够正常工作。这个目录的查找优先级比上面的查找高,也就意味着,如果在这个  资源目录下找到了匹配的图片,方法就会返回,而不会再去app包顶级目录中查找。

 


 

  可调整大小的Images

  可以通过向一个UIImage发送  resizableImageWithCapInsets:resizingMode: 消息,来把图片转换成可调整大小的图片。capInsets参数是一个UIEdgeInsets类型的结构体,由四个浮点型数字组成:top,left,bottom,right。它们代表着从图片边缘向内的距离。在一个比图片要大的上下文中,可调整大小的Image有两种工作模式,通过 resizingMode: value: 指定

  UIImageResizingModeTile

    在上面capInsets 指定的内部矩形区域会平铺在内部,每一个边缘由对应边的矩形区域平铺而成,而外面的四个角落的矩形不变。

  UIImageResizingModeStretch

    内部的矩形会被拉伸一次来填充内部,每个边缘由对应变的矩形区域拉伸而成,而外面的四个角落的矩形不变。

  例如:假设  self.iv  是一个有固定长宽的UIImageView,contentMode是UIViewContentModeScaleToFill。

(1)设置capInsets 为 UIEdgeInsetsZero

1
2
3
4
UIImage* mars = [UIImage imageNamed:@ "Mars" ];
UIImage* marsTiled = [mars resizableImageWithCapInsets: UIEdgeInsetsZero
                                                         resizingMode: UIImageResizingModeTile];
self .iv.image = marsTiled;

  

(2)

1
2
3
4
5
6
UIImage* marsTiled = [mars resizableImageWithCapInsets:
                          UIEdgeInsetsMake(mars.size.height/4.0,
                                           mars.size.width/4.0,
                                           mars.size.height/4.0,
                                           mars.size.width/4.0)
                          resizingMode: UIImageResizingModeTile];

  

(3)常用的拉伸策略是把几乎是原始图片的一半作为capinset,仅仅在中间留出1到2像素来填充整个内部。

1
2
3
4
5
6
UIImage* marsTiled = [mars resizableImageWithCapInsets:
                           UIEdgeInsetsMake(mars.size.height/2.0 - 1,
                                            mars.size.width/2.0 - 1,
                                            mars.size.height/2.0 - 1,
                                            mars.size.width/2.0 - 1)
                           resizingMode: UIImageResizingModeStretch];

  

 

在最新的Xcode5 中,我们可以不用代码来配置一个可调整大小的图片,仅仅通过Xcode5提供的一个 asset catalogs 功能,而不用多次编写同样的代码,这个功能仅在ios7.0以上版本可用。

 


 

  图片的渲染模式

  在ios应用界面的很多地方,会自动把图片当作透明遮罩,也称为模板。这样意味着会忽略图片的颜色,仅仅保留每个像素对应的透明度(alpha)。在屏幕上显示的图片就是单一的色调与图片透明度合成在一起的效果。例如标签栏按钮的图片或者在工具栏中类型为UIBarButtonItemSylePlain的按钮的图片,都是这种模式。
  在最新的ios7系统中,图片类添加了一个新的属性:renderingMode,表示图片渲染模式。这个属性是 只读的。为了改变图片这个属性,我们可以通过已有的图片以不同的渲染模式生成新的图片,调用这个方法:imageWithRendingMode:。渲染模式有三种,分别为:UIImageRenderingModeAlwaysOriginal,
UIImageRenderingModeAutomatic,
UIImageRenderingModeAlwaysTemplate 。
UIImageRenderingModeAutomatic
 
 
默认是UIImageRenderingModeAutomatic模式,也就是除了在上面所说的地方使用透明模板模式外,其他地方都是原样显示图片。有了这个渲染属性,我们可以强制图片按照通常的方式绘制,即使在一个通常用透明模板模式渲染图片的上下文中也可以,反之亦然。苹果公司希望iOS7应用在整个界面中使用更多的透明模板模式。下面是ios7系统设置应用中的例子:
 
 
  
  为了方便实现这种效果,iOS7给UIView添加了一个tintColor的属性,用来给图片包含的任意模板着色。而且,这个属性默认是从视图层次结构中继承下来的,贯穿整个应用,从UIWindow开始。此外,给你的应用主窗口分配一个tint color可能是你对主窗口为数不多的改变之一,否则你的应用将会采用系统的蓝色色调颜色(如果你使用storyboard故事板,可以在File inspector 文件检查器中修改这个tint color)。也可以为独立的视图设置它们自己的tint color,它们的子视图会继承该tint color。下面就是在一个窗口的tint color 为 红色的 应用中,两种同样的图片不同的展示,一个是通常的渲染模式,另外一个是模板模式:
 
 

   图形上下文
   UIImageView会为你绘制一张图片,并处理好所有的细节,很多情况下,这就是你所需要的。即使那样,你可能也会想直接用代码来绘制一些自己想要的东西,这时,你需要一个图形上下文。
  一个图形上下文通常来说就是你能够绘制的一块区域。相反地,你只能通过一个图形上下文来在代码中进行绘制。有多种方式来获得一个图形上下文,这里将介绍两种,这两种目前在我遇到的各种情况下用得最多:
   自己创建一个图片上下文
    UIGraphicsBeginImageContextWithOptions 函数生成一个适合用作图片的图形上下文。然后你可以在这个图形上下文中生成图片。当你完成了绘制,你可以调用UIGraphicsGetImageFromCurrentImageContext 来把当前的图形上下文转换成一个UImage,最后调用UIGraphicsEndImageContext来释放这个上下文。现在,你拥有了一个可以显示在你的界面中或者在其他上下文中绘制的或者是保存为一个文件的UIImage对象了。
   Cocoa给你一个图形上下文
    你可以子类化UIView,并实现drawRect:方法。在你实现的这个drawRect:方法被调用时,Cocoa已经为你创建了一个图形上下文,并叫你立刻使用它来绘制;不管你绘制什么,都会在UIView中显示出来。(这种情况的一个轻微的变种就是,你子类化CALayer,并实现drawInContext:方法,或者给layer图层委托一些对象,并实现drawLayer:inContext:方法,以后会再次讨论这个)。
  在任何给定的时刻,一个图形上下文要么是当前的图形上下文,要么不是:
  * UIGraphicsBeginImageContextWithOptions  不仅创建一个图片上下文,同时也会把这个上下文设置为当前的图形上下文。
  * 当drawRect:方法被调用时,UIView正在绘制的上下文就已经是当前的图形上下文了。
  * 以一个上下文为参数的回调,不会使任何的上下文为当前的图形上下文,相反,这个参数仅仅是一个图形上下文的引用。
  
  让初学者困惑的是有两个单独的工具集来绘制,它们在绘制时对图形上下文使用了不同的参数:
 
   UIKit
    很多Objective-C类知道如何绘制它们自己,包括UIImage,NSString(绘制文本),UIBezierPath(绘制图形)和UIColor。这些类中有些提供  了方便的方法和有限的功能;另一些则是功能非常强大。很多情况下,UIKit将是你所需要的全部。
 
    通过UIKit,你只能在当前的图形上下文中绘制。所以如果你是在使用UIGraphicsBeginImageContextWithOptions 或者drawRect:的情况下,那么你就可以直接使用UIKit提供的方便的方法;里面提供了一个当前的上下文,也是你想绘制的那个上下文。如果你已经持有了一个上下文参数,另一方面,你也想使用UIKit的方便方法,你将需要把这个上下文转变为当前的上下文,通过调用 UIGraphicsPushContext(记得在最后还原上下文,调用UIGraphicsPopContext)。
 
   Core Graphics
    这个是完整的绘图API。Core Graphics 通常称为Quartz,或者Quartz2D,是构成所有iOS绘画的绘画系统 ----UIKit的绘画就是构建在它之上的-----所以是包含了大量C函数的底层框架。这个小节将让你熟悉它的原理。为了获取更全面的信息,你可以学习苹果的Quartz 2D编程指南(Apple's Quartz 2D Programming Guide)。
 
    为了使用Core Graphics,你必须指定一个图形上下文来进行绘制,确切地说,是在每个函数调用中。但是在UIGraphicsBeginContextWithOptions或者drawRect:方法中,你没有一个上下文的引用;为了能够使用Core Graphics,你需要拿到这个引用。由于这个你想用来绘制的上下文就是当前的上下文,你可以调用 UIGraphicsGetCurrentContext来获得所需的引用。
 
  所以现在我们有两套工具集,对应的两种上下文又提供了三种方式,所以我们一共有六种方式绘画。下面我将一一说明这六种!你不需要担心实际的这些绘画命令,仅仅专注于怎么指定上下文以及我们是在使用UIKit还是Core Graphics。首先我将通过子类化UIView,并实现drawRect:方法来绘制一个蓝色圆形;使用UIKit已经为我提供的当前上下文来绘制:
  
1
2
3
4
5
6
- ( void ) drawRect: (CGRect) rect {
         UIBezierPath* p =
             [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
         [[UIColor blueColor] setFill];
         [p fill];
}

  现在我用Core Graphics 实现同样的效果;这样需要我首先拿到一个当前上下文的引用:

1
2
3
4
5
6
- ( void ) drawRect: (CGRect) rect {
         CGContextRef con = UIGraphicsGetCurrentContext();
         CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
         CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
         CGContextFillPath(con);
}

  接下来,我会在UIView子类中实现 drawLayer:inContext:。这种情况下,我们手中的上下文引用并不是当前上下文,所以我需要用UIKit把它转换成当前上下文:

1
2
3
4
5
6
7
8
- ( void )drawLayer:(CALayer*)lay inContext:(CGContextRef)con {
         UIGraphicsPushContext(con);
         UIBezierPath* p =
             [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
         [[UIColor blueColor] setFill];
         [p fill];
         UIGraphicsPopContext();
}

   为了在drawLayer:inContext:中使用Core Graphics,我仅仅需要简单地保留一个我持有的上下文即可:

1
2
3
4
5
- ( void )drawLayer:(CALayer*)lay inContext:(CGContextRef)con {
         CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
         CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
         CGContextFillPath(con);
}

  最后,为了完整性,让我们创建一个蓝色圆形的UIImage对象。我们可以在任何时间(我们不需要等待某些特定方法被调用)以及在任何类(我们不需要在UIView的子类)中创建。创建的UIImage你可以在任何地方正常使用,例如,你可以把它放到一个可见的UIImageView中当做图片展示,或者你可以把它保存在一个文件中,或者你可以在其他的绘制中使用(下一节介绍)。

 

首先,我使用UIKit绘制我的图片:

1
2
3
4
5
6
7
8
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO , 0);
     UIBezierPath* p =
         [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
     [[UIColor blueColor] setFill];
     [p fill];
     UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
     UIGraphicsEndImageContext();
     // im is the blue circle image, do something with it here ...

下面是使用Core Graphics实现的:

1
2
3
4
5
6
7
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO , 0); CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// im is the blue circle image, do something with it here ...

你可能会对UIGraphicsBeginImageContextWithOptions这个方法的参数感到疑惑,其实第一个参数显然是将要创建的图片的大小。第二个参数表明这个图片是否是不透明的,如果我在上面的方法中传递YES而是不是NO,我的图片将会有一个黑色背景,而我不想要这种效果。第三个参数指定图片的缩放比例,传递0是告诉系统根据当前屏幕的尺寸为我自动设置压缩比例,这样我的图片就会在单分辨率和双分辨率屏幕下都能完美显示。

 

你不必完全使用UIKit或者Core Graphics,相反地,你可以混合UIKit 调用和Core Graphics调用来操作同样的图形上下文。它们仅仅只是表示两种不同的方式对同样的图形上下文通信而已。


 

  CGImage绘画

  UIImage在Core Graphics中的版本是CGImage(实际上是CGImageRef)。它们可以很容易地互相转换:UIImage有一个CGImage的属性,可以访问它的Quartz 的图片数据,你也可以把CGImage 转换成UIImage,使用imageWithCGImage:或者initWithCGImage:(在实战中,你会更偏向使用更加可配置性的姐妹方法:imageWithCGImage:scale:orientation: 以及 initWithCGImage:scale:orientation:)。

  一个CGImage可以让你从一个原始图片的一个矩形区域中创建一个新的图片,而UIImage是做不到的。(一个CGImage还有其他强大的功能而UIImage没有的,例如你可以将图片的遮罩应用到CGImage中)。我将会通过分隔一张火星图片为两半,并分开单独绘制每一边。

 

注意,我们现在是在CFTypeRef范围下操作,必须自动管理好内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UIImage* mars = [UIImage imageNamed:@ "Mars" ];
// extract each half as a CGImage
CGSize sz = mars.size;
CGImageRef marsLeft = CGImageCreateWithImageInRect([mars CGImage],
                        CGRectMake(0,0,sz.width/2.0,sz.height));
CGImageRef marsRight = CGImageCreateWithImageInRect([mars CGImage],
                         CGRectMake(sz.width/2.0,0,sz.width/2.0,sz.height));
// draw each CGImage into an image context
UIGraphicsBeginImageContextWithOptions(
     CGSizeMake(sz.width*1.5, sz.height), NO , 0);
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextDrawImage(con,
                    CGRectMake(0,0,sz.width/2.0,sz.height), marsLeft);
CGContextDrawImage(con,
                    CGRectMake(sz.width,0,sz.width/2.0,sz.height), marsRight);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(marsLeft); CGImageRelease(marsRight)

但是这里的例子有个问题:绘制的东西上下颠倒了! 它不是被旋转了,而是从上到下映射,或者用专业的术语,翻转。这种想象会发生在你创建了一个CGImage,然后通过CGContextDrawImage绘制时,是由于源和目标上下文的本地坐标系统不匹配。

有多种的方式补偿这种不同坐标系统之间的不匹配。其中一种就是把CGImage绘制成一个中间的UIImage,然后从UIImage中获取CGImage,下面展示一个通用的函数来实现这种转换:

1
2
3
4
5
6
7
8
9
10
//  Utility for flipping an image drawing
CGImageRef flip (CGImageRef im) {
     CGSize sz = CGSizeMake(CGImageGetWidth(im), CGImageGetHeight(im));
     UIGraphicsBeginImageContextWithOptions(sz, NO , 0);
     CGContextDrawImage(UIGraphicsGetCurrentContext(),
                        CGRectMake(0, 0, sz.width, sz.height), im);
     CGImageRef result = [UIGraphicsGetImageFromCurrentImageContext() CGImage];
     UIGraphicsEndImageContext();
     return  result;
}

我们可以使用这个工具函数来修复我们上面例子中调用CGContextDrawImage产生的问题,让它们正确画出火星的一半。

1
2
3
4
CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height),
                        flip(marsLeft));
CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height),
                        flip(marsRight));

但是,我们仍然有一个问题:在双分辨率设备上,如果我们的图片有一个双分辨率的版本(@2x.png),这个绘制就会出错。原因就是我们使用 imageNamed:来获取原始的火星图片,这样就会返回一个为了适配双分辨率而设置自己的缩放比例来产生双倍分辨率的图片。但是CGImage没有scale属性,同时对这张图片为原始分辨率两倍一无所知!因此,我们在双分辨率设备上,我们通过调用 [mars  CGImage]获得到的火星CGImage图片,是火星图片大小的两倍,那么我们所有的计算都是错的。

所以,为了在CGImage提取想要的片,我们必须把所有适当的值乘以缩放比例scale,或者以CGImage的尺寸来描述大小。下面是我们在单分屏和双分屏都正确绘制的一个代码版本,并且补偿了翻转效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
UIImage* mars = [UIImage imageNamed:@ "Mars" ];
CGSize sz = mars.size;
// Derive CGImage and use its dimensions to extract its halves
CGImageRef marsCG = [mars CGImage];
CGSize szCG = CGSizeMake(CGImageGetWidth(marsCG),     
CGImageGetHeight(marsCG));
CGImageRef marsLeft =
     CGImageCreateWithImageInRect(
         marsCG, CGRectMake(0,0,szCG.width/2.0,szCG.height));
CGImageRef marsRight =
     CGImageCreateWithImageInRect(
         marsCG, CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height));
UIGraphicsBeginImageContextWithOptions(
     CGSizeMake(sz.width*1.5, sz.height), NO , 0);
// The rest is as before, calling flip() to compensate for flipping
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height),
                    flip(marsLeft));
CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height),
                    flip(marsRight));
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(marsLeft); CGImageRelease(marsRight);

另一种方案就是:在UIImage里面包装一个CGImage,绘制这个UIImage。UIImage可以通过调用 imageWithCGImage:scale:orientation:来实现这种方式,补偿缩放带来的影响。此外,通过绘制一个UIImage,而不是一个的CGImage,我们避免了翻转问题。下面是一种同时处理翻转和缩放的方法(没有调用我们上面的公用类):

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
UIImage* mars = [UIImage imageNamed:@ "Mars" ];
CGSize sz = mars.size;
// Derive CGImage and use its dimensions to extract its halves
CGImageRef marsCG = [mars CGImage];
CGSize szCG = CGSizeMake(CGImageGetWidth(marsCG),         
CGImageGetHeight(marsCG));
CGImageRef marsLeft =
     CGImageCreateWithImageInRect(
         marsCG, CGRectMake(0,0,szCG.width/2.0,szCG.height));
CGImageRef marsRight =
     CGImageCreateWithImageInRect(
         marsCG, CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height));
UIGraphicsBeginImageContextWithOptions(
     CGSizeMake(sz.width*1.5, sz.height), NO , 0);
[[UIImage imageWithCGImage:marsLeft
                      scale:mars.scale
                orientation:UIImageOrientationUp]
  drawAtPoint:CGPointMake(0,0)];
[[UIImage imageWithCGImage:marsRight
                      scale:mars.scale
                orientation:UIImageOrientationUp]
  drawAtPoint:CGPointMake(sz.width,0)];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(marsLeft); CGImageRelease(marsRight);

是的,另一种方案解决翻转,就是在绘制CGImage之前,对图形上下文进行线性转换,有效地翻转图形上下文中内部的坐标系统。这种方式很简洁,但是当有其他的线性转换时会变得难以理解。我会在下面的章节中谈论更多图形上下文转换的内容。

 

    为什么会发生翻转??

Core Graphics 会意外发生翻转的历史,来源于OS X世界,OS X 里的坐标系统的原点默认是在左下角,正Y方向是向上的,而在iOS中,坐标原点默认在左上角,正Y方向是向下的。在大多数的绘画中没有问题,因为图形上下文的坐标系统会自动适应的。另外,在iOS的Core Graphics框架中的上下文绘画时,上下文的坐标系统原点是左上角,我们都知道,但是,创建和绘制CGImage在两个坐标系统之间,互不匹配。

Snapshots 快照

  一个完整的视图 ----包括视图中的一个button、继承自这个视图的的所有视图 -----可以通过调用 drawViewHierarchyInRect:afterScreenUpdates:来绘制在当前的图形上下文中。这个方法是在iOS7中新添加的(比CGLayer提供的方法 renderInContext:快很多,已经被取代了)。得到的是原始视图的一个快照,跟原始视图看起来完全一样,只不过这个只是视图的一个位图图像,一个轻量级的虚拟拷贝。因为iOS界面的动态本质,快照功能显得非常有用。例如,你可以把一个视图的快照放在这个视图之上来隐藏发生的事情,或者在动画中展示视图移动的变化过程,而实际上只是一个快照而已。

下图展示了一个快照在我的一个应用中展示的效果。用户可以点击任意三个颜色调节按钮来调节颜色值,当颜色编辑的界面显示的时候,我想要用户有种只是临时界面的感觉,可以看到原来的界面潜伏在背后,但是原始的界面不能让用户分心,所以模糊处理了。实际上,模糊的只是原始界面的快照。

下面是图中快照的生成方式:

1
2
3
4
UIGraphicsBeginImageContextWithOptions(vc1.view.frame.size, YES , 0);
[vc1.view drawViewHierarchyInRect: vc1.view.frame afterScreenUpdates: NO ];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

  

接下来就是模糊化这张快照,并把它放到颜色编辑页面的下面。至于如何实现一个模糊效果就是另一个问题了。我可以会使用CIFilter(下一节的主题),但是太慢了;作为替代,我使用苹果提供的一个UIImage类别,它是作为“Blurring and Tinting an Image” 实例代码的一部分发布的。

一种更加快速获取一个视图快照的方式是使用UIView(或者UIScreen)实例方法 snapshotViewAfterScreenUpdates: 。返回的是一个UIView,而不是一个UIImage;更确切地说是一个知道怎么绘制一张图片的UIImageView,称为快照。这样的快照视图通常展示原来视图的样式,但是你也可以扩大它的边框bounds,这样快照图片也会被拉伸。如果你希望这个被拉伸的快照像可被拉伸的图片那样工作,可以用resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets方法代替。从快照视图中产生快照视图是完全合理的。

 

CIFilter 和CIImage

  这个“CI”在CIFilter和CIImage中代表Core Image,这是一种通过数字过滤器转换图片的技术。Core Image最初在OS X系统出现。有一些OS X提供的过滤器在iOS中不可用(或许是它们对于电话设备来说,数字处理太密集了)。

其中一个过滤器就是CIFilter。可以用的过滤器(大概120种,其中有24种是iOS7新添加的)分为几类:

* 图案和渐变(Patterns ,gradients)

  这些过滤器创建的CIImage可以和其它的CIImage组合在一起,例如单个颜色,棋盘图案,条纹,渐变。

* 混合(Compositing)

  这些过滤器可以组合多张图片,使用合成混合模式与我们使用Photoshop图片处理程序很相似。

* 颜色(Color)

  这些过滤器会适应或者修改一张图片的颜色。另外,你可以改变图片的饱和度,色度,亮度,对比度,灰度,白点,曝光度,阴影,聚焦等等。

* 几何(Geometric)

  这些过滤器在图像上执行基本的几何变换,例如缩放,旋转和裁剪。

* 变换(Transformation)

  这些过滤器可以扭曲,模糊或风格化图片。只有一部分适用于iOS。

* 过渡(Transition)

  这些过滤器提供了一个图像与另一个之间的过渡的框架。通过调用序列中的帧,可以实现图像与图像之间的过渡动画。

* 特定目的

  这些过滤器进行高度专业化的操作,如人脸检测和生成QR(二维)码。

 

  基本使用CIFilter是相当简单的,它本质上就如过滤器字面上的意思,只是一种由键和值组成的字典罢了。你通过调用 filterWithName:方法,并提供一个过滤器的字符串形式的名称来创建过滤器;为了明白这些过滤器名称有哪些,可以查阅苹果的 Core Image Filter Reference,或者传递一个nil 值的参数调用CIFilter 类方法 filterNamesInCategories:。每一个过滤器由一小部分的键值对来决定自身的行为。你可以完全从代码中学习这些键,但是通常你还是会查阅文档。对于你感兴趣的每个键,你提供一个键 - 值对,通过调用setValue:forKey:,或通过传递键和值以及过滤器名称参数来调用filterWithName:keysAndValues​​:方法来实践每个效果。在传递参数时,一个数字必须封装成一个NSNumber的类型,同时系统也提供了一些支持类,如CIVector(类似CGPoint 和 CGRect的结合体),CIColor,这些都是很容易理解和掌握的。

  一个CIFilter的键包括任何图像或图像上进行操作的过滤器;这样的图片必须是CIImage。你可以在过滤器的输出中获取CIImage;另外,过滤器之间可以链接在一起。但是,在过滤器链中的第一个过滤器会是什么呢?那这个过滤器操作的CIImage从哪里来呢?你可以从CGImage中,通过调用initWithCGImage:来获得一个CIImage,而你可以从UIImage的CGImage属性中获取一个CGImage。

  注意:不要视图为了方便,直接从UIImage中,通过调用实例方法CIImage来获取一个CIImage。这样不会把一个UIImage转化成CIImage,它仅仅只是指向了一个已经备份了UIImage对象的CIImage,而你的图片资源并没有被CIImage备份了,而是被CGImage备份了。我下面将会解释一个被CIImage备份的UIImage是从哪里来的。

 

  当你创建一个过滤器链时,实际上什么也没有发生。只有当你在过滤器链中转换最终的CIImage 为一个位图绘制时,密集的计算才会发生。有两种方式来实现这种效果:

  * 创建一个CIContext(通过调用contextWithOptions:),然后调用createCGImage:fromRect:,把最终的CIImage作为第一个参数传递进去。这里仅有的轻微棘手的事情是,CIImage没有一个框架(frame)或边界(bounds);它有一个面积。你通常会使用这个作为createCGImage:fromRect:的第二个参数。最终输出的CGImage可用于任何地方和目的,例如显示在你的应用中、转换成一个UIImage或者用来进行下一步的绘制。

  *直接通过调用UIImage类方法 imageWithCIImage:来创建一个UIImage。而获取最终的CIImage可以调用实例方法 initWithCIImage:;或者更加强大的 imageWithCIImage:scale:orientation: 又或者 initWithCIImage:scale:orientation:。你必须在这些调用之后把UIImage绘制在一些图形上下文中。最后一步是必不可少的,你需要亲自转换CIImage为一个位图,否则不会自动转换。另外,从imageWithCIImage:创建的UIImage并不能直接在UIImageView中显示,因为这个UIImage不包含任何绘制其自身的信息,它是用来绘画的,而不是用来展示的。

   为了阐明上面说的内容,我会通过一张我自己的原始图片,来创建一个圆形的插图效果:如下所示

  我们使用一个CIFilter来生成白色和黑色两种默认颜色之间的径向渐变效果。然后我们使用第二个CIFilter来把这个径向渐变当做我的头像和一个默认透明背景的混合遮罩,径向渐变为白色(一切渐变的内半径内)的地方,我们只是看到了我的头像,径向渐变为黑色的地方(一切渐变的外半径)则是透明的颜色,而在渐变的中间,使图像消失在圆带渐变的半径之间。从最终的过滤器链中输出的CIImage,我们可以把它转变成一个UIImage。

复制代码
UIImage* moi = [UIImage imageNamed:@"Moi"];
    CIImage* moi2 = [[CIImage alloc] initWithCGImage:moi.CGImage];
    CGRect moiextent = moi2.extent;
    CIFilter* grad = [CIFilter filterWithName:@"CIRadialGradient"];
    CIVector* center = [CIVector vectorWithX:moiextent.size.width/2.0
                                           Y:moiextent.size.height/2.0];
    [grad setValue:center forKey:@"inputCenter"];
    [grad setValue:@85 forKey:@"inputRadius0"];
    [grad setValue:@100 forKey:@"inputRadius1"];
    CIImage *gradimage = [grad valueForKey: @"outputImage"];
    CIFilter* blend = [CIFilter filterWithName:@"CIBlendWithMask"];
    [blend setValue:moi2 forKey:@"inputImage"];
    [blend setValue:gradimage forKey:@"inputMaskImage"];
    CGImageRef moi3 =
    [[CIContext contextWithOptions:nil]
     createCGImage:blend.outputImage
     fromRect:moiextent];
    UIImage* moi4 = [UIImage imageWithCGImage:moi3];
    CGImageRelease(moi3);
复制代码

  我们可以直接捕捉CIImage作为一个UIImage,而不用由来自于链中的最后CIImage产生的CGImage来转化成一个UIImage-----但是我们在之后绘制它,以让它生成一个从过滤器链输出的位图。例如,我们可以把它绘制在图片上下文中:

UIGraphicsBeginImageContextWithOptions(moiextent.size, NO, 0);
    [[UIImage imageWithCIImage:blend.outputImage] drawInRect:moiextent];
    UIImage* moi4 = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

  一个过滤器链可以包含在一个简单的自定义的CIFilter子类中。你的子类需要实现 outputImage(同时还有其他方法,例如 setDefaults),下面是我子类化的CIFilter

复制代码
@interface MyVignetteFilter ()
    @property (nonatomic, strong) CIImage* inputImage;
    @end
    @implementation MyVignetteFilter
    -(CIImage *)outputImage {
        CGRect inextent = self.inputImage.extent;
        CIFilter* grad = [CIFilter filterWithName:@"CIRadialGradient"];
        CIVector* center = [CIVector vectorWithX:inextent.size.width/2.0
                                               Y:inextent.size.height/2.0];
        [grad setValue:center forKey:@"inputCenter"];
        [grad setValue:@85 forKey:@"inputRadius0"];
        [grad setValue:@100 forKey:@"inputRadius1"];
        CIImage *gradimage = [grad valueForKey: @"outputImage"];
        CIFilter* blend = [CIFilter filterWithName:@"CIBlendWithMask"];
        [blend setValue:self.inputImage forKey:@"inputImage"];
        [blend setValue:gradimage forKey:@"inputMaskImage"];
        return blend.outputImage;
} @end
复制代码

而下面的这是如何使用这个CIFilter的子类,同时展示它的输出:

复制代码
CIFilter* vig = [MyVignetteFilter new];
    CIImage* im =
        [CIImage imageWithCGImage:[UIImage imageNamed:@"Moi"].CGImage];
    [vig setValue:im forKey:@"inputImage"];
    CIImage* outim = vig.outputImage;
    UIGraphicsBeginImageContextWithOptions(outim.extent.size, NO, 0);
    [[UIImage imageWithCIImage:outim] drawInRect:outim.extent];
    UIImage* result = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    self.iv.image = result;
复制代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值