iOS 绘画学习(2)
Snapshots 快照
一个完整的视图 ----包括视图中的一个button、继承自这个视图的的所有视图 -----可以通过调用 drawViewHierarchyInRect:afterScreenUpdates:来绘制在当前的图形上下文中。这个方法是在iOS7中新添加的(比CGLayer提供的方法 renderInContext:快很多,已经被取代了)。得到的是原始视图的一个快照,跟原始视图看起来完全一样,只不过这个只是视图的一个位图图像,一个轻量级的虚拟拷贝。因为iOS界面的动态本质,快照功能显得非常有用。例如,你可以把一个视图的快照放在这个视图之上来隐藏发生的事情,或者在动画中展示视图移动的变化过程,而实际上只是一个快照而已。
下图展示了一个快照在我的一个应用中展示的效果。用户可以点击任意三个颜色调节按钮来调节颜色值,当颜色编辑的界面显示的时候,我想要用户有种只是临时界面的感觉,可以看到原来的界面潜伏在背后,但是原始的界面不能让用户分心,所以模糊处理了。实际上,模糊的只是原始界面的快照。
下面是图中快照的生成方式:
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;