├── 1-绘制上下文 ├── 1-1.png ├── 1-10.png ├── 1-11.png ├── 1-12.png ├── 1-13.png ├── 1-14.png ├── 1-15.png ├── 1-16.png ├── 1-17.png ├── 1-2.png ├── 1-3.png ├── 1-4.png ├── 1-5.png ├── 1-6.png ├── 1-7.png ├── 1-8.png ├── 1-9.png ├── 1-绘制上下文.md ├── table1.png └── table2.png ├── 2-几何语言 ├── 2-1.png ├── 2-2.png ├── 2-3.png ├── 2-4.png ├── 2-5.png ├── 2-6.png ├── 2-7.png ├── 2-8.png ├── 2-几何语言.md ├── other1.png ├── table2-1-1.png ├── table2-1-2.png ├── table2-2.png ├── table2-3.png ├── table2-4.png ├── table2-5-1.png └── table2-5-2.png ├── 3-绘制图像 ├── 3-1.png ├── 3-10.png ├── 3-11.png ├── 3-12.png ├── 3-2.png ├── 3-3.png ├── 3-4.png ├── 3-5.png ├── 3-6.png ├── 3-7.png ├── 3-8.png ├── 3-9.png └── 3-绘制图像.md ├── 4-路径基础 ├── 4-1.png ├── 4-10.png ├── 4-11.png ├── 4-12.png ├── 4-13.png ├── 4-14.png ├── 4-15.png ├── 4-16.png ├── 4-2.png ├── 4-3.png ├── 4-4.png ├── 4-5.png ├── 4-6.png ├── 4-7.png ├── 4-8.png ├── 4-9.png ├── 4-路径基础.md ├── table4-1-1.png └── table4-1-2.png ├── 5-路径深入 ├── 5-1.png ├── 5-10.png ├── 5-11.png ├── 5-12.png ├── 5-13.png ├── 5-14.png ├── 5-15.png ├── 5-16.png ├── 5-2.png ├── 5-3.png ├── 5-4.png ├── 5-5.png ├── 5-6.png ├── 5-7.png ├── 5-8.png ├── 5-9.png ├── 5-路径深入.md ├── other1.png ├── table5-1-1.png └── table5-1-2.png ├── 6-绘制渐变 ├── 6-1.png ├── 6-10.png ├── 6-11.png ├── 6-12.png ├── 6-13.png ├── 6-14.png ├── 6-15.png ├── 6-16.png ├── 6-17.png ├── 6-18.png ├── 6-19.png ├── 6-2.png ├── 6-20.png ├── 6-21.png ├── 6-22.png ├── 6-23.png ├── 6-3.png ├── 6-4.png ├── 6-5.png ├── 6-6.png ├── 6-7.png ├── 6-8.png ├── 6-9.png └── 6-绘制渐变.md ├── 7-遮罩,模糊和动画 ├── 7-1.png ├── 7-10.png ├── 7-2.png ├── 7-3.png ├── 7-4.png ├── 7-5.png ├── 7-6.png ├── 7-7.png ├── 7-8.png ├── 7-9.png └── 7-遮罩,模糊和动画.md ├── 8-绘制文本 ├── 8-1.png ├── 8-10.png ├── 8-11.png ├── 8-12.png ├── 8-13.png ├── 8-14.png ├── 8-15.png ├── 8-16.png ├── 8-17.png ├── 8-18.png ├── 8-19.png ├── 8-2.png ├── 8-20.png ├── 8-21.png ├── 8-22.png ├── 8-23.png ├── 8-24.png ├── 8-25.png ├── 8-26.png ├── 8-27.png ├── 8-28.png ├── 8-29.png ├── 8-3.png ├── 8-30.png ├── 8-4.png ├── 8-5.png ├── 8-6.png ├── 8-7.png ├── 8-8.png ├── 8-9.png └── 8-绘制文本.md └── README.md /1-绘制上下文/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-1.png -------------------------------------------------------------------------------- /1-绘制上下文/1-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-10.png -------------------------------------------------------------------------------- /1-绘制上下文/1-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-11.png -------------------------------------------------------------------------------- /1-绘制上下文/1-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-12.png -------------------------------------------------------------------------------- /1-绘制上下文/1-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-13.png -------------------------------------------------------------------------------- /1-绘制上下文/1-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-14.png -------------------------------------------------------------------------------- /1-绘制上下文/1-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-15.png -------------------------------------------------------------------------------- /1-绘制上下文/1-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-16.png -------------------------------------------------------------------------------- /1-绘制上下文/1-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-17.png -------------------------------------------------------------------------------- /1-绘制上下文/1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-2.png -------------------------------------------------------------------------------- /1-绘制上下文/1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-3.png -------------------------------------------------------------------------------- /1-绘制上下文/1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-4.png -------------------------------------------------------------------------------- /1-绘制上下文/1-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-5.png -------------------------------------------------------------------------------- /1-绘制上下文/1-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-6.png -------------------------------------------------------------------------------- /1-绘制上下文/1-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-7.png -------------------------------------------------------------------------------- /1-绘制上下文/1-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-8.png -------------------------------------------------------------------------------- /1-绘制上下文/1-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/1-9.png -------------------------------------------------------------------------------- /1-绘制上下文/1-绘制上下文.md: -------------------------------------------------------------------------------- 1 | # 绘制上下文 2 | 3 | 绘制上下文是指在你的应用上通过一个虚拟的画布进行绘制。在本章,你需要复习iOS绘图的核心技术。深入理解上下文,才能知道如创建和绘制他们。通过学习UIKit,Core Graphics 和 Quartz中的绘图基础,使用上下文来创建图片,文档和子视图。 4 | ### 框架 5 | iOS绘图程序主要基于UIKit和QuartzCore框架,它由新颖的Objective-C界面(UIKit提供)和老旧一些的C语言方法和核心基础类(QuartzCore)组成。两者共同完整了你的代码。这里有一个例子分别展示了UIKit绘制和Quartz绘制: 6 | ``` 7 | //Draw a rounded rectangle in UIKit 8 | UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:inset cornerRadius:12]; 9 | [bezierPath stroke]; 10 | 11 | //Fill an ellipse in Quartz 12 | CGContextFillEllipseInRect(context, rect); 13 | ``` 14 | QuertzCore框架通常被称作Quartz或者Quartz 2D。后者是苹果官方命名,因为它原本是2D图形的API。他最初命名在苹果的开发者文档“Quartz 2D Programming Guide”。 15 | 16 | 这个框架实际涉及到的地方比Quartz 2D这个名字暗示的要广。QuartzCore引用混合和使用很多其他图片特征。比如说,Quartz API包括了动画, 贴片,以及Core Image 过滤(尽管Core Image有它自己完整的框架) 17 | 18 | 对于iOS开发者来说,Quartz往往比它的内部名字Core Graphics更加出名。本书使用Quartz或者Core Graphics都是表示一个意思。大多数以CG开头的C语言API都是指的Core Graphics。从几何(CGRect和CGPoint)到对象(CGColorRef,CGImageRef和CGDataProviderRef)。Quartz提供了极其广袤的绘图技术。 19 | 20 | Quartz和他的前辈已经很好的被使用了很长一段时间,可以一直追溯到20世纪80年代,在Display PostScript在NeXTStep操作系统中加强了图像时。然而,Core Graphics方法仍然持续在绘图开发中日复一日的发挥着重要的作用。尽管更新的程序替代了一些常用任务,但他们并未被完全取代掉。准备好在未来同这些框架一起工作吧。 21 | ### 何时绘制? 22 | 尽管iOS绘图对于开发者来说是完全一样的任务, 但任务本身可能不尽相同。大部分绘制会限制在特定的场地,有四个直接对图像有意义的部分使用极其相似的脚本(注:这句可能翻译有问题):创建子视图,构造图片,创建PDF以及构造核心图像。 23 | 24 | #### 创建子视图 25 | 26 | 每一个UIKit视图实际上都是一个空白的画布。你可以完全地自定义一个视图通过绘制一些你想要在应用中表达的内容。你可以通过一个很特别的方法 -drawRect:来完成。 该方法允许你通过一些UIKit和Quartz Drawing代码来自定义视图的外貌。 27 | 28 | 图1.1 29 | 30 | 图1-1 展示了一个自定义的颜色选择器。他包括了一组选择视图,每一个视图都是UIView通过drawRect:绘制的子视图。这些视图绘制了圆角,文本信息和一个色块。 31 | 32 | 除非你重用于一个子类,否者默认drawRect:是什么都不干的。子类想要通过UIKit和Core Graphics代码来创建控价外貌需要重用drawRect:方法,它添加了绘制请求来实现子类。简单的视图比如只更新背景颜色或是通过其他方法改变类容而非绘制的不应该重写drawRect:。同样地,OpenGL ES视图也不以此为绘图的接入点。 33 | 34 | drawRect:方法有一个近亲-drawRect:forViewPrintFormatter:允许你子类化的绘制类容和展示不同(注:可能翻译有误)。 35 | 36 | #### 构造图片 37 | 38 | 并不是每个图片一开始都是一张PNG或者一张JPEG。在iOS中,你可以绘制一个UIKit图片上下文,再把他转换为一个UIImage实例。这可以帮助你创建一张新图片,或是处理一些已存在的图片。 39 | 40 | 图1-2展示了一个代码创建的颜色选择轮盘,这个图片通过一系列的贝塞尔曲线圆弧绘制到一个UIKit图像的上下文中创建。处理的结果图片被添加到了一个标准的图片视图中,绘制允许你创建一个自定义的图片,而不用事先保存在文件目录中。 41 | 以代码为基础的绘制是一个很重要的折衷的办法。尽管你需要跟多的时间来创建图片(并不需要很大量的代码,而且是可以测量的),但是你可以得到一个很小的程序包,因为有很少的资源文件。你的图片也更加灵活——仅需改变创建他们的代码即可。 42 | 43 | 图1.2 44 | 45 | 46 | #### 创建PDF 47 | 48 | 和创建图片类似的方法也支持PDF的创建,你可以绘制在一个UIKit PDF上下文中,不需要直接发送到文件或是数据。它可以帮助你在应用中创建PDF上下文并分享,存储或是展示他们,就像图1-3那样。 49 | PDF提供一个高度的可分割的,系统独立标准封装了一个完成的文档描述。这个文档你在iOS中创建的会和你在其他电脑看到的或多或少地一样。操作系统的颜色管理系统也许会影响到颜色的展示 50 | 图1-3展示了一个在Popover的视图中多页PDF 51 | 52 | 图1.3 53 | 54 | 55 | #### 构造核心图像 56 | 当你碰触到现在UIKit的性能极限,你可能需要转而向标准的Quartz求助。Core Graphics一直处于领先地位,它的绘制能力在OS X已经高效使用了近十年了。任何在UIKit中不能马上调节的特征,都机会可以通过Quartz实现。 57 | Core Graphics上下文的绘制组件提供了灵活有力的绘图解决方案——哪怕那不如它的兄弟UIKit那样简单。Core Graphics 使用核心基础风格的C语言基础的对象并且需要手动管理内存。 58 | 比如说,你也许想要一字节一字节地处理图像。这个任务在UIKit好像不能很好的完成,但是使用Core Graphics的位图上下文却能够很好的被解决。图1-4展示了一个例子解释了为什么需要使用一些Quartz的特性。在这个例子中,左侧的RGB图像通过Core Graphics转换成了一个灰度图像。 59 | 得到的灰度图是通过Quartz的CGImageRef(以CG开头Ref结尾的类型是指,指向对象的指针)转换成UIImage实例(imageWithCGImage:),再通过图片视图展示出来的。 60 | 61 | 图1.4 62 | 63 | 64 | 65 | ### 上下文 Contexts 66 | 67 | 所有的iOS绘图操作都是从一个上下文开始的。从概念上来说,上下文非常类似于一页空纸或是一张空的画布。它们展示iOS绘图的目的地。它们囊括了所有绘图界质状态所包含的信息——比如说,无论画布旋转或是位移,画布上绘制何种颜色,你可以绘制在图片上的点的细节程度都是如此的(注:可能有误,这句好难啊🤯) 68 | 在iOS中,你主要会和两种上下文打交道:位图上下文和PDF上下文。Core Image框架海提供了第三种上下文类型,用来处理图片进程任务而非绘图。 69 | 70 | #### 位图上下文 71 | 72 | 位图上下文基本上是矩形数据数组。数据的大小取决于图片的每一个元素(或像素)展示何种类型的颜色。Device RGB——就像图1-4的左侧图片——需要用到3或4字节每个像素。透明图象需要4个字节,不透明图象3个字节。 73 | 为了优化存储,不透明位图不需理会透明值。透明图象多了一个alpha值。这个值存储在实际颜色和亮度中一个很特别的字节里。他反应了每个像素的透明度。颜色信息在Device RGB图片中存于3个字节中,分别对应了红,绿,蓝的数值。 74 | Device Gray 图像——如图1-4右侧所示——需要用到1或2字节每个像素。其中,亮度信息占一个字节,还有一个是透明度的可选字节。 75 | 76 | #### PDF上下文 77 | 78 | 从开发者的角度来看,PDF上下文和位图上下文的运行方式相似。你通过相同的指令和方法绘制它们。你设置颜色,绘制形状和文本就像你在绘制一个视图或图像一样。然而,它们还是有不同的地方的。 79 | PDF上下文在他的“后备存储”中包括了矢量数据,用来描述绘制在一个分辨率无关的方法中。位图是光栅化的(注:也可以叫点阵化),它使用像素数组来存储和绘制数据。 80 | PDF上下文也可以多于一页。你设置一个边界矩形,限定默认尺寸和每一页的位置。一个空矩形(CGRectZero)默认是一个标准的A(字母)页——8.5x11英尺,或是612x792点(第二章回详细讨论点和像素的区别)。 81 | PDF内部是由向量指令集储存的。他提供了在位图绘制中看不到的与分辨率无关的解决方案。苹果在文档中写道,“PDF文件是天生与分辨率无关的——它们绘制的尺寸可以增加或减少却不需要额外的牺牲图片的细节。用户感知位图图像的质量是通过视图可见部分的分辨率来决定的。” 82 | 总的来说,位图上下文和PDF上下文还是大致相似的。区别仅在于目的地(文件和数据的展示)和当你需要翻页时体现。 83 | 除了位图上下文和PDF上下文,你也可能会遇到核心图形上下文。 84 | 85 | #### Core Image上下文 86 | 87 | Core Image框架帮助你快速的处理图片。它提供相关代码来进行数码图片的处理和电脑维护图片资源。有了它,你可以处理滤镜,链型滤镜,进行特征检测(人脸识别),分析图片自适应。 88 | Core Image上下文把Core Image对象和Quartz 2D以及OpenGL紧密联系在一起。得利于移动端图片处理中心(GPU)的发展,核心图形上下文整合了Core Video的像素缓冲区。Core Image使用它自己的颜色和图片类(CIColor和CIImage),它们最好的优化了核心图形的快速滤镜和图片特征检测。 89 | 90 | ### 在UIKit中创建上下文 91 | UIKit提供了很友好的处理上下文的入口,最简单的图片绘制方法在下方代码1-1中展示。这个代码只展示了开启一个新的上下文(为上下文赋予一个尺寸),绘制,取到一个新的图片,结束这个上下文。这段代码生产出一个1:1比例的图片,所有的复合精确像素的位图上下文绘制操作都在这里进行。 92 | 93 | ``` 94 | UIGraphicsBeginImageContext(size); 95 | 96 | //Perform drawing here 97 | 98 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 99 | UIGraphicsEndImageContext(); 100 | 101 | ``` 102 | #### 设备比例 103 | 104 | 下列代码1-2展示了一个更加常见的(更有效的)入口。它帮助你根据屏幕比例来处理图片。options参数帮助你根据不同设备比例来绘制图片。Scale是指逻辑空间(以点计算)和物理展示(以像素计算)之间的关系。 105 | 106 | Scale为2的图片会产生一个Retina图像。像素值在每个方向均为两倍,生成的图片也会以4倍的空间储存相比于scale为1的图片来说。不透明参数(isOpaque)允许你忽略alpha通道以达到最优储存。你只需要3字节每像素即可储存一个不透明图象,而非4字节还有一个额外的alpha通道。 107 | 108 | ``` 109 | UIGraphicsBeginImageContextWithOptions(targetSize,isOpaque,deviceScale); 110 | 111 | //Perform drawing here 112 | 113 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 114 | UIGraphicsEndImageContext(); 115 | 116 | ``` 117 | 强烈建议你使用上面的代码来创建上下文。比如说,你想为UIImageView绘制一个图片的时候,总是会需要带上options参数的。 118 | 你可以通过UIScreen来获取第三个参数。图片绘制在初始屏幕上时,可以使用mainScreen,如下: 119 | 120 | ``` 121 | UIGraphicsBeginImageContextWithOptions(targetSize,isOpaque,[UIScreen mainScreen].scale); 122 | ``` 123 | 当然你可以把第三个参数设置为0.0。系统会自动让比例基于你的主屏。 124 | 当作用于视频输出时,有时候你可能会有一个额外的监视器来连接到iOS设备,或是当你使用AirPlay无线展示到另一个屏幕上时,需要设计新设备的特殊scale参数。你同样可以通过UIScreen查询到这些信息。 125 | 126 | #### 创建PDF上下文 127 | 在创建PDF时,你要么提供一个文件路径要么提供一个提供数据源的可变数据对象。代码1-3展示了 128 | 构建的例子。你提供了一个矩形(CGRectZero表示A尺寸的页面)和一个指明元数据的字典,以及新文档的安全信息,比如你也许会需要指明作者,拥有者和密码,印刷和复杂的限制,关键词等等。 129 | 和位图上下文一样,Core Graphics的成就(尤其是CGPDFContext)远比UIKit要悠久得多。如果你想钻研C语言基础的类或方法,你将会发现另一套PDF的产品。 130 | 131 | ``` 132 | UIGraphicsBeginPDFContextToFile(pathToFile, theBounds, documentInfo); 133 | UIGraphicsBeginPDFPage(); 134 | 135 | //perform drawing here 136 | 137 | UIGraphicsEndPDFContext(); 138 | ``` 139 | 140 | 每一页的绘制都需要调用UIGraphicsBeginPDFPage()方法,他在文档中提供新的一页给你来写入。当你完成输出之后,你不需要明确地结束每一页,但是你必须结束PDF上下文,就像你结束其他的上下文那样。 141 | 142 | ### 在Quartz中创建上下文 143 | 144 | Core Graphics允许你不通过UIKit入口创建位图上下文。这个方法使用更久远的API,他能有效地帮助你一字节一字节地处理绘制数据。第三章会通过使用很多有效的增强图片进程的例子通过Quartz代码。 145 | 代码1-4展示更为复杂一些的代码。相比于UIKit,可能更容易让开发者感到困扰。这是因为,Quartz使用旧风格的核心框架对象,还需要手动管理内存。 146 | 在使用Quartz代码时记得常常使用静态分析器(Product > Analyze),以便检测你的引用是否正常释放。他提供了代码分析工具,可以同时检测出UIKit方法和Quartz函数中的潜在bug。了解更多:http://clang-analyzer.llvm.org 147 | 代码1-4,需注意释放方法。如果上下文不能被创建,color space就必须被释放。在Core Graphic的每个绘制阶段,你都会为很多对象分配内存,你必须在函数或方法结束之前很好的管理它们。 148 | 最后一点,要注意如何使用kCGImageAlphaPremultipliedFirst。他提供了一个ARGB序列,使用友好Quartz的前置乘法(注:?)。每一个像素,alpha值都存储在这四个字节里的最前面而蓝色值在最后。这种结构是在CGImage.h中的CGImageAlphaInfo文档中定义的。 149 | 150 | ``` 151 | //create a color space 152 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 153 | if(colorSpace == NULl) 154 | { 155 | NSLog(@"Error allocating color space"); 156 | return nil; 157 | } 158 | //create the bitmap context,(Note: in new version of 159 | //Xcode, you need to cast the alpha setting.) 160 | CGContextRef context = CGBitmapContextCreate( 161 | NULL,width,height, 162 | BITS_PER_COMPONENT, 163 | width * ARGB_COUNT, 164 | colorSpace, 165 | (CGBitmapInfo)kCGImageAlphaPremultipliedFirst); 166 | if(context == NULL) 167 | { 168 | NSLog(@"Error:Context not created!"); 169 | CGColorSpaceRelease(colorSpace); 170 | return nil; 171 | } 172 | 173 | //Push the context; 174 | //(This is optional. Read on for an explanation of this.) 175 | //UIGraphicsPushContext(context); 176 | 177 | //Perform drawing here 178 | 179 | //Balance the context push if used. 180 | //UIGraphicsPopContext(); 181 | 182 | //Convert to image 183 | CGImageRef imageRef = CGBitmapContextCreateImage(context); 184 | UIImage *image = [UIImage imageWithCGImage:imageRef]; 185 | 186 | //clean up 187 | CGColorSpaceRelease(colorSpace); 188 | CGContextRelease(context); 189 | CFRelease(imageRef); 190 | ``` 191 | ### 在上下文中绘图 192 | 很多Quartz方法都依赖于你要绘制在上面的上下文。比如说,思考一下代码1-5中的方法,他设置了一个4像素宽,灰色,在一个矩形画布中划出一个椭圆。每个方法都带着一个CGContextRef类型的context参数,你可以自己创建上下文(如代码1-4那样)可以借助UIKit方法,这个我们下一节来探究。 193 | ``` 194 | //set the line width 195 | CGContextSetLineWidth(context,4); 196 | 197 | //set the line color 198 | CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor); 199 | 200 | //Draw an ellipse 201 | CGContextStrokeEllipseInRect(context, rect); 202 | ``` 203 | 1-5这段代码应该是写在代码1-4中的“Perform drawing here”这个位置的。1-4中,你已经完整创建了一个位图上下文,你可以尽情在这个上下文绘制了。 204 | 在代码1-4的最后,你创建了一个图片并且手动释放了上下文。图片1-5展示了代码1-5融入代码1-4之后的输出。你绘制了一个灰色的线条绘制的4像素宽的椭圆,图1-5的图片通过UIImageView展示。 205 | 这个UIKit的入口进程为imageWithCGImage:,他是最新的通过CGImageRef创建UIImage实例的方法。 206 | 207 | 图1.5 208 | 209 | ### 通过UIKit上下文来绘制 210 | UIKit简化了创建喝管理上下文的任务。只用一行就可以创建一个新的图片或是PDF上下文,另一行用来把这些事务包起来。在这两行之间,你可以自由地添加任何绘制请求,在绘制在当前的上下文中。 211 | 在代码1-6中也处理了和代码1-5中相同的绘制任务,当然绘制的结果也是完全一致的。然而,和绘制在自己创建的Quartz上下文不同,这里改进为创建一个新的图片上下文(image context)。你可以使用UIGraphicsGetCurrentContext()连接UIKit和Quartz,他会返回一个CGContextRef对象,允许你来处理一些核心图像的绘画请求。 212 | 又一次,当你完成1-6的代码之后,对比1-4和1-5的融合,你就会发现UIKit的绘图有多简单 213 | ``` 214 | //Establish the image context 215 | UIGraphicsBeginImageContextWithOptions(targetSize, isOpaque, 0.0); 216 | 217 | //Retrieve the current context 218 | CGContextRef context = UIGraphicsGetCurrentContext(); 219 | 220 | //Perform the drawing 221 | CGContextSetLineWidth(context,4); 222 | CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor); 223 | CGContextStrokeEllipseInRect(context, rect); 224 | 225 | //Retrieve the drawn image 226 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 227 | 228 | //End the image context 229 | UIGraphicsEndImageContext(); 230 | ``` 231 | 在开启任一图片或PDF上下文之后都可以使用UIGraphicsGetCurrentImage。这个上下文会一直持续到你调用UIGraphicsEndImageContext()或者UIGraphicsEndPDFContext(); 232 | 相似地,你可以使用UIGraphicsPushContext(context)在这些代码的内部再创建一个上下文。支持类似代码1-4中的手动管理Quartz上下文。你可以使用UIGraphicsPopContext()来结束上下文。这个方法可以把你绘制的上下文推入栈中。入栈出栈可以帮助你按需切换绘图目标。 233 | 此外,在明确的上下文环境之外,当前上下文为nil——并抛出异常。当调用drawRect:方法时,视图会把一个上下文推入UIKit的图像上下文栈中。所以当你使用drawRect:方法时,你可以默认总是有一个固定的上下文存在。 234 | 235 | ``` 236 | -(void) drawRect:(CGRect) rect 237 | { 238 | //Perform drawing here 239 | //if called, UIGraphicsGetCurrentContext(); 240 | //returns a valid context. 241 | } 242 | ``` 243 | 244 | #### UIKit当前的上下文 245 | 在Quartz中,几乎每一个绘制方法都需要一个context参数,你可以清楚地在每一个方法中看到,比如,这个绘制划线颜色为灰色的方法: 246 | ``` 247 | CGContextSetStokeColorWithColor(context, [UIColor grayColor].CGColor); 248 | ``` 249 | 在UIKit绘制中,上下文在运行时状态下创建。思考1-7的代码。这些代码又一次创建了一个一样的4像素宽的在图1-5中展示的椭圆。然而,这次的代码却没有明确的指定一个上下文。 250 | 这些代码创建了一个椭圆路径,把线宽设置为4,划线颜色设置为灰色,然后用划线绘制这个路径。每一步,上下文都被访问了。就像1-6的代码那样,一样的灰色椭圆绘制在了上下文中,只是这个上下文没有那么明确,1-7代码不用提及上下文即可完成这些操作。 251 | ``` 252 | //stroke an ellipse using a Bezier path 253 | UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:rect]; 254 | path.lineWidth = 4; 255 | [[UIColor grayColor] setStroke]; 256 | [path stroke]; 257 | ``` 258 | 发生了什么?让我们来探究一下。原来,UIKit有一个图像上下文堆。他让绘制操作实现在堆顶的上下文中。这些什么线宽或事灰色路径之类的设置都在最高层的上下文中实现。所以,如何使Core Graphics上下文和UIKit融合? UIKit提供了两种关键方法,可以简化我们在本章前些部分学到的方法: 259 | * 你需要使用UIGraphicPushContext(context)方法手动入栈一个上下文。这个方法会把上下文推入栈中,然后设置你想要绘制的上下文为被激活状态。 260 | * 通过UIGraphicsPopContext()来平衡,该操作吧当前上下文出栈,重新设置激活状态的上下文下一个上下文或是为nil。 261 | 262 | 通过融汇代码1-7的方法,你可以把贝塞尔曲线为基础的绘制代码嵌入Quartz为基础的像1-3那样的上下文中。这些方法建立起了UIKit绘图和核心图象上下文之间的桥梁。 263 | 264 | 总结,融合UIKit绘制和核心图像上下文的方法如下: 265 | 1. 创建一个核心图像上下文; 266 | 2. 通过UIGraphicsPushContext()入栈; 267 | 3. 使用暗示的在当前上下文中绘制的UIKit方法和明示上下文的Quartz方法结合来绘制。 268 | 4. (可选,把绘制好的图像存储在图片中) 269 | 5. 通过UIGraphicsPopContext(()出栈; 270 | 6. 释放上下文的内存。 271 | 272 | 如果你在一个没有激活状态下的UIKit中进行绘制,你会收到一个警告:上下文无效。 273 | 274 | ### UIKit和Quartz中的颜色 275 | 有很多核心框架的类都有对应的UIKit类,反之亦然。往往他们都是免过桥费的(toll free bridged),什么意思呢? 就是说,核心框架风格的数据类型可以等价交换为UIKit的版本。你通过ARC桥梁请求(__bridge)在核心框架和UIKit之间切换。但免过桥费在很多Quartz转UIKit的关系中尤其少,包括颜色这一块。 276 | 大多数UIKit中的绘画方法和类封装了Quartz方法和核心图像的对象。UIColor对象中包装了CGColor类,UIBezierPath对象内部包含了CGPath。UIImage封装了CGImage或事CIImage。尽管这些封装并不是等价对象,然而你可以很容易的获得背后的Quartz元素,而不通过桥接。 277 | 在UIKit中,UIColor提供颜色的颜色值和透明度。你可以从多个入口创建颜色,但是最常用的是RGB值创建(colorWithRed:green:blue:alpha:)或HSV创建(colorWithHue:saturation:brightness:alpha:)。 278 | 当使用Core Graphics时,你将会发现你在UIKit和Quartz中来回切换。为了解决这个问题,每一个UIColor都提供了一个CGColor属性。这个属性提供了Quartz类CGColorRef相对应的颜色和透明度的值。 279 | 你可以通过这个属性来为Quartz方法提供参数,如下: 280 | ``` 281 | 282 | CGContextSetFillColorWithColor(context,[UIColor greenColor].CGColor); 283 | 284 | ``` 285 | 这个方法使用了Quartz方法来设计填充颜色。第二个参数用一个标准的UIColor绿色开头,属性CGColor提供了一个Quartz 2D绘图颜色,很容易的被用来核心图像绘制。 286 | 287 | 特别要注意,从UIKit的封装中提取出Quartz颜色时,你需要手动retain这个颜色,让它可以在他的父类之外延续生命周期。当使用CGColorRef变量而非一个固定实例来计算释放内存问题的时候,很多开发者被ARC的内存管理难住。Mark Dalrymple在《the Big Nerd Ranch》中有讨论过这些问题:http://blog.bignerdranch.com/296-arc-gotcha-unexpectedly-short-lifetimes 288 | 从Quartz返回到UIKit,需要用一个类构造方法。该方法使用核心图像的对象引用构造了一个UIKit单例: 289 | ``` 290 | UIColor *color = [UIColor colorWithCGColor:myCGColorRef]; 291 | ``` 292 | > NOTE: 当使用UIColor实例创建一个CGColor对象时,当你分配一个CGColor变量之后ARC会提示你无法使用UIKit color,并弹出异常。当你创建一个核心图像属性时永远记得retain和release,即便它是有UIKit对象创建的。 293 | 294 | 295 | ### 画家模型 296 | 297 | iOS使用画家模型在上下文中绘制。除非你特别处理,否则所有新的绘制都是在已存在的绘制之上进行绘制。就和画家在画布上用颜料绘图是一样的。修改上下文都是通过覆盖新的绘制来完成的。 298 | 代码1-8展示了这个模型。这段代码画了两个贝塞尔曲线的圆。左紫右绿。图1-6是绘制的结果。绿色的圆覆盖在紫色圆形之上。 299 | 300 | 图1.6 301 | 302 | > NOTE: 这段代码用到greenColor和purpleColor并不是UIKit给的标准颜色,而是本书提供的固定的颜色,RGB值分别是(125,162,63)和(99,62,162),感兴趣可以进一步了解:http://github.com/erica/uicolor-utilities 303 | 304 | ``` 305 | - (UIImage *)buildImage 306 | { 307 | //Create two circular shapes 308 | CGRect rect = CGRectMake(0,0,200,200); 309 | UIBezierPath *shape1 = [UIBezierPath bezierPathWithOvalInRect:rect]; 310 | rect.origin.x += 100; 311 | UIBezierPath *shape2 = [UIBezierPath bezierPathWithOvalInRect:rect]; 312 | 313 | UIGraphicsBeginImageContext(CGSizeMake(300,200)); 314 | 315 | //First draw purple 316 | [purpleColor set]; 317 | [shape1 fill]; 318 | 319 | //Then draw green 320 | [greenColor set]; 321 | [shape2 fill]; 322 | 323 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 324 | UIGraphicsEndImageContext(); 325 | return image; 326 | } 327 | ``` 328 | 如果你转换绘画的顺序,shape2先,然后shape1,就得到如图1-7所示的结果。尽管位置和颜色都是和他们在图1-6中一样的。 329 | 330 | 图1.7 331 | 332 | ### 透明度 333 | 334 | 绘图时,透明度也是相当重要的一个角色。调整紫色的透明度会改变绘制的结果。这里把透明度改为原来的一半: 335 | ``` 336 | purpleColor = [purpleColor colorWithAlphaComponent:0.5f]; 337 | 338 | ``` 339 | 图1-8展示了调整透明度之后的图案,尽管紫色依然覆盖在绿色之上,但透明度的原因,绿色已经时可见的了。 340 | 341 | 图1.8 342 | 343 | 你可以已经发现,尽管绘制结果会随着程序改变,但有一个东西一直保持不变:新绘制的东西总是会影响到原来已经绘制的图像,而无视数学应用。这些应用就算使用混合模式,比如“目的顶峰”,那也只有当图片在空白位置绘制时才不会影响到原有的上下文数据(注:???) 344 | 345 | 关键在于理解你在添加新的源(不论是一个形状,或事一条线,一个图片)和由上下文所展示出来的你绘图的目的地。绘图程序运行你一步步向你的目标绘制就像现实中的画家一样。 346 | 347 | ### 上下文状态 348 | 在代码1-8中,当响应颜色实例时,set方法给定接下来的当前上下文的fill或是stroke操作的颜色。在这个代码中,紫色先设置,然后是绿色。当所有颜色都设置好后,才有接下来的绘制操作。 349 | 有两种颜色的指定方式:填充(setFill)和划(setStroke)。填充操作用颜色完全填充形状内部。划操仅形状边沿。图1-9中,填充色为绿色,而划颜色为紫色。 350 | 所有这三种方法(set,setFill和setStroke)都更新了当前的绘图状态,特别是激活填充和划的颜色。 351 | 352 | 图1.9 353 | 354 | #### 应用状态 355 | 思考下列用于创建1-9图所示图像的代码: 356 | ``` 357 | [greenColor setFill]; 358 | [purpleColor setStroke]; 359 | [bunnyPath fill]; 360 | [bunnyPath stroke]; 361 | 362 | ``` 363 | 他设置了填充和划的颜色然后在贝塞尔曲线中应用他们。这最大的问题是:这些方法的请求的目标是谁?谁在储存这些填充和划颜色的状态,并让他们运用到接下来的操作中? 364 | 答案是当前的上下文。UIGraphicsGetCurrentContext()方法返回的对象存储了填充和划的颜色。这个上下文会影响到所有的设置和绘图方法。 365 | 366 | 所有的上下文都会储存图像状态信息,他们扮演者绘图操作属性的角色。填充和划仅仅是储存在上下文中的两个状态类型。你之后会发现,上下文可以储存相当多的状态。图像状态会调整每一个绘图操作。 367 | 368 | #### Push 和 Pop 图象状态 369 | 所有的上下文都拥有一个图像状态设置的栈。每次创建新的上下文时,栈都会有一个新鲜的状态。随后,你可以尽情设置状态,如果需要,还可以出栈或入栈图片状态(GState)栈。 370 | 这个栈和之前提到UIKit上下文栈不同。上下文栈储存了绘制的目的地,让你通过出栈入栈来管理当前需要绘制的上下文。绘图目的地像是一个画布。当你改变了绘制栈,你只是换了一个画布。而状态栈每一个上下文都独有一个栈。它为一个上下文储存了绘制偏好的设置,改变每个“画布”中的绘制操作。两者都是用栈,却是图像系统中不同的两个部分。 371 | 每个图片状态都记录了他的改变。比方说,你把一个新的状态推进栈,然后调整默认线宽为10,这个上下文状态会持续到他被推出栈。推出后,线宽会变为他被设置为10之前的值。 372 | 代码1-9展示了图像状态栈的管理过程。他把填充和划的颜色分别设置为绿色和紫色。画出一个兔子然后调用CGContextSaveCState()“保存”当前状态,程序会把这些状态的复制推入当前上下文的GState栈。任何改变都会应用于新的图片状态拷贝。 373 | 如果你不做任何改变,你将继续绘制出绿色紫边的兔子。然而,代码1-9更新了代码,改为橙色和蓝色。重写当前的状态颜色设置。新的兔子绘制出来就是橙色和蓝色了。 374 | 最后代码通过CGContextRestoreCState()还原了之前的状态。他会推出所有新加入的栈的状态。最后一个兔子便是紫色和绿色,具体如下: 375 | ``` 376 | UIGraphicsBeginImageContext(size); 377 | CGContextRef context = UIGraphicsGetCurrentContext(); 378 | 379 | //Set initial stroke/fill colors 380 | [greenColor setFill]; 381 | [purpleColor setStroke]; 382 | 383 | //Draw the bunny 384 | [bunnyPath fill]; 385 | [bunnyPath stroke]; 386 | 387 | //Save the state 388 | CGContextSaveGState(context); 389 | 390 | //Change the fill/stroke colors 391 | [[UIColor orangeColor] setFill]; 392 | [[UIColor blueColor] setStroke]; 393 | 394 | //Move then draw again 395 | [bunnyPath applyTransform:CGAffineTransformMakeTranslation(50,0)]; 396 | [bunnyPath fill]; 397 | [bunnyPath stroke]; 398 | 399 | //Restore the previous state 400 | CGContextRestoreCState(context); 401 | 402 | //Move then draw again 403 | [bunnyPath applyTransform:CGAffineTransformMakeTranslation(50,0)]; 404 | [bunnyPath fill]; 405 | [bunnyPath stroke]; 406 | 407 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 408 | UIGraphicsEndImageContext(); 409 | ``` 410 | 411 | 图1.9 412 | 413 | ### 状态类型 414 | 一个上下文可以保存很多种的状态,不仅仅只有填充和划。每一个状态都表达了现在上下文持久的部分。表1-1列举出了可定制的状态属性,可以用来调节使用中的核心图像上下文请求,并且提供了一些图片实例粗略地展示了一下这些设置将如何改变绘制。 415 | 416 | 表1 417 | 表2 418 | 419 | 表的对照翻译: 420 | 421 | | Explanation | 解释 | 翻译 | 422 | | :-------------: |:-------------:| ----- | 423 | | Color | 颜色 | 颜色状态结合fill和stroke设置决定在上下文中如何绘制 | 424 | | Transformation matrices | 变换矩阵 | 该属性实现几何变换,允许你旋转放缩和位移,可以得到复杂的几何变换结果 | 425 | | Clipping | 剪辑 | 当你剪辑一个上下文时,创建一个形状自动排除类容。而且不仅限于圆或矩形,还可以时任意你能想到的形状 | 426 | | Line parameters | 线属性 | 线状态描述Quartz如何绘制线,这些状态包括宽度(线厚度),虚线(用虚线绘图),倾斜限制(尖角的度数),结合风格(如何表达角;风格包括倾斜,圆角,或是斜切),帽子(线段结束的地方端点是圆或方) | 427 | | Flatness | 平面度 | 这个元素决定了弯曲部分的弯曲程度,特别是数学弧度点和渲染点的最大允许距离(注:真的不会翻译这一句。。)。默认为0.6,更大会产生更多锯齿,但是会让渲染速度更快,因为需要更少的计算。 | 428 | | Antialiasing | 抗锯齿 | 该属性决定了Quartz的划线的流畅度通过取两像素之间的平均值。当然这样会使得渲染更慢,但是绘制结果在视觉上效果更好。Quartz是默认使用抗锯齿的 | 429 | | Alpha levels | 透明度 | 该属性决定了上下文绘制素材的透明度。alpha值从1全见到0全不可见,绘制材料逐渐透明 | 430 | | Text traits | 文本特性 | 该属性包含了字体,字体大小,字母间距,文本绘制模式。模式指文本如何绘制(划,填充等等)。其他细节还有字体流畅度和子像素位置等。 | 431 | | Blend modes | 混合模式 | 混合模式使用颜色和透明度来觉得新图层覆盖在已有图层上的部分的颜色,Quartz有很多复杂的混合模式,附录A详细的展示了他们 | 432 | 433 | 434 | ### 上下文坐标系 435 | 在UIKit中,坐标系原点(0,0)为屏幕左上点,然后想右和下延伸。而Quartz中,原点位于左下。 436 | 如图1-11,分别在UIKit(左)和Quartz(右)中描述了一个{ 20, 20, 40, 40}的方块。他们都是离远点20点对象,但是如你所见,出来的结果并不同。 437 | 438 | 图1.11 439 | 440 | 原点位置取决于你上下文的创建方法。若你使用UIKit方法创建上下文,原点为左上。如果使用CGBitmapContextCreate()创建,原点为左下。 441 | 442 | #### 转换上下文 443 | 你可以调整核心图像上下文到UIKit的原点。代码1-10展示这些代码,包括以下这些步骤: 444 | 1. 把CGContextRef推入UIKit图像栈 445 | 2. 通过位移转换和调整比例原有的上下文,在垂直方向转换上下文。 446 | 3. 把当前转换于上下文的当前转换矩阵(CTM)链接。该操作调整上下文坐标系模仿UIKit,从左上开始绘制。 447 | 4. 在新的坐标系中开始绘制吧。 448 | 5. 出栈。 449 | 450 | 你也可以直接使用Quartz的坐标系,不做坐标转换。然而,大多数UIKit开发者鼓励使用单坐标系,在一个熟悉的坐标系中完成绘制任务。有些开发者会创建宏来定义相同的转换方法。这可以更形象地使用QUARTZ_ON/QUARTZ_OFF。代码不会变,但开发着可以很好的在当前状态的代码中检查。 451 | 452 | 代码1-10包含了次要的转换代码,不需要你使用上下文尺寸。但说实话,这有一点点黑客,但他确实奏效,因为图片恢复会使用和上下文同样的尺寸和比例。 453 | 你同样可以获得上下文的尺寸通过方法CGBitmapContextGetHeight()和CGBitmapContextGetWidth(),通过屏幕比例分别返回像素的数量。着确保你工作在某些位图上下文中(如UIGraphicsBeginImageContextWithOptions()创建的上下文)和你在上下文中适配的屏幕尺寸 454 | 455 | ``` 456 | //Flip context by supplying the size 457 | void FlipContextVertically(CGSize size) 458 | { 459 | CGContextRef context = UIGraphicsGetCurrentContext(); 460 | if(context == NULL) 461 | { 462 | NSLog(@"Error: No context to flip"); 463 | return; 464 | } 465 | CGAffineTransform transform = CGAffineTransformIdentity; 466 | transform = CGAffineTransformScale(transform, 1.0f, -1.0f); 467 | transform = CGAffineTransformTranslate(transform, 0.0f, -size.height); 468 | CGContextConcatCTM(context,transform); 469 | } 470 | 471 | //Flip context by retrieving image 472 | void FlipImageContextVertically() 473 | { 474 | CGContextRef context = UIGraphicsGetCurrent(); 475 | if(context == NULL) 476 | { 477 | NSLog(@"Error: No context to flip"); 478 | return; 479 | } 480 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 481 | FlipContextVertically(image.size); 482 | } 483 | 484 | //Query context for size and use screen scale 485 | //to map from Quartz pixels to UIKit points 486 | CGSize GetUIKitContextSize() 487 | { 488 | CGContextRef context = UIGraphicsGetCurrentContext(); 489 | if( context == NULL) return CGSizeZero; 490 | CGSize size = CGSizeMake(CGBitmapContextGetWidth(context), 491 | CGBitmapContextGetHeight(context)); 492 | CGFloat scale = [UIScreen mainScreen].scale; 493 | return CGSizeMake(size.width/scale, size.heigth/scale); 494 | } 495 | 496 | //Initialize the UIKit context stack 497 | UIGraphicsPushContext(context); 498 | 499 | //Flip the context vertically 500 | FlipContextVertically(size); 501 | 502 | //Draw the test rectangle. It will now use the UIKit origin 503 | //instead of the Quartz origin. 504 | CGRect testRect = CGRectMake(20,20,40,40); 505 | UIBezierPath *path = [UIBezierPath bezierPathWithRect:testRect]; 506 | [greenColor set]; 507 | [path fill]; 508 | 509 | //Pop the context stack 510 | UIGraphicPopContext(); 511 | 512 | ``` 513 | ### 剪裁 514 | 剪裁(Clipping)帮助你排除所有在你上下文路径之外的绘画操作。想要裁剪,先要为你的上下文添加一个路径,然后调用CGContextClip()方法。图1-12展示了一个例子,一些带有颜色的圆仔上下文中被一个Hello形状的单词裁剪。 515 | 516 | 图1.12 517 | 518 | 代码1-11设计到剪裁的部分分为以下几步: 519 | 520 | 1. 保存图片状态。保存裁剪之前的图片状态以便稍后恢复,不过你不需要返回未裁剪时的状态,跳过这个步骤。 521 | 2. 为上下文添加路径,使用CGContextClip()进行裁剪。添加一个临时路径出存在图像上下文中,一旦储存,就会产生一个剪切的遮罩。他圈出了那些你不想绘制的上下文区域。本例使用UIBezierPath实例,他和CGContextClip()方法是不兼容的。把贝塞尔曲线的CGPath属性转换为CGPathRef可以使之兼容。 522 | 3. 裁剪后,可以操作任何标准绘图操作。在区域外绘制的素材会自动被忽略掉。 523 | 4. 结尾时,恢复图片状态。这可以帮助你返回正常的绘制不会继续被裁剪。 524 | 525 | 这段程序相当于保存,裁剪,绘制,恢复。(大声读出来吧!)第六章会介绍使用Objective-C块来在保存和存储中使用裁剪和绘制,而非使用管理状态这种明确的响应。 526 | 527 | ``` 528 | //Save the state 529 | CGContextSaveGState(context); 530 | 531 | //Add the path and clip 532 | CGContextAddPath(context, path.CGPath); 533 | CGContextClip(context); 534 | 535 | //Perform clipped drawing here 536 | 537 | //Restore the state 538 | CGContextRestoreGState(context); 539 | 540 | //Drawing done here is not clipped 541 | ``` 542 | 543 | ### 变换 544 | 545 | 图1-13中的字母序列是通过在一个圆上的各个点上绘制每个字母得到的。这个图利用了UIKit中很好的一个内嵌特性:NSString实例,他能很好的绘制在上下文中。你仅需告诉他绘制的点坐标或矩形即可,如下: 546 | 547 | ``` 548 | [@"Hello" drawAtPoint:CGPointMake(100, 50) withAttributes:@{NSFontAttributeName:font}]; 549 | ``` 550 | 这个语法在iOS7中改变,并且弃用之前的APIs。iOS7之后,你需要这样用: 551 | ``` 552 | [@"Hello" drawAtPoint:CGPointMake(100, 50) withFont:font]; 553 | ``` 554 | 图1.13 555 | 556 | 这个圆顺时针每(2 x Pi / 26)度摆放一个字母。每个x y通过计算与中心点的位移得到。下列代码通过使用r x sin(theta)和 r x cos(theta)来得到每一个字母点的位置。 557 | ``` 558 | NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 559 | for(int i=0; i<26; i++) 560 | { 561 | NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; 562 | CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; 563 | 564 | CGFloat theta = M_PI - i * (2 * M_PI /26.0); 565 | CGFloat x = center.x + r * sin(theta) - letterSize.width /2.0; 566 | CGFloat y = center.y + r * cos(theta) - letterSize.height /2.0; 567 | 568 | [letter drawAtPoint:CGPointMake(x,y) 569 | withAttributes:@{NSFontAttributeName:font}]; 570 | } 571 | ``` 572 | 这是一个还可以接受的解决办法,但是你可以通过上下文的变换更好的改进这个方法。 573 | 574 | > 注意: 高中代数是核心图像开发者的基础。复习一下sin,cos,tan等还是很必要的(注:后面又介绍了一个网站,就不翻译了) 575 | 576 | #### 变换状态 577 | 每一个上下文都存储了2D仿射变换的状态。这个变换被叫做当前变换矩阵。它定义了绘制时如何旋转放缩和位移。它提供了更为灵活和有力的方法来进一步改进绘制操作。对比图1-14和1-13,你会发现神奇的上下文变换所作的改进。 578 | 579 | 图1.14 580 | 581 | 代码1-12展示了创建上图的步骤。它包含了一系列的旋转每一个字母的变换操作。上下文的保存和恢复确保了黑体的部分可以继续在后续中使用。这些变换都是基于上下文中心点的。 582 | 他可以让上下文自由旋转在某一点周围,所以每一个字母可以精确地绘制在一个角度上。向左移动半个字母宽度的距离,确保每个字母都是中心围绕在圆弧上。 583 | ``` 584 | NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 585 | 586 | //Start drawing 587 | UIGraphicsBeginImageContext(bounds.size); 588 | CGContextRef context = UIGraphicsGetCurrentContext(); 589 | 590 | //Retrieve the center and set a radius 591 | CGPoint center = RectGetCenter(bounds); 592 | CGFloat r = center.x * 0.75f; 593 | 594 | //Start by adjusting the context origin 595 | //this affects all subsequent operations 596 | CGContextTranslateCTM(context, center.x, center.y);//(注:这句是黑体) 597 | 598 | //Iterate through the alphabet 599 | for(int i = 0; i < 26; i++) 600 | { 601 | //Retrieve the letter and measure its display size 602 | NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; 603 | CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; 604 | 605 | //Calculate the current angular offset 606 | CGFloat theta = i * (2 * M_PI /(float) 26); 607 | 608 | //Encapsulate each stage of the drawing 609 | CGContextSaveGState(context); 610 | 611 | //Rotate the context 612 | CGContextRotateCTM(context, theta); 613 | 614 | //Translate up to the edge of the radius and move left by 615 | //half the letter width, The height translation is negative 616 | //as this drawing sequence uses the UIKit coordinate system. 617 | //Transformations that move up go to lower y values. 618 | COContextTranslateCTM(context, -letterSize.width /2, -r); 619 | 620 | //Draw the letter 621 | [letter drawAtPoint:CGPointMake(0,0) 622 | withAttributes:@{NSFontAttributeName:font}]; 623 | CGContextRestoreGState(context); 624 | } 625 | 626 | //Retrieve and return the image 627 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 628 | UIGraphicsEndImageContext(); 629 | return image; 630 | ``` 631 | #### 创建更加精确的图层 632 | 一个更考究的解决方案需要绘制字母圆环避免“I”两端的空隙和“W”两端的挤压。代码1-13详细的展示创建更为考究的如1-15图所示的图片图层的方法。这个代码可以提供出色的图层布局。 633 | 开始时,计算图层的总宽度。计算每个独立字母的宽度(就像这里所做的一样)或者直接测量字符串作为一个整体。他允许你标记你的图层进度,产生一个从头到尾所占的百分比。 634 | 接下来,通过百分比调整每个字母的位置。用百分比计算出旋转的角度。 635 | 最后,如1-12一样绘制字母,你将会得到一个更具字母自然尺寸排列的图层。 636 | ``` 637 | //Calculate the full extent 638 | CGFloat fullSize = 0; 639 | for (int i = 0; i <26; i++) 640 | { 641 | NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; 642 | CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; 643 | fullSize += letterSize.width; 644 | } 645 | 646 | //Initialize the consumed space 647 | CGFloat consumedSize = 0.0f; 648 | 649 | //Iterate through each letter, consuming that width 650 | for (int i =0; i <26; i++) 651 | { 652 | //Measure each letter 653 | NSString *letter = [alphabet substringWithRange:NSMakeRange(i, 1)]; 654 | CGSize letterSize = [letter sizeWithAttributes:@{NSFontAttributeName:font}]; 655 | 656 | //Move the pointer forward, calculating the 657 | //new percentage of travel along the path 658 | consumedSize += letterSize.width/2.0f; 659 | CGFloat percent = consumedSize / fullSize; 660 | CGFloat theta = percent * 2 * M_PI; 661 | consumedSize += letterSize.width / 2.0f; 662 | 663 | //Prepare to draw the letter by saving the state 664 | CGContextSaveGState(context); 665 | 666 | //Rotate the context by the calculated angle 667 | CGContextRotateCTM(context, theta); 668 | 669 | //Move to the letter position 670 | CGContextTranslateCTM(context, -letterSize.width/2, -r); 671 | 672 | //Draw the letter 673 | [letter drawAtPoint:CGPointMake(0,0) withFont:font]; 674 | 675 | //Reset the context back to the way it was 676 | CGContextRestoreGState(context); 677 | } 678 | 679 | ``` 680 | 681 | 图1.15 682 | 683 | ### 设置线属性 684 | 每一个上下文都保存着线宽属性坐图形状态的一部分,你可以通过调用CGContextSetLineWidth()设置线宽,通过点尺寸(注:非像素而是点的意思)作为他的参数。随后的绘制的操作都会继承这个状态的改变。然而,线宽却不会应用于UIBezierPath实例中,以及主要的UIKit绘图工具。所有的UIKit路径都是通过lineWidth属性来调整划线的宽度的。不论他设置什么值都胜过上下文的设置。 685 | 来看看代码1-14吧。它创建了一个线宽为4的路径。然后又使用上下文创建了一个20点宽的线。图1-16展示了绘制的结果。如图所示,Quartz绘制的路径使用了20点的线宽,而UIKit所绘制的却是的是path的属性值。 686 | ``` 687 | UIGraphicsBeginImageContext(bounds.size); 688 | CGContextRef context = UIGraphicsGetCurrentContext(); 689 | 690 | //Build a Bezier path and set its width 691 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect 692 | cornerRadius:32]; 693 | path.lineWidth = 4; 694 | 695 | //Updating the context state to use 20-point wide lines 696 | CGContextSetLineWidth(context, 20); 697 | 698 | //Draw this path using the context state 699 | [purpleColor set]; 700 | CGContextAddPath(context, path.CGPath); 701 | CGContextStrokePath(context); 702 | 703 | //Draw the path directly through UIKit 704 | [greenColor set]; 705 | [path stroke]; 706 | ``` 707 | 708 | 图1.16 709 | 710 | UIKIt代码限制了线宽对语义上的UIBezierPath对象并且总的来说对上下文无效。这并不是一件坏事,入栈一个线宽状态来绘制形状还不如直接设置width属性给特定的对象。但也不一定是这样。这段代码是普遍的。你也可以在线段中看到这样。 711 | 712 | #### 线段 713 | 线段模式并不是像线宽那样在UIKit和Quartz中是分裂的。上下文状态中的改变也会影响到UIKit的路径,使用setLineDash:count:phase:方法。 714 | 这个代码片段会创建如图1-17那样的输出: 715 | ``` 716 | [greenColor set]; 717 | CGFloat dashes[] = {6, 2}; 718 | [path setLineDash:dashes count:2 phase:0]; 719 | [path stroke]; 720 | 721 | ``` 722 | 该代码段用了3个参数。第一个设置了线段模式(6个点长度有颜色,2个点长度无色),第二个参数为第一个参数中数字的个数(有两项,所以为2)。第三个参数定义了相位偏移。你将会在第四章中看到更多关于线段的知识。 723 | 用纯Quartz代码也可以得到一样的效果,CGContextSetLineDash()。下面的代码片段一样会得到图1-17所示的效果。 724 | ``` 725 | [greenColor set]; 726 | CGFloat dashes[] = {6, 2}; 727 | CGContextSetLineDash(context, 0, dashes, 2); 728 | [path stroke]; 729 | ``` 730 | 731 | 图1.17 732 | 733 | 734 | 735 | ### 总结 736 | 本章介绍了iOS绘图的基础:上下文,你学习了如何创建上下文,了解到不同类型的可用上下文,同时学习了如何把上下文转换为图片或是PDF文件。在iOS中,上下文提供了大量的状态控制。在你离开本章之前,这里还有如下几点,你需要思考思考: 737 | * 任何的绘画请求,都有很多解决方案来解决同样的问题。你更倾向于使用Core Graphics或者UIKit只会影响你所使用的API,不过往往没有谁是正确的答案。能绘出你需要的结果即可。 738 | * UIKit的类在不断地改进。在iOS的每一代,它都提供了越来越多的资源来跳过直接使用Quartz。UIKit APIs往往更加简洁,吝啬,且易于理解。大部分的开发者都会感到使用UIKit的方法会有多便捷。 739 | 苹果通常鼓励开发者尽可能使用高级API,仅当有高级API无法完成的任务时再使用低级API。有很多事都是低级API可以做到而高级API不行的。比方说,用带有合适的阴影的笔触来绘图,或事输出颜色空间。 740 | * 在使用C语言基础的核心框架时,是可以收工进行内存管理,是很好的习惯。理解带Ref的对象如何工作,何时如何释放,对将来相关的操作顺利运行是很好的保障。学习使用ARC风格桥接转换核心框架的系统,肯能会花费大量的时间,但是你却会获得更多收获。 741 | * 谨记:如果你方法以Copy或是Create开头,你必须释放这个元素。如果以Get开头,则不需要。且,无论如何,Create前缀的方法总是要跟着一个释放的方法。比如,创建一个颜色空间,需要有对应的CGColorSpaceRelease()。 742 | * Quartz和UIKit绘图方法都是线程安全的。苹果科技问答1637中提到iOS4开始,“在UIKit中绘制一个图像上下文是线程安全的,包括访问和操作当前的图像栈,绘制图像和字符,在第二个线程中使用颜色的字体对象”。 743 | 744 | 745 | 746 | -------------------------------------------------------------------------------- /1-绘制上下文/table1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/table1.png -------------------------------------------------------------------------------- /1-绘制上下文/table2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/1-绘制上下文/table2.png -------------------------------------------------------------------------------- /2-几何语言/2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-1.png -------------------------------------------------------------------------------- /2-几何语言/2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-2.png -------------------------------------------------------------------------------- /2-几何语言/2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-3.png -------------------------------------------------------------------------------- /2-几何语言/2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-4.png -------------------------------------------------------------------------------- /2-几何语言/2-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-5.png -------------------------------------------------------------------------------- /2-几何语言/2-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-6.png -------------------------------------------------------------------------------- /2-几何语言/2-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-7.png -------------------------------------------------------------------------------- /2-几何语言/2-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/2-8.png -------------------------------------------------------------------------------- /2-几何语言/2-几何语言.md: -------------------------------------------------------------------------------- 1 | # 几何语言 2 | 3 | 绘图和几何是无法避免地紧密相连的。为了完成复杂的绘图操作,你必须使用iOS可以表述的几何语言。本章将会带你从基础开始。首先是点-像素二分法,然后是核心结构,再然后转向UIKit对象。你将会学习这些项目都是什么,并了解他们如何在绘图中使用。 4 | 5 | ### 点和像素 6 | 在iOS中,点表示屏幕或绘图的位置。它由测量单元组成,描述了位置和绘图操作的范围。点对于物理世界测量和特殊屏幕硬件来说是不灵活的。它允许你表示独立使用中的设备上的位置信息。 7 | 点不是像素。像素是可寻址的屏幕组件,且直接和特定的硬件绑定。每个像素都独立设置了亮度和颜色值。而点则相反,对应逻辑坐标空间。 8 | 比方说,所有iPhone家族的成员都在纵向320点宽的位置展示一个对象。这些点在旧机型上为320像素宽,而在新的Retina设备上为640像素宽。而坐标系是统一的,无论你使用是Retina设备或是旧设备。 9 | 如图2-1所示,位于(160.0,240.0)的坐标,在3.5英寸的iPhone或iPod屏幕上位于中心位置,不论像素密度如何。同样的点在4英寸的iPhone或iPod上却里中心点偏上。新设备的中心变成了(160,284.0)。 10 | 在横向时,同样的点在iPhone屏幕上位于左下角。在更大的iPad屏幕上却出现在左上角。 11 | 12 | 图2.1 13 | 14 | #### 比例 15 | 16 | UIScreen类提供了一个scale属性。该属性反应了展示对象的像素密度和点系统之间的关系。屏幕比例属性用于转换用点测量的逻辑坐标空间和物理像素坐标系。Retina设备比例为2.0,非Retina设备为1.0。你可以通过如下方法检测设备是否为Retina设备: 17 | ``` 18 | - (BOOL)hasRetinaDisplay 19 | { 20 | return ([UIScreen mainScreen].scale == 2.0f); 21 | } 22 | 23 | ``` 24 | 主屏总是关联设备当前的展现。其他的屏幕可能有通过AirPlay或苹果链接线展示的屏幕。每个屏幕都有一个availableModes属性。它提供了一个数组的解决方案对象,按由低到高的顺序排列。 25 | 很多屏幕支持多种模式。比方说,VGA可能会提供6个或以上的解决方案。解决方案的数量往往是由硬件决定的。总是会至少有一种解决方案存在。但是你总是需要提供更多选择给用户。 26 | UIScreen也提供了两个很有用的展示尺寸的属性。bounds属性返回屏幕的边界矩形,通过点来测量。它给你提供了整个屏幕尺寸,不包括任何屏幕元素,比如状态条,导航条和切换条。applicationFrame属性也是用点来测量的。他包含了状态条如果状态条可见的话,提供了程序元素的窗口尺寸。 27 | 28 | #### iOS 设备 29 | 表2-1总结了iOS的设备家族,并列出了每个成员的可寻址坐标空间。这里有5个比较有代表性的成员,你只会碰见3种逻辑空间(苹果也许会在未来介绍新的几何),如下几个: 30 | 31 | * 3.5英寸iPhone设备(320.0x380.0点) 32 | * 4.0英寸iPhone设备(320.0x568.0点) 33 | * iPads (768.0x1024.0点) 34 | 35 | 表2-1-1 36 | 表2-1-2 37 | 38 | ### 视图坐标 39 | 有很多绘图代码都紧密关联着你所需绘制的视图,特别是使用drawRect:方法的时候。所有视图的原生坐标系都是左上为原点的。 40 | 在iOS7以后,可以自己通过edgesForExtenedLayout属性调整视图控制器的原点是否在跳行条上或者导航条下。默认情况,是从导航条以下开始算原点的,提供一个边对边的UI。 41 | 42 | #### Frames 和 Bounds 43 | 视图(至少)会存在于两个世界。一个视图frame属性是根据他的父视图的坐标系来确定的。该属性确定了视图在其父视图上的位置和大小。而一个视图的bounds属性在他自己的坐标系中定义,所以他的定位默认为(0,0)。(当你只使用视图的一部分时,这个定位值也是会变的,就比如说使用scroll view)。视图的frame和bounds属性时紧密相连的一对。一个改变了另一个也随之改变的。 44 | 思考图2-2,灰色视图的起始点为(80,100)。水平方向200点,垂直方向150点。他在父视图中的frame属性为{80,100,200, 150}。在自己的坐标系中,bounds属性为{0,0,200,150}。延展的值保持一致,而起始点的值不同。 45 | 46 | | | Gray View | Parent | 47 | | :-------------: |:-------------:| ----- | 48 | | Origin | {0, 0} | {80, 100} | 49 | | Circle | {66, 86} | {146, 186} | 50 | 51 | 52 | 表2-2 53 | 54 | 一个点的位置坐标是依据坐标系视情况而定的。在图2-2中,在灰色视图中一个圆包裹了一个点。在灰色视图的坐标系中,点位置为(66,86)。在他的父视图的坐标系中,点的位置坐标为(146, 186)。直接通过坐标相加进行转换,(66 + 80, 100 + 86),即(146,186)。 55 | 56 | > 注意: 一个视图的frame是通过bounds,center和其他transform计算来的。他描述了可以包括整个视图的最小矩形。 57 | 58 | #### 坐标系转换 59 | iOS SDK提供了很多坐标系转换的方法。比如说,你也许会希望吧一个点从视图坐标系转换到它的父视图的坐标系中,来确定在它的父视图的哪个位置进行绘制。你可以如下这么做: 60 | ``` 61 | CGPoint convertedPoint = [outerView convertPoint:samplePoint fromView:grayView]; 62 | ``` 63 | 你可以使用任何视图实例调用这个方法。尤其是你想要转换一个点到另一个坐标系上时(toView:)或事从另一个坐标系转移过来(fromView:),像例子中那样。 64 | 这些视图必须存在于同一个UIWindow,否则这个方法不会奏效。这些视图不需要有任何其他的相关关系了。然而,他们可以是兄弟姐妹,父子,祖先,或其他关系。这个方法会返回一个你想要得到的点坐标。 65 | 转换矩形的方法和转换点的方法也是类似的。把一个矩形从一个视图转换到另一个坐标系,使用convertRect:fromView:。转换回来,使用convertRect:toView: 66 | 67 | ### 关键结构体 68 | iOS绘图使用四个关键的结构体来定义几何基元:点,尺寸,矩形,转换。这些结构体都使用同一额单位:逻辑点。点通过CGFloat值来定义。在iOS中用float,而OS X使用double。 69 | 不像像素,固定整数点且与设备硬件无关。他们的值和亚像素精度提供的数学坐标有关。iOS绘图系统使用你习惯的数学。 70 | 71 | 你会用到的四个基元如下: 72 | * CGPoint —— 点结构由x和y组成,他们定义了逻辑位置。 73 | * CGSize —— 尺寸结构由width和height组成,他们定义了横纵轴上的延展 74 | * CGRect —— 矩形由使用点定义的origin属性和一个size属性组成。 75 | * CGAffineTransform —— 放射变换结构描述了几何项的改变——特别是,一个项目如何放置,放缩,旋转,他储存了a,b,c,d,tx和ty六个值的矩阵来定义变换。 76 | 77 | 下一章会更加深入地介绍这几项。你需要先有一个基础的几何上的认识之后,才可以更加深入地在绘图中使用他们。 78 | 79 | #### 点 80 | CGPoint储存了逻辑位置,通过定义x和y的值来确定。CGPointMake()可以很方便地通过两个参数来创建结构体: 81 | ``` 82 | struct CGPoint{ 83 | CGFloat x; 84 | CGFloat y; 85 | } 86 | ``` 87 | 这些值可以是任何浮点数的点值。负数依然有效。 88 | 89 | #### 尺寸 90 | CGSize结构体储存了两个方向上的延展,本别是width和height。使用CGSizeMake()方法创建,两值也可以为负数,然而日常中很少这么用: 91 | ``` 92 | struct CGSize{ 93 | CGFloat width; 94 | CGFloat height; 95 | } 96 | ``` 97 | 98 | #### 矩形 99 | CGRect结构体由两个子结构体组成:CGPoint定义了矩形的位置,CGSize定义了延展的大小。CGRectMake()方法用四个参数定了rect结构体。四个参数的顺序为x,y, width, height。比方说,CGRectMake(0,0, 50,100): 100 | 101 | ``` 102 | struct CGRect{ 103 | CGPoint origin; 104 | CGSize size; 105 | } 106 | ``` 107 | 调用CGRectStandardize()把size中为负的部分转换为相等的正值。 108 | 109 | #### 转换 110 | 转换是iOS几何学中最强大一个部分。他允许点从一个坐标系转移到另一个坐标系。也允许你放缩旋转,镜像,位移等等当你绘制的时候,通过保存线性和相关比例。你在第一章中调整绘图上下文位置时已经遇到了绘制中进行转换,或当你读到第五章操作路径或形装时,也会遇到。 111 | 在2D和3D图像中,转换都是很广泛的被应用到的。转换提供了复杂的架构来解决几何问题。核心图像的版本(CGAffineTransform)使用3x3的矩阵,来解决2D问题。而在3D中,使用Core Animation中的图层定义的4x4的矩阵。Quartz转换允许你进行几何的位移放缩旋转。 112 | 所有的转换都可以用如下所示的底层转换矩阵表示: 113 | 114 | 1 115 | 116 | 用C语言机构提表示为: 117 | ``` 118 | struct CGAffineTransform{ 119 | CGFloat a; 120 | CGFloat b; 121 | CGFloat c; 122 | CGFloat d; 123 | CGFloat tx; 124 | CGFloat ty; 125 | } 126 | ``` 127 | 128 | ##### 创建转换 129 | 不像其他核心图像结构体,你几乎很少直接使用放射变换。大部分人甚至都不需要使用CGAffineTransformMake()(需要六个参数)来创建结构体。 130 | 取而代之的是,CGAffineTransformMakeScale(),CGAffineTransformMakeRotate()或是CGAffineTransformMakeTranslation()。这些方法分别通过参数创建了放缩,旋转,位移矩阵。这些方法可以允许你根据你想完成的变换执行对应的操作。需要旋转一个对象?直接输入特定的选择程度即可。需要移动对象?直接输入移动位移即可。每一个方法创建了一个transform来完成指定操作。 131 | 132 | ##### 分层转换 133 | 相关方法允许你把一个转换覆盖在另一个转换之上,完成一个复杂的变换。不像第一个函数,他们只取一部分变换作为参数。他们定义一个变换会层叠另一个变换在顶上。不像“make”方法,他们总是按一定序列摆放的,且每一个变换的结果都会传递到下一个变换。比方说,你也许想创建一个围绕他的中心选择和放缩的事物,你可以如下列代码这样处理: 134 | ``` 135 | CGAffineTransform t = CGAffineTransformIdentity; 136 | t = CGAffineTransformTranslate(t, -center.x, -center.y); 137 | t = CGAffineTransformRotate(t, M_PI_4); 138 | t = CGAffineTransformScale(t, 1.5, 1.5); 139 | t = CGAffineTransformTranslate(t ,center.x, center.y); 140 | ``` 141 | 开始是一个默认的变换。默认的transfrom是个基础,就像加法中的0或者乘法中的1一样。在使用几何对象时,一般以此开始。是用来也确保一个固定的起始点以服务于之后的操作。 142 | 因为转换应用于对象的起始点,并不是中心点。你需要把中心点移动到起始位置。放缩和选择总是与坐标为(0,0)的点关联。如果你想让他们关联到另一个点,比如说一个视图或是路径的中心点,你需要把你想要的点移动到0,0的位置。这一连串的变换都会存储在唯一的变换t中。 143 | 144 | ##### 曝露转换 145 | UIKit库提供了大量的辅助图像和绘制操作的方法。还包括一些转为放射变换特定的方法。你可以通过UIKit的NSStringFromCGAffineTransform()方法打印出一个视图的变换。下面展示的是一个视图选择45度并放缩1.5倍的变换打印: 146 | ``` 147 | 2013-03-31 09:43:20.837 HelloWorld[41450:c70] [1.06066, 1.06066, -1.06066, 1.06066, 0, 0] 148 | ``` 149 | 这里没有做中心在定位,所以只有两个操作。 150 | 这些数字并不是那么直观。特别是,他没有直接告诉你放缩了多少选择了多少。不过幸运的是,又很简单方法可以处理,可直接展示更加直观的值。代码2-1展示如何计算X,Y方向上的放缩,以及旋转角度。 151 | 当然,没有必要去计算translation(位移)的值,因为这些值直观地储存在tx和ty中。 152 | ``` 153 | //Extract the x scale from transform 154 | CGFloat TransformGetXScale(CGAffineTransform t) 155 | { 156 | retrun sqrt(t.a * t.a + t.c * t.c); 157 | } 158 | 159 | //Extract the y scale from transform 160 | CGFloat TransformGetYScale(CGAffineTransform t) 161 | { 162 | retrun sqrt(t.b * t.b + t.d * t.d); 163 | } 164 | 165 | //Extract the rotation in radians 166 | CGFloat TransformGetRotation(CGAffineTransform t) 167 | { 168 | return atan2f(t.b, t.a); 169 | } 170 | ``` 171 | #### 预定义常量 172 | 每一个核心图像结构体都有预定义的常量。常见如下: 173 | * Geometric zeros —— 这个常量提供默认的零值。CGPointZero坐标(0,0)的点常量,CGSizeZero提供宽高均为0的尺寸常量。CGRectZero等价于CGRectMake(0,0,0,0)。 174 | * Geometric identity —— CGAffineTransformIdentity 提供一个常量默认转换。应用于任何几何元素,这个变换都会以相同的开始值返回。 175 | * Geomeric infinity —— CGRectInfinite是一个无穷范围的矩形。宽和高都被设置为CGFLOAT_MAX, 可达的最大的浮点数。起始点设置为可达的最小负数(-CGFLOAT_MAX/2)。 176 | * Geometric nulls —— CGRectNull 和CGRectZero有区别,他没有位置信息。CGRectZero的位置是(0,0)CGRectNull的位置是(INFINITY,INFINITY)。当你请求矩形交叉或分离时会用到这个矩形。 177 | 任何与CGRectNull结合的矩形都会返回原始的rect。例如,{10,10,10,10}与CGRectNull结合结果仍然是{10,10,10,10}.而与CGRectZero结合这会变为{0, 0, 20, 20},原点变为(0,0)且翻倍尺寸,改变位置。 178 | #### 转换为对象 179 | 因为几何元素和结构体并不是对象,把他们与标准Objective-C结合往往是困难的。你不能把一个尺寸或是矩形添加到NSArray或是一个字典中。你也无法给一个点或者变换设置初始值。正因如此,核心图像和核心框架提供方法和类来转换和压缩结构体为一个对象。对常见的几何结构体对象解决方案为字符串,字典和值。 180 | ##### strings 181 | 通过2-2表中的一系列方法,可以把结构体转换为一个字符串。这些便捷的方法允许你通过易读的格式来打印信息和储存结构体到文件,通过很好的定义和很容易恢复的代码。一个转换过的点或者尺寸会显示成这样:{5, 2}.(NSStringFromCGPoint(CGPointMake(5,2))) 182 | > 字符串是打印debugging信息最有用的。 183 | 184 | 表2-2 185 | 186 | ##### 字典 187 | 字典提供了另一种转换几何结构体到可以储存和打印的对象的方法。表2-3所示,它被限制只能转换点,尺寸和矩形。而且,他返回的是CFDictionaryRef实例,你还需要把结果桥接为NSDictionary对象。 188 | > 字典在储存用户定义的结构体时是最有用的。 189 | 190 | 表2-3 191 | 192 | 下列代码转换一个矩形到一个Cocoa Touch字典,然后转换回CGRect并打印: 193 | ``` 194 | //Conert CGRect to NSDictionary representation 195 | CGRect textRect = CGRectMake(1, 2, 3, 4); 196 | NSDictionary *dict = (__brigde_transfer NSDictionary *)CGRectCreateDictionaryRepresentation(testRect); 197 | NSLog(@"Dictionary: %@", dict); 198 | 199 | //Convert NSDictionary representation to CGRect 200 | CGRect outputRect; 201 | BOOL success = CGRectMakeWithDictionaryRepresentation(__bridge CFDictionaryRef)dict, &outputRect); 202 | if(success) 203 | NSLog(@"Rect: %@",NSStringFromCGRect(outputRect)); 204 | ``` 205 | 下面是上列代码的输出, 如你所见矩形被储存,然后重新取出: 206 | ``` 207 | 2013-04-02 08:23:07.323 HelloWorld [62600:c07] Dictionary:{ 208 | Height =4; 209 | Width =3; 210 | X = 1; 211 | Y = 2; 212 | } 213 | 2013-04-02 08:23:07.323 HelloWorld[62600:c07] 214 | Rect:{{1,2},{3,4}} 215 | ``` 216 | > 注意:CGRectMakeWithDictionaryRepresentation()有一个bug,又一些在OS X上创建的值在iOS中不可读。 217 | 218 | ##### 值 219 | NSValue类提供了一个C语言数据的容器。他可以储存数量类型(像是integers和floats),点和结构体。UIKit扩大普通的NSValue行为为一个压缩的核心图像基元。 当一个数量类型转为值类型后,你可以添加几何基元到Objective-C 像其他对象那样收集和处理。 220 | > 值在添加结构体到数组和字典进行应用内计算的时候最有用 221 | 222 | 表2-4 223 | 224 | > 注意:NSCoder提供UIKit定制的几何转码和解码方法,提供给CGPoint,CGSize,CGRect和CGAffineTransform。这些方法帮助你储存和恢复你给予特殊key的几何结构体。 225 | 226 | #### 几何测试 227 | 核心图像提供一系列的几何测试方法,如表2-5所示。这些项目可以让你比较其他的项目或是检查你可能遇到的特殊情况。这些方法的名字很好的解释了他们的用途。 228 | 229 | 表2-5 230 | 表2-5 231 | 232 | #### 其他基础方法 233 | 这些最后还有一些你可能需要了解的方法: 234 | * CGRectInset(rect, xinset, yinset) —— 这个方法会得到一个与原矩形同中心但更大或者更小的矩形。inset为正则变小,反之变大。这个方法在移动绘制图形和子图像离开边界来提供一个空白空间是会显得很有用。 235 | * CGRectOffset (rect, xoffset, yoffset) —— 这个方法会得到一个与原矩形偏离x,y距离的矩形。Offset可以很好的应用于拖动矩形在屏幕上移动,或是创建一个阴影效果。 236 | * CGRectGetMidX(rect)和 CGRectGetMidY(rect) —— 这两个方法返回矩形x方向和y方向上的中心坐标。这个方法可以很容易的获取bounds或者frames的中心点。相关方法还有minX,maxX,minY,maxY,width和height。中心点可以很好的在绘图中心项中使用。 237 | * CGRectUnion(rect1,rect2) —— 这个方法返回一个可以所有源矩形都包括进去的最小矩形。这个方法可以帮你确定你所绘制的元素最小的边界盒子。这个方法可以把你所有的绘制路径和元素结合起来并且作为他们的背板。 238 | * CGRectIntersection(rect1,rect2) —— 这个方法返回两个矩形的交叉部分,若无交叉则返回CGRectNull。交叉可以在使用AutoLayout计算矩形插入时起到很好的作用。路径边界和图像边界之间的交叉,可以得到两者间固有的内容。 239 | * CGRectIntegral(rect) —— 这个方法把源矩形转换为整数。所有的orign值都会四舍五入到整数。而size值则会向上舍入。如此来确保新的矩形会把原始的矩形完整的包括进去。整数话矩形可以为绘制提速。视图绘制通过像素边界,这样操作可以更好的减少锯齿和模糊。 240 | * CGRectStandardize(rect) —— 这个方法会返回一个正数宽高值的基础矩形。标准化可以帮助你更好地简化数学运算当操作一些交叉绘制,特别当用户交叉在左上中而非右下时。 241 | * CGRectDivide(rect, &sliceRect, &remainderRect, amount, edge) —— 这个方法是核心图像这些方法中最复杂的一个了,但是它也是举足轻重的。除法可以交叉切割一个矩形成一个部分,以便于你可以细化绘制区域。 242 | 243 | ### 使用CGRectDivide() 244 | CGRectDivide()方法是极其便利的。它提供了真地非常简单的方法来分割和细化矩形成几部分。每一步,你都会定义需要分割多少,分割哪一部分。你可以用从任何边界进行分割,包括CGRectMinXEdge,CGRectMinYEdge,CGRectMaxXEdge和CGRectMaxYEdge。 245 | 一系列的代码之后会得到如图2-3所示的图案。代码2-2展示了具体如何实现。这段代码先是分割出矩形左边的一小部分,然后纵轴对半分割剩下的部分。然后再在剩下的部分左右两侧再分出一小块。 246 | 247 | 图2-3 248 | 249 | ``` 250 | UIBezierPath *path; 251 | CGRect remainder; 252 | CGRect slice; 253 | 254 | //Slice a section off the left and color it orange 255 | CGRectDivide(rect, &slice, &remaider, 80, CGRectMinXEdge); 256 | [[UIColor orangeColor] set]; 257 | path = [UIBezierPath bezierPathWithRect:slice]; 258 | [path fill]; 259 | 260 | //slice the other portion in half horizontally 261 | rect = remainder; 262 | CGRectDivide(rect, &slice, &remainder, remainder.size.height / 2, CGRectMinYEdge); 263 | 264 | //Tint the sliced portion purple 265 | [[UIColor purpleColor] set]; 266 | path = [UIBezierPath bezierPathWithRect:slice]; 267 | [path fill]; 268 | 269 | //Slice a 20-point segment from the bottom left 270 | //Draw it in gray 271 | Rect = remainder; 272 | CGRectDivide(rect, &slice, &remainder, 20, CGRectMinXEdge); 273 | [[UIColor grayColor] set]; 274 | path = [UIBezierPath bezierPathWithRect:slice]; 275 | [path fill]; 276 | 277 | //And another 20-point segment from the bottom right, 278 | //Draw it in gray 279 | rect = remainder; 280 | CGRectDivide(rect, &slice, &remainder, 20, CGRectMaxXEdge); 281 | //Use same color on the right 282 | path = [UIBezierPath bezierPathWithRect:slice]; 283 | [path fill]; 284 | 285 | //Fill the rest in brown 286 | [[UIColor brownColor] set]; 287 | path = [UIBezierPath bezierPathWithRect:remainder]; 288 | [path fill]; 289 | 290 | ``` 291 | 292 | ### 矩形公用自定义方法 293 | 你使用CGRectMake()来创建frames,bounds或者其他矩形参数。它接收4个浮点数参数x,y,width和height。这是Quartz绘图中最为重要的一个方法。 294 | 但常常,你总是想通过你经常使用的对象,如points和size,来直接创建矩形。但你可以通过组合域来重写方法。你也行会发现有一个实用的简化方法是多么有用,通过使用结构体为参数。如代码2-3: 295 | ``` 296 | CGRect RectMakeRect(CGPoint origin,CGSize size) 297 | { 298 | return (CGRect){.origin = origin, .size = size}; 299 | } 300 | ``` 301 | 出奇意料地,Quartz没有一个内建方法来获取矩形的中心点。虽然你可以很容易地通过核心图像方法取到x,y的中点。直接通过rect返回一个点坐标会更加方便一些。代码2-4是我自己在绘图工作中经常会使用到的。 302 | ``` 303 | CGPoint RectGetCenter(CGRect rect) 304 | { 305 | return CGPointMake(CGRectGetMidX(rect),CGRectGetMidY(rect)); 306 | } 307 | ``` 308 | 围绕着一个中心点来创建一个矩形也是常常会遇到的挑战。比方说,你想让一个文本中心对齐某点,或事围绕一个点来摆放某个形状。代码2-5即为实现方法。你提供一个中心点和一个尺寸,返回的矩形会描述你的目标位置。 309 | ``` 310 | CGRect RectAroundCenter(CGPoint center, CGSize size) 311 | { 312 | CGFloat halfWidth = size.width / 2.f; 313 | CGFloat halfHeight = size.height / 2.f; 314 | return CGRectMake(center.x - halfWidth, center.y - halfHeight, size.width, size.height); 315 | } 316 | ``` 317 | 代码2-6使用代码2-4和2-5的结合来给指定矩形绘制一个中心对齐的字符串。它计算了字符串的尺寸(使用iOS7的API),然后和矩形的中心一起来创建了一个中心围绕的对象。图2-4即为效果。 318 | ``` 319 | NSString *string = @"Hello World"; 320 | UIFont *font = [UIFont fontWithName:@"HelveticaName" size:48]; 321 | 322 | //Calculate string size 323 | CGSize stringSize = [string sizeWithAttributes:[NSFontAttributeName:font]]; 324 | 325 | //Find the target rectangle 326 | CGRect target = RectAroundCenter(RectGetCenter(grayRectangle), stringSize); 327 | 328 | //Draw the string 329 | [greenColor set]; 330 | [string drawInRect:target withFont:font]; 331 | ``` 332 | 333 | 图2-4 334 | 335 | 另一个解决中心对齐的方法在代码2-7中展示。它使用一个已存在的矩形和它的中心来得到另一个矩形,而非通过尺寸和目标点。你可以使用这个方法通过Bezier曲线得到的bounds矩形来计算。通过下列方法来让路径位于一个矩形中心处。 336 | ``` 337 | CGRect RectCenteredInRect(CGRect rect, CGRect mainRect) 338 | { 339 | CGFloat dx = CGRectGetMidX(mainRect) - CGRectGetMidX(rect); 340 | CGFloat dy = CGRectGetMidY(mainRect) - CGRectGetMidY(rect); 341 | return CGRectOffset(rect , dx, dy); 342 | } 343 | ``` 344 | ### 适配和填充 345 | 往往,你需要重新改变绘制的尺寸来适配更小或更大的空间,而非使用他的原始尺寸。为了完成这些,你需要计算绘制的目标,不论你是在绘制路径,图像或是上下文。下面有四个基础程序你需要完成:中心对齐,适配,填充,压缩。 346 | 如果你曾经使用过view content mode,这些代码看起来会比较相似。当绘制的时候,你会使用和UIKit一样的填充视图和上下文的策略。他们对应的填充策略分别是:center,scale aspect fit,scale aspect fill 和 scale to fill。 347 | 348 | #### 中心对齐 349 | 中心对齐会按照矩形的原始比例摆放视图,直接摆放在目标的中心点位置。素材比展示区域大会被裁剪。素材比区域小则会修边(在四边都会留下多余的区域,在绘制中叫做“windowboxing”)图2-5展示了这两种情况。 350 | 在每个情况中,图像都是用代码2-5中的RectAroundCenter()来完成中心对齐的。在外部的大图会被裁剪,而内部小图则会留下很多额外的空间。 351 | 352 | 图2-5 353 | 354 | #### 适配 355 | 当你需要适配一个项目到某个目标时,你需要保持它原有的比例并让它的所有部分都可见。由于需要维持原有比例,结果也并不会是全覆盖的,也往往会留下一些额外的空间。 356 | 在物理世界中,修边是指在一个框架图中使用的背景或边界。这个背景或边界在画和物理尺寸中留下空间。这里,我使用一个“matting(修边)”来表示绘图区域和目标矩形之间的额外区域。 357 | 图2-6展示了这种适配。灰色背景为目标矩形。淡紫色背景是需要适配的图案通过2-8代码的计算得到的结果。除非两者比例恰好切合,绘制区域需要做中心对齐操作,把额外的区域留在上下或左右两端。 358 | 359 | 图2-6 360 | 361 | ``` 362 | //Multiply the size components by the factor 363 | CGSize SizeScaleByFactor(CGSize aSize, CGFloat factor) 364 | { 365 | return CGSizeMake(aSize.width * factor, aSize.height * factor); 366 | } 367 | 368 | //Calculate scale for fitting a size to a destination 369 | CGFloat AspectScaleFit(CGSize sourceSize, CGRect destRect) 370 | { 371 | CGSize destSize = destRect.size; 372 | CGFloat scaleW = destSize.width / sourceSize.width; 373 | CGFloat scaleH = destSize.height / sourceSize.height; 374 | return MIN(scaleW,scaleH); 375 | } 376 | 377 | //Return a rect fitting a source to a destination 378 | CGRect RectByFittingInRect(CGRect sourceRect, CGRect destinationRect) 379 | { 380 | CGFloat aspect = AspectScaleFit(sourceRect.size, destinationRect); 381 | CGSize targetSize = SizeScaleByFactor(sourceRect.size, aspect); 382 | return RectAroundCenter(RectGetCenter(destinationRect), targetSize); 383 | } 384 | ``` 385 | 386 | #### 填充 387 | 填充,如图2-7所示,确保你的目标区域每一个像素都有源图像等一部分。不要被绘图的“fill(填充,用特定颜色或图案填充路径)”操作迷惑,这个操作是允许像2-6那样的额外像素区域的。 388 | 为了完成这个操作,填充或扩充或缩放源矩形来精确地覆盖在目标上。他会裁剪掉所有在目标以外的部分,不论是在上下或是左右。结果矩形会是中心对齐的,但只有一个维度会完整的展示出来。 389 | 代码2-9计算出来目标目标矩形的过程,图2-7展示了结果。在左侧的图,耳朵和脚将会被裁剪掉。右侧则裁剪掉鼻子和尾巴。除非图片比例和目标比例完全一致才不会被裁剪,填充通过裁剪在抵消适配的多余框。 390 | 391 | 图2-7 392 | 393 | ``` 394 | //Calculate scale for filling a destination 395 | CGFloat AspectScaleFill(CGSize sourceSize, CGRect destRect) 396 | { 397 | CGSize destSize = destRect.size; 398 | CGFloat scaleW = destSize.width / sourceSize.width; 399 | CGFloat scaleH = destSize.height / sourceSize.height; 400 | return MAX(scaleW, scaleH); 401 | } 402 | 403 | //return a rect that fills the destination 404 | CGRect RectByFillingRect(CGRect sourceRect,CGRect destinationRect) 405 | { 406 | CGFloat aspect = AspectScaleFill(sourceRect.size, destinationRect); 407 | CGSize targetSize = SizeScaleByFactor(sourceRect.size, aspect); 408 | return RectAroundCenter(RectGetCenter(destinationRect),targetSize); 409 | } 410 | ``` 411 | 412 | #### 压缩 413 | 压缩会调整源图像的比例来适配到整个目标区域。目标区域的每一个像素都会有源图像里的内容。如图2-8所示。素材会在一个方向上重新设置尺寸,以便可以容纳得下。这里,兔子图像压缩了横向来匹配目标矩形。 414 | 不像之前的适配风格,压缩不需要额外的计算。直接绘制即可,Quartz会做其他剩下的工作。 415 | 416 | 图2-8 417 | 418 | ### 总结 419 | 本章介绍了一些基本的术语和一些操作核心绘图的方法。你已经了解了像素和点的区别,探索了常见的数据类型,学习了如何计算绘图位置。这里有一些最后需要注意的地方,在你离开本章之前可以进行思考: 420 | 421 | * 在编写代码是总是要把屏幕比例考虑进去。相关的设备如iPad2和第一代iPad mini没有使用Retina屏幕。专注于逻辑空间(点)而不是设备空间(像素)可以让你更灵活地支持更新和新的设备几何。 422 | * 大部分绘制都是相互关联的,尤其是当你需要触摸操作时。学习如何把点从一个坐标系转移到另一个坐标系能更好的按照你的想法来定位,而不是定位到继承中。 423 | * 放射变换总是被看成是“其他的”核心图像结构体,但是它和其他更常见的兄弟(点,尺寸和矩形)一样重要。变换是十分强大的可以很大程度上简化绘制操作。如果你有多余的时间,很值得花费一些经历来学习变换和矩阵数学(线性代数)。Khan Academy(www.khanacademy.org) 在这个课题上提供了很好的教程。 424 | * 一个强健的几何方法代码库可以在很多项目中都使用到。基础几何是永远不会过时的,相似的任务总是会反复出现。学会如何中心对齐矩阵和计算适配和填充目标会在很多时候节约你的时间,并且保证代码长久高效运行。 425 | 426 | -------------------------------------------------------------------------------- /2-几何语言/other1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/other1.png -------------------------------------------------------------------------------- /2-几何语言/table2-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-1-1.png -------------------------------------------------------------------------------- /2-几何语言/table2-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-1-2.png -------------------------------------------------------------------------------- /2-几何语言/table2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-2.png -------------------------------------------------------------------------------- /2-几何语言/table2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-3.png -------------------------------------------------------------------------------- /2-几何语言/table2-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-4.png -------------------------------------------------------------------------------- /2-几何语言/table2-5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-5-1.png -------------------------------------------------------------------------------- /2-几何语言/table2-5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/2-几何语言/table2-5-2.png -------------------------------------------------------------------------------- /3-绘制图像/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-1.png -------------------------------------------------------------------------------- /3-绘制图像/3-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-10.png -------------------------------------------------------------------------------- /3-绘制图像/3-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-11.png -------------------------------------------------------------------------------- /3-绘制图像/3-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-12.png -------------------------------------------------------------------------------- /3-绘制图像/3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-2.png -------------------------------------------------------------------------------- /3-绘制图像/3-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-3.png -------------------------------------------------------------------------------- /3-绘制图像/3-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-4.png -------------------------------------------------------------------------------- /3-绘制图像/3-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-5.png -------------------------------------------------------------------------------- /3-绘制图像/3-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-6.png -------------------------------------------------------------------------------- /3-绘制图像/3-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-7.png -------------------------------------------------------------------------------- /3-绘制图像/3-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-8.png -------------------------------------------------------------------------------- /3-绘制图像/3-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/3-绘制图像/3-9.png -------------------------------------------------------------------------------- /3-绘制图像/3-绘制图像.md: -------------------------------------------------------------------------------- 1 | # 绘制图像 2 | 3 | 本章介绍图像绘制,它将会涉及到在一个图像上下文创建,调整,恢复事物。你将会用到很多关于图像,绘图和iOS相关的知识。你可以在图像上下文中渲染一个图像并改变他们的版本。你也可以生产缩略图或是取到图片的一部分。你也可以创建事物来根据按钮或其他Auto Layout对象来适当地收缩。上下文提供方法转变图像实例为数据对象。允许你应用图片处理技术以便融入结果到你的用户界面中。本章,你会学习到常见的图像绘制任务并且找到他们的解决方案。 4 | 5 | ### UIKit Images 6 | 7 | UIKit图像总是围绕着UIImage类。它是一个强大且灵活的类,并且很好的隐藏了内部实现细节,以便可以使用很少的代码来完成很多展示任务。它最常用的代码是从文件中倒入数据并把数据表示的图象添加到UIImageView实例中,下面有一个例子: 8 | ``` 9 | 10 | UIImage *image = [UIImage imageNamed:@"myImage"]; 11 | myImageView.image = image; 12 | 13 | ``` 14 | 当然也不限制你去夹在外部数据来导入图像。iOS允许你在需要的时刻用特定的方法用代码创建图像。代码3-1是一个不那么重要的例子,它创建一个新的UIImage实例,设置了颜色和尺寸。函数返回了一个颜色样本。 15 | 为了完成这些,代码3-1创建了一个图像上下文。然后设置了一个颜色并且通过UIRectFill()填充上下文。然后恢复并从上下文返回一个新的图像。 16 | 代码3-1展示了基础的绘制骨架。在这个方法的绘制颜色矩形的地方,你可以绘制你自己的“蒙娜丽莎”。通过你自己的自定义绘制程序并设置符合你的app要求的绘制尺寸。 17 | ``` 18 | UIImage *SwatchWithColor(UIColor *color, CGFloat side) 19 | { 20 | //Create image context (using the main screen scale) 21 | UIGraphicsBeginImageContextWithOptions(CGSizeMake(side,side), YES, 0.0); 22 | 23 | //Perform drawing 24 | [color setFill]; 25 | UIRectFill(CGRectMake(0, 0, side, side)); 26 | 27 | //Retrieve image 28 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 29 | UIGraphicsEndImageContext(); 30 | return image; 31 | } 32 | ``` 33 | 这里有一些其他的有关图像的事情你需要记住: 34 | * 你通过size属性来查询图像的长宽。size返回点单位而非像素单位。所以返回点值也许会在Retina设备中变为双倍: 35 | ``` 36 | UIImage *swatch = SwatchWithColor(greenColor, 120); 37 | NSLog(@"%@",NSStringFromCGSize(swatch.size)); 38 | ``` 39 | * 图像可以在PNG数据或是JPEG数据中相互转换,通过使用UIImagePNGRepresentation()和UIImageJPEGRepresentation()方法。这两个方法返回NSData对象,其中包括了压缩后的图像数据。 40 | * 你可以通过CGImage属性来恢复一个图像的Quartz展示。UIImage基本上对Core Graphics和Core Image来说不怎么有用。很多核心图像的方法都需要CGImage。当然这个属性也不能用于核心图像的创建,你还必须转换底层的CIImage到CGImage以便于在核心图像中使用。 41 | 42 | > UIImage支持TIFF,JPEG,GIF,PNG,DIB(BMP),ICO,CUR和XBM格式,你也可以通过使用ImageIO框架来导入其他格式(如RAW)。 43 | 44 | ### 创建缩略图。 45 | 通过创建缩略图,你可以把一个大图实例转换为一个小图。缩略图可以嵌入到表视图单元格里,联系人总结(注:我也不太懂是什么,有了解的同学可以偷偷告诉我。。。),和一些其他图片起到重要作用的地方。第二章有介绍仅提取图片一部分的方法。缩略图提供了一个实际的,图片定向为其服务,同时为简单的图像绘制提供一个很好的起点。 46 | 你可以通过创建一个想要得到的图像上下文尺寸来创建缩略图,如100x100。使用drawInRect:来绘制源图像到上下文中。以得到一个新的缩略图结束: 47 | ``` 48 | UIImage *image = [UIImage imageNamed:@"myImage"]; 49 | [image drawInRect:destinationRect]; 50 | UIImage *thumbnail = UIGraphicsGetImageFromCurrentImageContext(); 51 | ``` 52 | 得到合适缩略图的关键是纵横比。不论你要划或是填充,我们都希望能保留图片的内部特征而不扭曲变形。图3-1展示了一个悬崖的图。图片的长度要比宽度高——1933像素宽和2833像素高。缩略图展示在右侧,它并没有过多关注纵横比问题。因为如此,缩略图的纵向被挤压了。 53 | 这也不算是一个难看的结果——实际上,如果你们有看到左侧的图,你可能根本不会发现问题——但这毕竟不是一个精确的结果。使用这样的图像会有很大的出现错误图像陈列的风险。也许不会跳出错误,但是很明确地会对对比例敏感的用户产生困扰。 54 | 55 | 图3.1 56 | 57 | 图3-2展示了一个合适的缩略图应该如何。并非直接绘制到目标中,它计算了矩形填充(左侧)和适配(右侧)目标区域。代码3-2解释了不同之处,创建了填充或是适配的矩形来绘制,而不是直接绘制。 58 | 59 | 图3.2 60 | 61 | ``` 62 | UIImage *BuildThumbnail(UIImage *sourceImage,CGSize targetSize,BOOL useFitting) 63 | { 64 | UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0, 0); 65 | 66 | //Establish the output thumbnail rectangle 67 | CGRect targetRect = SizeMakeRect(targetSize); 68 | 69 | //Create the source image`s bounding rectangle 70 | CGRect naturalRoot = (CGRect){.size = sourceImage.size}; 71 | 72 | //Calculate fitting or filling destination rectangle 73 | //See Chapter 2 for a discussion on these functions 74 | CGRect destinationRect = useFitting ? RectByFittingRect(naturalRect, targetRect) : RectByFillingRect(naturalRect, targetRect); 75 | 76 | //Draw the new thumbnail 77 | [sourceImage drawInRect:destinationRect]; 78 | 79 | //Retrieve and return the new image 80 | UIImage *thumbnail = UIGraphicsGetImageFromCurrentImageContext(); 81 | UIGraphicsEndImageContext(); 82 | return thumbnail; 83 | } 84 | ``` 85 | 86 | ### 提取子图像 87 | 不像缩略图那样,会把图像数据变为一个更小的版本。子图像会取到源图像的一部分并且按照一样的标准。图3-3展示了一个展示图像左上侧雪貂头部特写的子视图。倍放大的子视图突出了取到的那一部分。如你所见,因为放大的原因,图像也变模糊了。 88 | 89 | 图3.3 90 | 91 | 代码3-3展示详细的实现代码。使用简单的Quartz方法CGImageCreateWithImageInRect()来从源图像中创建新的图像。使用Quartz而不是UIKit是因为这里已经有一个创建好的方法了。 92 | 当你使用核心图像方法时,矩形会自动代表你调整像素线使用CGRectIntegral()。然后会被原始图像矩形分割,所以不会出现超出原始图像的部分。这帮你节省了很多的工作量。你所需要做的仅仅是把CGImageRef转换成为UIImage实例。 93 | 但是当你在Retina体系中使用或者是在从Quartz坐标中取出数据时缺点也会暴露出来。因为如此,我在代码3-3放入了第二个方法,一个完全由UIKit来完成的方法,避免了坐标的转换。这个方法所用的相关参数都是以点为单位的,而非像素。这在你询问一个图像的边界然后在起中心创建矩形时显得尤为重要。如果你忘记进行点到像素转换到话,在Retina中“中心”会往左上角偏移。但是始终使用UIKit的话,你就可以回避全部的问题,确保你所取的部分是你想要取到的精确的部分。 94 | ``` 95 | UIImage *ExtractRectFromImage(UIImage *sourceImage, CGRect subRect) 96 | { 97 | //Extract image 98 | CGImageRef imageRef = CGImageCreateWithImageInRect(sourceImage.CGImage, subRect); 99 | if(imageRef != NULL) 100 | { 101 | UIImage *output = [UIImage imageWithCGImage:imageRef]; 102 | CGImageRelease(imageRef); 103 | return output; 104 | } 105 | 106 | NSLog(@"Error: Unable to extract subimage"); 107 | return nil; 108 | } 109 | 110 | //This is a little less flaky 111 | //when moving to and from Retina images 112 | UIImage *ExtractSubimageFromRect(UIImage *sourceImage,CGRect rect) 113 | { 114 | UIGraphicsBeginImageContextWithOptions(rect.size, NO, 1); 115 | CGRect destRect = CGRectMake(-rect.origin.x, -rect.origin.y, sourceImage.size.weight, sourceImage.size.height); 116 | [sourceImage drawInRect:destRect]; 117 | UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); 118 | UIGraphicsEndImageContext(); 119 | return newImage; 120 | } 121 | ``` 122 | ### 转换图像为灰度图 123 | 图3-4展示了黑熊的图像,图片中心有一个灰度框,所有的其他颜色都被移除了。 124 | 125 | 图3.4 126 | 127 | 为了得到这个图像,我绘制了黑熊图两次。第一次绘制,我绘制了整个图像,而第二次裁剪了图像中间的矩形的部分并把他转换为灰度图。我在原图上方新画了一版,并给灰度图像添加了一个黑色边界。 128 | 涉及到的步骤如下: 129 | ``` 130 | //Clip the context 131 | CGContextSaveGState(context); 132 | CGRect insetRect = RectInsetByPercent(destinationRect, 0.40); 133 | UIRectClip(insetRect); 134 | 135 | //Draw the grayscale version 136 | [GrayscaleVersionOfImage(sourceImage) drawInRect:destinationRect]; 137 | CGContextRestoreGState(context); 138 | 139 | //Outline the border between the two versions 140 | UIRectFrame(insetRect); 141 | ``` 142 | 如何处理灰度图的细节在下面的代码3-4中有写。GrayscaleVersionOfImage()函数使用原图来创建一个新的上下文来完成任务。“设备灰色”每个像素占用一个字节且没有透明度信息。他形成了一个新的绘图区域只可以显示灰度结果。 143 | 在真实世界中,当你用紫色蜡笔画画的时候,所有紫色接触的地方都会是紫色。而在灰度图中绘制却像是拍一张黑白照片。不论你用什么颜色绘制,出来的结果都是灰色的,它会根据你的画笔的亮度来调整灰度,而不会设计任何色彩。 144 | 在所有上下文中,恢复结果和储存数据都取决于你。CGBitmapContextCreateImage()可以绘制源图像并且重新取到灰度版本。这个方法和在位图上下文中使用UIGraphicsGetImageFromCurrentImageContext()类似,仅是多了一点内存的管理。 145 | 以一个保存原图亮度值的UIImage结束,而不是颜色。Quartz会代表你处理所有的细节工作。你不需要分开计算每个像素的亮度等级对应的灰度。你只需要输入目标特性即可(尺寸和颜色空间),剩下的都会帮你完成。这真是一个简单的处理图像的方法啊。 146 | ``` 147 | UIImage *GrayscaleVersionOfImage(UIImage *sourceImage) 148 | { 149 | //Establish grayscale color space 150 | CGColorSpaceRef colorSpace = CGColorSpaceeCreateDeviceGray(); 151 | if(colorSpace == NULL) 152 | { 153 | NSLog(@"Error creating grayscale color space"); 154 | return nil; 155 | } 156 | 157 | //Extents are integers 158 | int width = sourceImage.size.width; 159 | int height = sourceImage.size.height; 160 | 161 | //Build context: one byte per pixel, no alpha 162 | CGContextRef context = CGBitmapContextCreate(NULL,width,height, 163 | 8,//8 bits per byte 164 | width, colorSpace, 165 | (CGBitmapInfo)kCGImageAlphaNone); 166 | CGColorSpaceRelease(colorSpace); 167 | if(context == NULL) 168 | { 169 | NSLog(@"Error building grayscale bitmap context"); 170 | return nil; 171 | } 172 | 173 | //Replicate image using new color space 174 | CGRect rect = SizeMakeRect(sourceImage.size); 175 | CGContextDrawImage(context, rect, sourceImage.CGImage); 176 | CGImageRef imageRef = CGBitmapContextCreateImage(context); 177 | CGContextRelease(context); 178 | 179 | //Return the grayscale image 180 | UIImage *output = [UIImage imageWithCGImage:imageRef]; 181 | CFRelease(imageRef); 182 | return output; 183 | } 184 | ``` 185 | 186 | ### 带水印的图像 187 | 水印是图像绘制中很常见的一种请求。原本的水印是模糊地印在报纸上来标记文章来源的。现在的文本水印实用就不一样了。它印在图片上避免复制和重用,或是用特殊标志或来源来标记素材。 188 | 代码3-5展示了如何制作一个图3-5那样的简单水印。水印无外乎就是绘制一个图像,然后再绘制点别的——也许会是字符串,logo或是符号——覆盖在图像,然后恢复到新的版本。 189 | 190 | 图3.5 191 | 192 | 代码3-5的例子绘制了一个覆盖在原图上的斜向字符串水印(“watermark”)。通过45度角旋转上下文来实现,它使用混合模式来强调水印同时也展示原图的细节。因为这段代码是在iOS7上面写的,当你绘制的时候必须独立设置文本的颜色和字体。如果你不设置的话,字符串就会“消失”——然后你就会抓耳挠腮不知所以,就像我在更新这个例子的时候一样。 193 | 另一个常用代码是使用扩散的白色覆盖图适度调整透明度,然后就像绘制logo的影子一样绘制出来(而非直接绘制logo)在图片的某一部分上。路径裁剪在这里会有使用。在第五章会进一步的探讨。 194 | 每个水印都会不同程度上改变原有的图像。随着图像的改变或模糊,移除水印变得越发困难。 195 | ``` 196 | UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0); 197 | CGContextRef context = UIGraphisGetCurrentContext(); 198 | 199 | //Draw the original image into the context 200 | CGRect targetRect = SizeMakeRect(targetSize); 201 | UIImage *sourceImage = [UIImage imageNamed:@"pronghorn.jpg"]; 202 | CGRect imgRect = RectByFillingRect(SizeMakeRect(sourceImage.size), targetRect); 203 | [sourceImage drawInRect:imgRect]; 204 | 205 | //Rotate the context 206 | CGPoint center = RectGetCenter(targetRect); 207 | CGContextTranslateCTM(context, center.x, center.y); 208 | CGContextRotateCTM(context, M_PI_4); 209 | CGContextTranslateCTM(context, -center.x, -center.y); 210 | 211 | //Create a string 212 | NSString *watermark = @"watermark"; 213 | UIFont *font = [UIFont fontWithName:@"HelveticaName" size:48]; 214 | CGSize size = [watermark sizeWithAttributes:@{NSFontAttributeName:font}]; 215 | CGRect stringRect = RectCenteredInRect(SizeMakeRect(size), targetRect); 216 | 217 | //Draw the string, using a blend mode 218 | CGContextSetBlendMode(context, kCGBlendModeDifference); 219 | [watermark drawInRect:stringRect withAttributes:@{NSFontAttributeName:font, NSForegroundColorAttributeName:[UIColor whiteCOlor]}]; 220 | 221 | //Retrieve the new image 222 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 223 | UIGraphicsEndImageContext(); 224 | return image; 225 | ``` 226 | ### 取得图像数据 227 | 你可以通过PNG(UIImagePNGRepresentation())和JPEG(UIImageJPEGRepresentation())来查询图像,这些方法返回的数据很合适根据文件类型来储存图片。它包括了文件头和标记数据,内部块,和压缩。这些数据并不是一位一位地操作的。当你想要进行图片处理的时候,你想要取到上下文的位数组。代码3-6展示了如何做到。 228 | 这个方法在上下文中绘制了一个图片然后是用CGBIitMapContextGetData()来获得位数据。它会复制这些位数据到NSData实例中,然后返回这个实例。 229 | 把输出到数据封装到一个NSData对象中可以绕开内存管理的问题。虽然你最后可能还是会用到C语言的API来进行计算,但是目前为止还是在Objective-C的语境中。 230 | > 注意:这里讨论位级别的处理并不是代表着要使用Core Image,它有自己的技术和实际使用。 231 | #### 创建上下文 232 | 你已经在本书见过很多次CGBitmapContextCreate()方法了。但是如何创建上下文依然值得被讨论。大多数情况下,你可以视其为样板,仅需做少量的改变。这里有一些参数的分解和你写你需要支持的值: 233 | * void *data ——第一个参数填的是NULL,表示让Quartz代替你分配内存。随后Quartz会用自己的方法来管理内存,所以你不必显示分配内存或释放。通过CGBitmapContextGetData()来获取数据,如代码3-6中所示。如方法名中的“get”暗示的那样,这个方法只读数据却不进行拷贝,否则会干涉到内存管理。 234 | * size_t width 和 size_t height —— 接下来的两个参数为图像的宽和高,size_t在iOS中定义为unsigned long。代码3-6传递的是从源数据获取到的延展值到CGBitmapContextCreate()中。 235 | * size_t bitsPerComponent —— 在UIKit中,都是使用8位的(uint_8)。除非你有什么非要改变不可的原因,一般都是填入8。在Quartz 2D中,程序引导表中都支持像素格式,包括5位,16位,32位组件。一个“组件(component)”代表单个数据频道。ARGB数据使用4个组件每像素。灰度图使用一个组件(没有透明通道)或是两个(有透明通道)。 236 | * size_t bytesPerRow —— 行尺寸乘以每一个组件的位数来计算每一行的位数。典型地,在ARGB图中使用width * 4,而在直接灰度图(无透明度)中使用width即可。特别注意这个值。它不仅作为属性很有用,你也需要用它来计算位数组中任意像素的(x,y)的偏移,通过(y * bytesPerRow + x)。 237 | * CGColorSpaceRef colorspace —— 当使用设备RGB和设备灰度时,需要传入色彩空间来供给Quartz在位图上下文中使用。 238 | * CGBitmapInfo bitmapInfo —— 这个参数定义了位图使用的alpha通道的风格。根据经验来说,彩色图像使用kCGImageAlphaPremultipliedFirst,灰度图使用kCGImageAlphaNone。如果你好奇,查询Quartz2D编程指导可以查到更多的其他选项。在iOS7以后,确保在CGBitmapInfo中设置alpha值设置以避免复杂的问题。 239 | 240 | ``` 241 | NSData *BytesFromRGBImage(UIImage *sourceImage) 242 | { 243 | if(!sourceImage) return nil; 244 | 245 | //Establish color space 246 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 247 | if(colorSpace == NULL) 248 | { 249 | NSLog(@"Error creating RGB color space"); 250 | return nil; 251 | } 252 | 253 | //Establish context 254 | int width = sourceImage.size.width; 255 | int height = sourceImage.size.height; 256 | CGContextRef context = CGBitmapContextCreate(NULL, width, height, 257 | 8,//bits per byte 258 | width * 4,//bytes per row 259 | colorSpace, 260 | (CGBitmapInfo)kCGImageAlphaPremultipliedFirst); 261 | CGColorSpaceRelease(colorSpace); 262 | if(context == NULL) 263 | { 264 | NSLog(@"Error creating context"); 265 | return nil; 266 | } 267 | 268 | //Draw source into context bytes 269 | CGRect rect = (CGRect){.size = sourceImage.size}; 270 | CGContextDrawImage(context, rect, sourceImage.CGImage); 271 | 272 | //Create NSData from bytes 273 | NSData *data = [NSData dataWithBytes:CGBitmapContextGetData(context) 274 | length:(width * height * 4)];//bytes per image 275 | CGContextRelease(context); 276 | return data; 277 | } 278 | ``` 279 | > 注意:当响应能力是不可忽略要考虑进去的时候,不要等着UIImage实例或者他底层的CGImage来慢慢解压。通过CGImageSource可以缓存用CGImage加载的解压后的图片。这是ImageIO框架中的一部分,可以设定自己的缓存解压策略(kCGImageSourceShouldCache)。这样操作会带来更快的绘画体验,虽然也需要消耗更多内存。 280 | 281 | ### 通过字节来创建图像 282 | 代码3-7颠倒了图片导出字节的剧本,它通过字节来创建图片。因此,你可以把字节作为第一个参数传给CGBitmapContextCreate()。这告诉Quartz不需要去申请内存但需要把数据从源上下文转移到新的上下文中。 283 | 除了一个小的改变,代码3-7到目前为止都看起来很熟悉。从上下文创建新的图片,转换CGImageRef到UIImage,返回新的image实例。 284 | 为了确保可以从任何方向对数据进行转换——图片到数据或是数据到图片——你可以把图片处理整合到绘制代码中,使用UIView中的结果。 285 | ``` 286 | UIImage *ImageFromBytes(NSData *data, CGSize targetSize) 287 | { 288 | //check data 289 | int width = targetSize.width; 290 | int height = targetSize.height; 291 | if(data.length < (width * height * 4)) 292 | { 293 | NSLog(@"Error: Got %d bytes. Expected %d bytes", data.length,width * height * 4); 294 | return nil; 295 | } 296 | 297 | //Create a color space 298 | CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); 299 | if(colorSpace == NULL) 300 | { 301 | NSLog(@"Error creating RGB color space"); 302 | return nil; 303 | } 304 | 305 | //Create the bitmap context 306 | Byte *bytes = (Byte *) data.bytes; 307 | CGContextRef context = CGBitmapContextCreate(bytes, width, height, 308 | BITS_PER_COMPONENT,//8 bits per component 309 | width * ARGB_COUNT,//4 bytes in ARGB 310 | colorSpace, 311 | (CGBitmapInfo)kCGImageAlphaPremultipliedFirst); 312 | CGColorSpaceRelease(colorSpace); 313 | if(context == NULL) 314 | { 315 | NSLog(@"Error creating context"); 316 | return nil; 317 | } 318 | 319 | //Convert to image 320 | CGImageRef imageRef = CGBitmapContextCreateImage(context); 321 | UIImage *image = [UIImage imageWithCGImage:imageRef]; 322 | 323 | //Clean up 324 | CGContextRelease(context); 325 | CFRelease(imageRef); 326 | 327 | return image; 328 | 329 | } 330 | ``` 331 | 332 | ### 绘图和自动布局 333 | 在自动布局中 —— 一个iOS和OS X中最新的以约束为基础的系统 —— 一个视图的内容扮演者很重要的作用就如同约束之于布局那样。这就涉及到“视图的固有内容尺寸”。这个尺寸描述了展示视图的全部内容所需要的最小空间,而没有挤压或裁剪。它通过所有视图展示的内容属性获得。在图像和绘图中,它表示为以点为单位的图像的“自然尺寸”。 334 | 当你为你点视图添加了装饰(比如阴影,闪光,或其他超出你的视图核心内容区域的部分)之后,自然尺寸也许就不会反映出你真实想要在自动布局中想要的样子。在自动布局中,约束决定了视图的大小和位置,使用几何元素“alignment rectangle”。如你将看到的,UIKit使用帮助控制来布局。 335 | 336 | #### Alignment Rectangles 337 | 当开发者制作复杂的视图时,他们也许会使用视觉上的装饰品,比如说阴影,外部高光,反射,雕刻线。做这些工作往往会导致有些东西会画在视图外面。不像frame,一个视图的对齐矩形(alignment rectangle)必须限制核心可见元素。它的值需要不被画在视图之外的新视图影响。如图3-6中最左的图,它描述了一个绘有阴影和标记的视图。当自动布局的时候,你希望自动布局只关注核心元素——中心的蓝色矩形——而不是关注其他的装饰品。 338 | 339 | 图3.6 340 | 341 | 中心的图突出了视图的对齐矩形。这个矩形没有包括进所有的装饰,只框住了你想要自动适配去关注的部分。对比右侧图,这个版本包括了所有可见的部分,把视图的结构扩展到了需要对齐的矩阵之外。 342 | 右手边的图包含了所有视图的可见部分,包括阴影和标记。这些装饰品可能会潜在地改变视图的对齐特征(比如说center,bottom和right)当你需要在自动适配中考虑进去的时候。 343 | 当你使用alignment rectangle而非frame的时候,自动适配会确保关键的信息,比如边界和中心,合适的在布局中使用。在图3-7中,被装饰的视图很好地和背景网格对齐。而边界和阴影都不会改变它的对齐。 344 | 345 | 图3.7 346 | 347 | #### Alignment Inset 348 | 绘图经常会包含写死的装饰品比如高光,阴影等等,他们占用很小的内存并且高效运行。因此,很多开发者会提前绘制他们因为他们的低开销。 349 | 为了容纳多余的视觉元素,使用imageWithAlignmentRectInsets:使用UIEdgeInset结构体和UIImage,返回一个处理后的图像。Insets定义了矩形top,left,bottom和right的偏移。使用它来描述矩形边界的移动。这个inset可以确保对齐矩阵是正确的,就算在图片里还绘制有额外的装饰 350 | 351 | ``` 352 | typedef struct { 353 | CGFloat top, left, bottom, right; 354 | }UIEdgeInsets; 355 | ``` 356 | 下面的代码片段容纳一个20点单位的阴影通过插入对齐rect到底部和右侧: 357 | ``` 358 | UIImage *image = [[UIImage imageName:@"Shadowed.png"] imageWithAlignmentRects:UIEdgeInsetsMake(0, 0, 20, 20)]; 359 | UIImageView *imageView = [[UIImageView alloc] initWithImage:image]; 360 | ``` 361 | 手动约束insets也许会比较困难,尤其是当你需要更新图片的时候。当你已知对齐rect和所有图像的边界,你可以通过如下方法自动计算出边界。代码3-8定义了一个简单的inset创建器。它能得到对齐矩阵子啊每一个边界上的偏移,然后返回UIEdgeInset。使用该方法通过核心可见部分的固定几何来创建insets 362 | ``` 363 | UIEdgeInsets BuildInsets(CGRect alignmentRect, CGRect imageBounds) 364 | { 365 | //Ensure alignment rect is fully within source 366 | CGRect targetRect = CGRectIntersection(alignmentRect, imageBounds); 367 | 368 | //Calculate insets 369 | insets.left = CGRectGetMinX(targetRect) - CGRectGetMinX(imageBounds); 370 | insets.right = CGRectGetMaxX(imageBounds) - CGRectGetMaxX(targetRect); 371 | insets.top = CGRectGetMinY(targetRect) - CGRectGetMinY(imageBounds); 372 | insets.bottom = CGRectGetMaxY(imageBounds) - CGRectGetMaxY(targetRect); 373 | 374 | return insets; 375 | } 376 | ``` 377 | #### 使用Alignment Rects来绘制图像 378 | 图3-8展示了运行在布局中的对齐矩形。上方图片中的图没有设置对齐偏好。因此,整个图片(所有的灰色正方形,包括那个兔子,阴影以及星星)都中心对齐与它的父视图。而下方的图片使用了alignment insets。这次仅有兔子的边框(内部轮廓)中心对齐。所以中心也改变了。现在兔子的中学对应父视图的中心,而非整个视图的中心。多余的绘画区域和其他图片的细节将不会影响布局。 379 | 380 | 图3.8 381 | 382 | 代码3-9展示了下方图片对齐和绘制的过程。它创建了一个灰色的背景,一个绿色的兔子,一个红色的标记,还有一个展示兔子边界的轮廓框。并且还给兔子添加了阴影,把标记移动到了兔子的右上角。阴影和标记是在iOS可视元素中非常常见的,尽管在iOS7扁平化之后。 383 | 然而麻烦的在后面,为了对齐输出的图像,通过兔子的UIBezierPath来获得边界框,这个路径和标志,背景,阴影都是对立的。通过应用代码3-8的边界嵌入,代码3-9创建了一个图像仅仅把兔子给框住。 384 | 这确实是一个很好的方法,让带有装饰的Quartz或UIKit绘图与Auto Layout无缝地协同工作。 385 | ``` 386 | UIBezierPath *path; 387 | 388 | //Begin the image context 389 | UIGraphicsBeginImageContextWithOptions(targetsize, NO, 0.0); 390 | CGContextRef context = UIGraphicsGetCurrentContext(); 391 | CGRect targetRect = SizeMakeRect(targetSize); 392 | 393 | //Fill the background of the image and outline it 394 | [backgroundGrayColor setFill]; 395 | UIRectFill(targetRect); 396 | path = [UIBezierPath bezierPathWithRect:targetRect]; 397 | [path strokeInside:2]; 398 | 399 | //Fit bunny into an inset, offset rectangle 400 | CGRect destinationRect = RectInsetByPercent(SizeMakeRect(targetSize), 0.25); 401 | destinationRect.origin.x = 0; 402 | UIBezierPath *bunny = [[UIBezierPath bunnyPath] pathWithinRect:destinationRect]; 403 | 404 | //Add a shadow to the context and draw the bunny 405 | CGContextSaveGState(context); 406 | CGContextSetShadow(context, CGSizeMake(6,6), 4); 407 | [greenColor setFill]; 408 | [bunny fill]; 409 | CGContextRestoreGState(context); 410 | 411 | //Outline bunny`s bounds,which are the alignment rect 412 | CGRect alignmentRect = bunny.bounds; 413 | path = [UIBezierPath bezierPathWithRect:alignmentRect]; 414 | [darkGrayColor setStroke]; 415 | [path strokeOutside:2]; 416 | 417 | //Add a red badge at the top-right corner 418 | UIBezierPath *badge = [[UIBezierPath badgePath] pathWithinRect:CGRectMake(0, 0, 40, 40)]; 419 | badge = [badge pathMoveCenterToPoint:RectGetTopRight(bunny.bounds)]; 420 | [[UIColor redColor] setFill]; 421 | [badge fill]; 422 | 423 | //Retrieve the initial image 424 | UIImage *initialImage = UIGraphicsGetImageFromCurrentImageContext(); 425 | UIGraphicsEndImageContext(); 426 | 427 | //Build and apply the insets 428 | UIEdgeInsets insets = BuildInsets(alignmentRect, targetRect); 429 | UIImage *image = [initialImage imageWithAlignmentRectInsets:insets]; 430 | 431 | //Return the updated image 432 | return image; 433 | ``` 434 | 435 | ### 创建可伸缩的图片 436 | 可以调整尺寸的绘制允许创建适配视图时边界不会变形的图像。他们保存图像细节,确保你改变的尺寸只是图片的中部。图3-9展示了一个伸缩过的图片的例子。这里展示了一个按钮的背景,他的中部可以收缩或扩张,取决于绑定在按钮上的文字。为了确保只有中部被缩放,一套“cap insets”来禁止边缘缩放。这个“cap”确保了中心区域(大片紫色的区域)可以夸大或压缩,而不影响圆角喝线框。 437 | 438 | 图3.9 439 | 440 | 比较图3-9和图3-10可以得到处理上述问题的意义。图3-10展示了一个相同的按钮图像但是并没有使用“cap insets”。他的边界和圆角随着按钮其他部分的图片扩张而扩张,产生了可见的错误。图3-9才应该是3-10想要得到的效果。 441 | 442 | 图3.10 443 | 444 | 代码3-10展示了背后的代码。它创建了一个40x40的图像上下文,绘制了连个圆角矩形,并为背景添加固定颜色。它完成了基础的图像。然后代码3-10调用resizableImageWithCapInsets:这个方法会创建一个使用“cap insets”的新图像,这句代码正是使图3-9和3-10产生区别的原因。 445 | 446 | 尽管iOS7介绍了无边界的修整按钮,代码中所使用的传统按钮依旧在三分程序中有重要的作用。在这苹果自己的WWDC大会上都有强调,它给的很多例子中都有这样的代码。在第七章中,你会读到更多按钮特效,如光彩效果,纹理和其他非线性效果。自定义按钮的时代永远不会结束,他们只需要比原先跟多的创意罢了。 447 | > 注意:还有一些需要强制使用拉伸图像的地方。通过GPU拉伸,添加一个拉伸器而非手动拉伸。在图案颜色方面是一样的,在本章之后的部分会了解到。填充颜色也会比手动绘制要快一些。 448 | ``` 449 | CGSize targetSize = CGSizeMake(40, 40); 450 | UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0); 451 | 452 | //Create the outer rounded rectangle 453 | CGRect targetRect = SizeMakeRect(targetSize); 454 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:targetRect 455 | cornerRadius:12]; 456 | 457 | //Fill and stroke it 458 | [bgColor setFill]; 459 | [path fill]; 460 | [path strokeInside:2]; 461 | 462 | //Create the inner rounded rectangle 463 | UIBezierPath *innerPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInse(targetRect, 4, 4) cornerRadius:8]; 464 | 465 | //Stroke it 466 | [innerPath strokeInside:1]; 467 | 468 | //Retrieve the initial image 469 | UIImage *baseImage = UIGraphicsGetImageFromCurrentImageContext(); 470 | UIGraphicsEndImageContext(); 471 | 472 | //Create a resizable version, with respect to 473 | //the primary corner radius 474 | UIImage *image = [baseImage resizableImageWithCapInsets:UIEdgeInsetMake(12, 12, 12, 12)]; 475 | return image; 476 | ``` 477 | 478 | ### 渲染PDF 479 | 图3-11展示了在图形上线中渲染一个pdf的结果。这个任务在核心图像API中会有点复杂,该API并未提供绘制一页pdf的选项。 480 | 481 | 图3.11 482 | 483 | 这个代码涉及到了很多步骤,如下方代码所示。你需要先打开一个pdf文档。然后使用CGPDFDocumentCreateWithURL()得到文档的引用,以便在上下文中使用。 484 | 当你取到文档后,需要完成以下几步: 485 | 1. 通过CGPDFDocumentGetNumberOfPages()检查文档的页数。 486 | 2. 通过CGPDFDocumentGetPage()得到每一页,页面是从1开始计数的,而非0. 487 | 3. 记得结束时使用CGPDFDocumentRelease()释放文档。 488 | 489 | ``` 490 | //Open PDF document 491 | NSString *pdfPath = [[NSBundle mainBundle] pathForResource:@"drawingwithquartz2d" ofType:@"pdf"]; 492 | 493 | CGPDFDocumentRef pdfRef = CGPDFDocumentCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:pdfPath]); 494 | if(pdfRef == NULL) 495 | { 496 | NSLog(@"Error loading PDF"); 497 | return nil; 498 | } 499 | 500 | //··· use PDF document here 501 | 502 | CGPDFDocumentRelease(pdfRef); 503 | ``` 504 | 通过CGPDFPageRef来获取所有的页面,代码3-12可以让你在图像上下文中绘制任一pdf页。但是有一点会比较麻烦,PDF的方法使用的Quartz的坐标系(左下坐标系)。 505 | 因此,你需要使用一点小技巧。首先,把上下文掷为垂直方向,以便于pdf是上下翻页的,然后转换目标矩形,以便于在正确的位置绘制。 506 | 你也许会想为什么要做两次转换,答案是:当你转换坐标系到左下坐标系后,你原来在右上角的对象会绘制在右下角,因为他仍存在于UIKit世界。当你调整绘制上下文的转换时,你的矩形也要适应这样的转换,如下方代码3-12所示。如果你跳过这一步,你的PDF会在出现在右下方,而非预想的右上方。 507 | 我们鼓励你自己尝试完成这些步骤。你将会更好的理解坐标的一致性。转换坐标系不仅仅是“修复”Quartz绘制,他还会影响到所有的位置坐标。在第七章中也会发现类似的问题。 508 | (注:省略了两段) 509 | 510 | ``` 511 | void DrawPDFPageInRect(CGPDFPageRef pageRef,CGRect destinationRect) 512 | { 513 | CGContextRef context = UIGraphicsGetCurrentContext(); 514 | if(context == NULL) 515 | { 516 | NSLog(@"Error: No context to draw to"); 517 | return; 518 | } 519 | CGContextSaveState(context); 520 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 521 | 522 | //Flip the context to Quartz space 523 | CGAffineTransform transform = CGAffineTransformIdentity; 524 | transform = CGAffineTransformScale(transform, 1.0f, -1.0f); 525 | transform = CGAffineTransformTranslate(transform, 0.0f, -image.size.height); 526 | CGContextConcatCTM(context, transform); 527 | 528 | //Flip the rect,which ramains in UIKit space 529 | CGRect d = CGRectApplyAffineTransform(destinationRect, transform); 530 | 531 | //Calculate a rectangle to draw to 532 | //CGPDFPageGetBoxRect() returns a rectangle 533 | //representing the page`s dimension 534 | CGRect pageRect = CGPDFPageGetBoxRect(pageRef, kCGPDFCropBox); 535 | CGFloat drawingAspect = AspectScaleFit(pageRect.size, d); 536 | CGRect drawingRect = RectByFittingInRect(pageRect, d); 537 | 538 | //Draw the page outline(optional) 539 | UIRectFrame(drawingRect); 540 | 541 | //Adjust the context to the page draws within 542 | //the fitting rectangle(drawingRect) 543 | CGContextTranslateCTM(context, drawingRect.origin.x, drawingAspect, drawingAspect); 544 | 545 | //Draw the page 546 | CGContextDrawPDFPage(context, pageRef); 547 | 548 | CGContextRestoreGState(context); 549 | } 550 | ``` 551 | ##### 位图上下文几何 552 | 553 | 当你使用位图上下文时,你会获取到某些信息。比方说,上下文的宽高。CGBitmapContextGetHeight()和CGBitmapContextGetWidth()返回了宽高的整型值。然而,却不能得到上下文的比例——上下文的像素点数和输出图像的点数的比例。因为比例有点过于依赖于抽象。纵观全书,scale是通过UIKit的UIGraphicsBeginImageContextWithOptions()设置的。并非直接和Quartz绘制绑定的。 554 | 因此,代码3-12没有用位图上下文方法来修正尺寸。这点很重要,因为转换操作使用点单位,而不是像素单位。如果你使用Retina设备,需要一个2倍的元素。你的200-点转换用CGBitmapContextGetHeight()会返回一个400-像素的值。 555 | 撇开点和像素的计算,上下文方法并非并无用处。他可以获取当前的转换矩阵(CGContextGetCTM()),每一行的字节数(CGBitmapContextGetBytesPerRow()),透明度(CGBitmapContextGetAlphaInfo() ),等等。在Xcode的文档中搜索“ContextGet”你可以找到相应的上下文方法。 556 | 557 | 558 | ### 创建模式图像 559 | 模式图像是UIKit里的珍宝。你在代码中选择一个模式或是通过文件加载一个图像,然后就可以像使用“color”属性一样把他付给其他视图,如下: 560 | 561 | ``` 562 | self.view.backgroundColor = [UIColor colorWithPatternImage:[self buildPattern]]; 563 | ``` 564 | 565 | 图3-12展示了一个通过代码3-13创建的简单的模式。代码使用同样的技术:旋转,镜象,交替摆放。 566 | 如果你也想在app中有类似的效果,可以下载一个Patterno(Mac商城19.99美元,人民币128)。 567 | 可伸缩矢量图(SVG)模式应用广泛。SVG使用XML标准来为web定义矢量图,提供了一个新的模式设计方法。http://phibit.com/svgpatternst 提供简单但是视觉效果良好的效果。PaintCode(Mac商城99.99美元)可以吧SVG片段转化为标准的UIKit绘制,为SVG资源和UIKit提供了良好的桥梁。仅需把SVG转换为TextEdit文本,修改后缀为.svg。然后导入PatinCode,它会把SVG翻译为Objective-C,这样就可以直接加入到Xcode工程中了。 568 | 569 | > 通过谷歌查询Apple Tectnote 31 或访问http://en.wikipedia.org/wiki/Dogcow 570 | 571 | 图3.12 572 | 573 | ``` 574 | -(UIImage *)buildPattern 575 | { 576 | //Create a small tile 577 | CGSize targetSize = CGSizeMake(80, 80); 578 | CGRect targetRect = SizeMakeRect(targetSize); 579 | 580 | //Start a new image 581 | UIGraphicsBeginImageContextWithOptions(targetSize, NO, 0.0); 582 | CGContextRef context = UIGraphicsGetCurrentContext(); 583 | 584 | //Fill background with pink 585 | [customPinkColor set]; 586 | UIRectFill(targetRect); 587 | 588 | //Draw a couple of dogcattle in gray 589 | [[UIColor grayColor] set]; 590 | 591 | //First,bigger with interior detail in the top-left. 592 | //Read more about Bezier path objects in Chapters 4 and 5 593 | CGRect weeRect = CGRectMake(0, 0, 40, 40); 594 | UIBezierPath *moof = BuildMoofPath(); 595 | FitPathToRect(moof, weeRect); 596 | RotatePath(moof, M_PI_4); 597 | [moof fill]; 598 | 599 | //then smaller, flipped around, and offset down and right 600 | RotatePath(moof, M_PI); 601 | OffsetPath(moof, CGSizeMake(40, 40)); 602 | ScalePath(moof, 0.5, 0.5); 603 | [moof fill]; 604 | 605 | //Retrieve and return the pattern image 606 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 607 | UIGraphicsEndImageContext(); 608 | return image; 609 | } 610 | ``` 611 | 612 | ### 总结 613 | 本章解决了一些在上下文绘制时常常会遇到的问题,你读到了基础图片世代,转换字节和图像,添加插入适配,并学习了PDF绘制。再继续阅读后面的章节之前,可你需要想一想以下一些内容: 614 | * 615 | -------------------------------------------------------------------------------- /4-路径基础/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-1.png -------------------------------------------------------------------------------- /4-路径基础/4-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-10.png -------------------------------------------------------------------------------- /4-路径基础/4-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-11.png -------------------------------------------------------------------------------- /4-路径基础/4-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-12.png -------------------------------------------------------------------------------- /4-路径基础/4-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-13.png -------------------------------------------------------------------------------- /4-路径基础/4-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-14.png -------------------------------------------------------------------------------- /4-路径基础/4-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-15.png -------------------------------------------------------------------------------- /4-路径基础/4-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-16.png -------------------------------------------------------------------------------- /4-路径基础/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-2.png -------------------------------------------------------------------------------- /4-路径基础/4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-3.png -------------------------------------------------------------------------------- /4-路径基础/4-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-4.png -------------------------------------------------------------------------------- /4-路径基础/4-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-5.png -------------------------------------------------------------------------------- /4-路径基础/4-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-6.png -------------------------------------------------------------------------------- /4-路径基础/4-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-7.png -------------------------------------------------------------------------------- /4-路径基础/4-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-8.png -------------------------------------------------------------------------------- /4-路径基础/4-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/4-9.png -------------------------------------------------------------------------------- /4-路径基础/4-路径基础.md: -------------------------------------------------------------------------------- 1 | 2 | # 路径基础 3 | 4 | 贝塞尔曲线是iOS最重要的绘制工具之一,可以用来绘制轮廓,裁剪路径,定义动画路径,等等。不论你是创建自定义视图中的某一个元素,或是添加一个PS级别的效果,或者基础的任务比如画线和圆,使用UIBezierPath类都可以轻易且有效的实现。 5 | 6 | ### 为什么使用贝塞尔 7 | 8 | 当创建一个按钮或是斜角,阴影或是图案的时候,贝塞尔曲线都能提供有效且灵活的解决方案。如图4-1,所有的截屏都是由各种贝塞尔曲线的元素绘制而组成的。 9 | 图4.1 10 | 上方的截屏来自一个简单的音频app,他是蓝白相间的图像还有黑色的音频线条,他们都会通过连续的或断续的线条绘制的。这些线条是非常常用的,在很多绘制的情况下都会用到。 11 | 下方的图就比较复杂了,兔子的几何图存在单独的一个贝塞尔曲线中,被圆角矩形包围,覆盖在按钮上的弧度也是贝塞尔曲线为基础的椭圆。贝塞尔曲线同事裁剪边界形状的路径,用来定义限制微妙的颜色变化的范围,从而得到3D的效果。 12 | 虽然这两个截图的绘制效果截然不同,类的操作方式却是相同的。如果你想在你的app中使用UIKit绘制,你需要和UIBezierPath类建立起更加亲密且舒适的关系。 13 | 14 | ### 类的便捷方法 15 | 16 | UIBezierPath类的方法创建矩形,椭圆,圆角矩形和弧,提供只用一个参数就可以使用的路径风格元素的方法。 17 | 18 | * Rectangles —— bezierPathWithRect:用于绘制界面中的各种矩形元素。你可以使用该方法绘制任何种类的矩形。 19 | * Ovals and circle —— bezierPathWithOvalInRect:提供绘制任何形状的圆和椭圆的工具。 20 | * Rounded rectangles —— bezierPathWithRoundedRect:cornerRadius:可以用于创建iOS设计师非常喜欢的圆角矩形,该路径非常适合按钮和提示框,还有很多其他的视图元素。 21 | * Corner-controlled rounded rectangles ——bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:允许你仅在矩形的某一个或几个角进行圆角操作。这个方法很方便的帮助你创建一些并不想要四个角都是圆角的控件。 22 | * Arcs —— bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise:通过定义起始角度和结尾角度来绘制弧线,通过弧线增加视觉修饰,来创建个新的视觉分割,以达到优化全局GUI的效果。 23 | 24 | 贝塞尔曲线类方法提供一个基础的绘制起点,如代码4-1的例子演示的那样,这段代码通过结合多个形状创建一个新的贝塞尔曲线。每一步,添加一个路径,最后凑成一个如图4-2所示的“脸”的图像。 25 | 如代码4-1所示的,贝塞尔曲线单例内部是易变的。你可以添加形状和元素让他们变成一个新的图像。此外,路径也不必是连续的,如图4-2,他们明显是由不相交的子形状构成的,每一个都可以单独绘制。 26 | 27 | > 注意:这里使用的代码只是例子,仅展示绘制时需要的API的简例,而非之前书中的代码,可以直接用在项目中。 28 | 29 | 30 | 图4.2 31 | 32 | 尽管你可能会在绘制过于复杂的路径时遇到性能的问题,但其实更多的时候是在处理用户交互的操作时才会发生。我会将绘图应用程序中的绘图表示为路径。比如幼儿园的孩子,没有谁比他们更会长时间的绘制五到十分钟,甚至都不带抬手指的,过长的路径会导致性能的下降。你的普通预构建的路径中加入矢量图,都不应该忽略掉这个问题。 33 | 有时候你也想分割开路径,一旦绘制,j路径的秒变颜色和填充颜色都会应用在所有的元素中。当你需要多种绘制的风格时,你需要创建多个独立的路径,每一个都是一个独立的统一操作单元。 34 | 35 | ``` 36 | CGRect fullRect = (CGRect)(.size = size); 37 | 38 | //Establish a new path 39 | UIBezierPath *bezierPath = [UIBezierPath bezierPath]; 40 | 41 | //Create the face outline 42 | //and append it to the path 43 | CGRect inset = CGRectInset(fullRect, 32, 32); 44 | UIBezierPath *faceOutline = [UIBezierPath bezierPathWithOvalInRect:inset]; 45 | 46 | [bezierPath appendPath:faceOutline]; 47 | 48 | //Move in again, for the eyes and mouth 49 | CGRect insetAgain = CGRectInset(inset, 64, 64); 50 | 51 | //Calculate a radius 52 | CGPoint referencePoint = CGPointMake(CGRectGetMinX(insetAgain),CGRectGetMaxY(insetAgain)); 53 | CGPoint center = RectGetCenter(inset); 54 | CGFloat radius = PointDistanceFromPoint(referencePoint, center); 55 | 56 | //Add a smile from 40 degrees around to 140 degree 57 | UIBezierPath *smile = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:RadianFromDegrees(140) endAngle:RadiansFromDegrees(40) clockwise:NO]; 58 | [bezierPath appendPath:smile]; 59 | 60 | //Build Eye 1 61 | CGPoint p1 = CGPointMake(CGRectGetMinX(insetAgain),CGRectGetMinY(insetAgain)); 62 | CGRect eyeRect1 = RectAroundCenter(p1, CGSizeMake(20,20)); 63 | UIBezierPath *eye1 = [UIBezierPath bezierPathWithRect:eyeRect1]; 64 | [bezierPath appendPath:eye1]; 65 | 66 | //Build Eye 2 67 | CGPoint p2 = CGPointMake(CGRectGetMaxX(insetAgain),CGRectGetMinY(insetAgain)); 68 | CGRect eyeRect2 = RectAroundCenter(p1, CGSizeMake(20,20)); 69 | UIBezierPath *eye2 = [UIBezierPath bezierPathWithRect:eyeRect2]; 70 | [bezierPath appendPath:eye2]; 71 | 72 | 73 | //Draw the complete path 74 | bezierPath.lineWidth = 4; 75 | [bezierPath stroke]; 76 | ``` 77 | ### 嵌套圆角矩形 78 | 图4-3显示了两个嵌套的圆角矩形,构建圆角矩形在iOS绘图任务中非常常见,每个路径都被填充和描边:外部线宽为2点宽,内部为1点宽,图4-3展示了一个很常见的错误。 79 | 80 | 图4.3 81 | 82 | 仔细观察每个图像的四个角。第一张图和第二张图的拐角半径不同。左图内部矩形缩进4点,并使用12曲度点圆角。 83 | ``` 84 | UIBezierPath *path2 = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(destinationRect, 4, 4) cornerRadius:12]; 85 | ``` 86 | 第二个图同样向内缩进4点,但是曲度改为8,使两套路径之间更好的拟合,确保两条曲线在平行的位置从始至终都有不错的视觉效果。 87 | 88 | 89 | ### 创建路径 90 | 当系统提供的路径(如矩形和椭圆)不足以满足您的需求时,您可以迭代地构建路径。通过逐点布局,来添加曲线和直线。 91 | 每个贝塞尔曲线都可以包括多种几何元素,包括如下: 92 | 93 | * 通过moveToPointc:创建定位 94 | * 通过addLineToPoint:创建直线 95 | * 通过addCurveToPoint:controlPoint1:controlPoint2:创建三次贝塞尔曲线线段 96 | * 通过addArcToCenter:radius:startAngle:endAngle:clockwise:创建弧线。 97 | 98 | 图4-4显示了一个由一系列三次曲线线段组成的星星。代码4-2有详细的创建过程,他创建了一个新的路径,起点为p0,然后通过一系列的三次曲线来构成星形。 99 | 100 | 图4.4 101 | 102 | 这个代码看起来不是特别友好,确实,它就是这样的。构建贝塞尔曲线时非常不友好的过程。事实上,这个特别的形状我是从Photoshop那边创建的。我画了这个形状,然后保存到psd中,然后使用PaintCode(Mac商城中的Photoshop内构项目99美元+20美元)将矢量图转换为一系列的objective-C的请求。 103 | 说到绘图,最适合表现形状的工具往往都不在Xcode里。但是,在设计了形状之后,你的代码有很多事情可以做,来管理和优化你的app素材。因此,你不必担心在Photoshop或其他工具中建立的素材会有精确度的问题。你会发现UIBezierPath实例展示了矢量艺术。你可以缩放,平移,旋转等等,更具你的需求来调整素材。 104 | 105 | ``` 106 | //Create new path, courtesy of PaintCode (paintcodeapp.com) 107 | UIBezierPath *bezierPath = [UIBezierPath bezierPath]; 108 | 109 | //Move to the start of the path, at the right side 110 | [bezierPath moveToPoint:CGPointMake(883.23, 430.54)]//p0 111 | 112 | //Add the cubic segments 113 | [bezierPath addCurveToPoint:CGPointMake(749.25, 358.4) //p1 114 | controlPoint1:CGPointMake(873.68, 370.91) 115 | controlPoint2:CGPointMake(809.43,367.95)]; 116 | [bezierPath addCurveToPoint:CGPointMake(661.1, 353.25) //p2 117 | controlPoint1:CGPointMake(721.92, 354.07) 118 | controlPoint2:CGPointMake(690.4,362.15)]; 119 | [bezierPath addCurveToPoint:CGPointMake(492.9, 156.15) //p3 120 | controlPoint1:CGPointMake(575.39, 316.25) 121 | controlPoint2:CGPointMake(629.21, 115.47)]; 122 | [bezierPath addCurveToPoint:CGPointMake(461.98, 169.03) //p4 123 | controlPoint1:CGPointMake(482.59, 160.45) 124 | controlPoint2:CGPointMake(472.29, 164.74)]; 125 | [bezierPath addCurveToPoint:CGPointMake(365.36, 345.52) //p5 126 | controlPoint1:CGPointMake(409.88, 207.98) 127 | controlPoint2:CGPointMake(425.22, 305.23)]; 128 | [bezierPath addCurveToPoint:CGPointMake(262.31, 358.4) //p6 129 | controlPoint1:CGPointMake(341.9, 364.44) 130 | controlPoint2:CGPointMake(300.41, 352.37)]; 131 | [bezierPath addCurveToPoint:CGPointMake(133.48, 460.17) //p7 132 | controlPoint1:CGPointMake(200.89, 368.12) 133 | controlPoint2:CGPointMake(118.62, 376.61)]; 134 | [bezierPath addCurveToPoint:CGPointMake(277.77, 622.49) //p8 135 | controlPoint1:CGPointMake(148.46, 544.36) 136 | controlPoint2:CGPointMake(258.55, 544.36)]; 137 | [bezierPath addCurveToPoint:CGPointMake(277.77, 871.12) //p9 138 | controlPoint1:CGPointMake(201.89, 700.9) 139 | controlPoint2:CGPointMake(193.24, 819.76)]; 140 | [bezierPath addCurveToPoint:CGPointMake(513.51, 798.97) //p10 141 | controlPoint1:CGPointMake(382.76, 934.9) 142 | controlPoint2:CGPointMake(435.24, 786.06)]; 143 | [bezierPath addCurveToPoint:CGPointMake(723.49, 878.84) //p11 144 | controlPoint1:CGPointMake(582.42, 810.35) 145 | controlPoint2:CGPointMake(628.93, 907.89)]; 146 | [bezierPath addCurveToPoint:CGPointMake(740.24, 628.93) //p12 147 | controlPoint1:CGPointMake(834.7, 844.69) 148 | controlPoint2:CGPointMake(722.44, 699.2)]; 149 | [bezierPath addCurveToPoint:CGPointMake(883.23, 430.54) //p0 150 | controlPoint1:CGPointMake(756.58, 564.39) 151 | controlPoint2:CGPointMake(899.19, 530.23)]; 152 | ``` 153 | 154 | ### 绘制贝塞尔曲线路径 155 | 创建贝塞尔路径实例后,通过应用填充或者描边绘制在上下文中。填充绘制,为路径内的所有区域添加颜色。描边绘制,使用路径线宽属性中的线宽来描画路径轮廓,仅绘制边缘。典型的绘图模式如下所示: 156 | 157 | ``` 158 | myPath.lineWidth = 4.0f; 159 | [[UIColor blackColor] setStroke]; 160 | [[UIColor redColor] setFill]; 161 | [myPath fill]; 162 | [myPath stroke]; 163 | ``` 164 | 165 | 这个代码片段为路径设置线宽,并设置填充和描边的颜色,然后进行填充和描边。 166 | 代码清单4-1介绍了我认为更方便的方法。方法定义了一个类类别,添加了两种新的绘制方式,浓缩了绘制的过程,使用如下: 167 | ``` 168 | [myPath fill:[UIColor redColor]]; 169 | [myPath stroke:4 color:[UIColor blackColor]]; 170 | ``` 171 | 这些设置都会变成一个参数,这些函数可以帮助你无需更新上下文的属性及可以完成一系列新的绘图操作,所有的状态都会恢复或保存在如第一章所述的图形状态(GState)中。这确保了传递的参数不会丢失,您可以返回到之前完全相同的上下文中。 172 | 173 | > 注意:在使用生产代码时(也就是说,并非是去写一本书或是编写易读的例子的时候),请确保命名你的类别,添加自定义前缀以确保不会和其他的Apple或Apple日后可能更新的类重叠。esstroke:color:看起来肯定不如stroke:color:好看,但他起码可以确保长久的安全性。 174 | 175 | ``` 176 | @implementation UIBezierPath (HandyUtilities) 177 | //Draw with width 178 | - (void) stroke: (CGFloat) width color:(UIColor *)color 179 | { 180 | CGContextRef context = UIGraphicsGetCurrentContext(); 181 | if(context == NULL) 182 | { 183 | NSLog(@"Error: No context to draw to"); 184 | return; 185 | } 186 | 187 | CGContextSaveGState(context); 188 | 189 | //Set the color 190 | if (color) [color setStroke]; 191 | 192 | //Store the width 193 | CGFloat holdWidth = self.lineWidth; 194 | self.lineWidth = width; 195 | 196 | //Draw 197 | [self stroke]; 198 | 199 | //Restore the width 200 | self.lineWidth = holdWidth; 201 | CGContextRestoreGState(context); 202 | } 203 | 204 | //Fill with supplied color 205 | - (void) fill:(UIColor *)fillColor 206 | { 207 | CGContextRef context = UIGraphicsGetCurrentContext(); 208 | if(context == NULL) 209 | { 210 | NSLog(@"Error: No context to draw to"); 211 | return; 212 | } 213 | CGContextSaveGState(context); 214 | [fillColor set]; 215 | [self fill]; 216 | CGContextRestoreGState(context); 217 | } 218 | ``` 219 | ### 绘制内部路径 220 | 在UIKit中,UIRectFrame()函数作为参数可以在矩形内绘制一条线。该函数提供特别干净好看的结果。代码4-2是我的一些灵感。 221 | 代码4-2执行了和图4-3相同的操作,但是有两点轻微的不同。首先,画笔大小加倍的粗了。然后,他通过addClip来剪切图像。 222 | 通常情况下,画线描边操作会在路径边缘的中心绘制。他使绘制的边一半在边缘外,一半在边沿内。双倍的尺寸确保了在内部的路径正好是你想要的尺寸。 223 | 如第一章所说,裁剪会创建一个遮罩。它不允许把素材添加到边界之外的上下文中。代码4-2中,裁剪防止笔画越过路径边缘,因此所有的绘图都发生在路径内部。 224 | 225 | ``` 226 | - (void)strokeInside:(CGFloat)width color:(UIColor *)color 227 | { 228 | CGContextRef context = UIGraphicsGetCurrentContext()' 229 | if(context == NULL) 230 | { 231 | NSLog(@"Error: No context to draw to"); 232 | return; 233 | } 234 | 235 | CGContextSaveGState(context); 236 | [self addClip]; 237 | [self stroke:width * 2 color:color]; // Listing 4-1 238 | CGContextRestoreGState(context); 239 | } 240 | ``` 241 | ### 填充路径 和 奇/偶填充路径 242 | 在矢量图中,缠绕规则确定所在区域是在内部还是在外部。这些规则确定如图4-5所示的区域是否是需要作色填充的区域。Quartz使用一种奇/偶规则,这是一种算法,来确定某一区域是否是路径的“内部”。 243 | 244 | 图4.5 245 | 246 | 在许多绘图场景您都会使用此规则。例如,奇偶规则允许你绘制的复杂边界。它提供一种从文本布局中剪切区域的方法,以便于插入图像。同时为选择区域反转提供了基础,允许你创建正负空间等等。 247 | 该算法通过投射光线(一端固定的直线,指向给定的方向)从路径内的点到路径外远处的点,算法计算光线穿过任何一条线的次数。如果光线穿过次数为偶数,该点在形状之外。如果为基数,则在其内部。 248 | 图4-5左侧嵌套的正方形示例中,中心点通过四条交线后离开路径,在最后一个交点之前,第三个和第四个框内部的正方形之间,只有3个交线。因此,更具奇/偶填充规则,第一个位于形状外部,因为它穿过偶数行,第二个点位于内部,因为它穿过奇数个点。 249 | 这样产生的结果即为图4-5所示。当你使用usesEvenOddFillRule属性之后,UIKit会用此来计算哪些是内部,应该填充,而哪些是外部,不能填充。 250 | 在Quartz中有一个特殊的上下文填充方法CGContextEOFillPath(),会使用奇偶填充法来填充路径。如果用普通的版本,CGContextFillPath(), 则不会使用奇偶填充规则。 251 | 252 | ### 检索路径边界和中心 253 | UIBezierPath类有一个bounds(边界)属性。它返回一个完全把矩形包裹住的矩形,包括控制点。如果把该属性当作路径的frame,起始点几乎从来不会为零。相反,它展示了最左上的点,或是用于构建路径的控制点。 254 | bounds是很有用的,因为它提供了快速的计算和足够好的近似绘制。图4-6展示了一个固有的缺点。当您使用精确的布局时,bounds不会把图像曲线的真实长度考虑在计算中。因此,bounds几乎总是超过实际边界范围的。 255 | 256 | 图4.6 257 | 虚线表示的路径外界矩形为bounds,内部矩形为真实边界。 258 | 259 | 尽管尺寸相近,但是它们并不相同。当你需要绘制完美的图像时,这些不同可能会非常重要。 260 | 代码4-3使用Quartz的CGPathGetPathBoundingBox()方法来获得更好的bounds。这种方法需要更多的计算,因为函数必须在控制点之间进行插值,以沿着曲线建立点。得到的结果会远好于之前的方法。通过时间开销和处理器需求来换取准确性。 261 | PathBoundingBox()方法返回图4-6中更加接近的边界。相关函数PathBoundingBoxWithLinewidth(),通过吧lineWidth属性加入计算来获得bounds。描边绘制是在边界上绘制的,因此路径通常会延展一半的线宽在所有方向。 262 | 代码4-3还包括了一个计算路径中心点的方法,同时包括一般的(PathCenter())和精确的(PathBoundingCenter())结果。后者提供了更好的解决方案,因为精确度很重要。(轻微的误差都会导致仿射变换的错误) 263 | 264 | > 注意: 在我自己的开发中,我会将代码4-3的例子实现为UIBezierPath的类别。 265 | 266 | ``` 267 | //Return calculated bounds 268 | CGRect PathBoundingBox(UIBezierPath *path) 269 | { 270 | return CGPathGetPathBoundingBox(path.CGPath); 271 | } 272 | 273 | //Return calculated bounds taking line width into account 274 | CGRect PathBoundingBoxWithLineWidth(UIBezierPath *path) 275 | { 276 | CGRect bounds = PathBoundingBox(path); 277 | return CGRectInset(bounds, -path.lineWidth/2.f, -path.lineWidth/2.f); 278 | } 279 | 280 | //Return the calculated center point 281 | CGPoint PathBoundingCenter(UIBezierPath *path) 282 | { 283 | return RectGetCenter(PathBoundingBox(path)); 284 | } 285 | 286 | //Return the center point for the bounds property 287 | CGPoint PathCenter(UIBezierPath *path) 288 | { 289 | return RectGetCenter(path.bounds); 290 | } 291 | 292 | ``` 293 | 294 | ### 变换路径 295 | 贝塞尔曲线的applyTransform:方法转换路径上的所有点和控制点,通过把仿射变换矩阵作为参数传递给这个方法。这些更改会在适当的位置发生并且会更新路径。比如,使用如下比例变换后,整个myPath都会缩小: 296 | ``` 297 | [myPath applyTransfrom:CGAffineTransformMakeScale(0.5f, 0.5f)]; 298 | ``` 299 | 如果希望保留原始路径,清先创建一个副本,并将变换应用于该副本。这使你可以执行多个更改,而保持原始的路径不变: 300 | ``` 301 | UIBezierPath *pathCopy = [myPath copy]; 302 | [pathCopy applyTransfrom:CGAffineTransformMakeScale(0.5f, 0.5f)]; 303 | ``` 304 | 使用identity转换是无法还原路径的,他会产生一个“无操作”的结果(路径保持不变)而不是反转(路径返回原来的位置)。 305 | 306 | ##### 起始点问题 307 | 使用变换不是总会产生你所期望的结果,如图4-7所示,比如说,我创建了一个路径,并让其旋转: 308 | 309 | ``` 310 | [path applyTransform:CGAffineTransfromMakeRotation(M_PI/9)]; 311 | ``` 312 | 图4-7中的左图展示了你也许想要得到的结果:图像绕着自己的中心点旋转了20度。右图才是在你没有设置一个变换的起始点时,真实会发生的效果。路径会环绕坐标系的远点来旋转。 313 | 314 | 图4.7 315 | 316 | 幸运的是,确保选择和放缩以希望的方式发生相对比较容易。代码4-4详细介绍了ApplyCenteredPathTransform(),这个方法通过设置平移来结束仿射变换,然后重置坐标系。这些额外的步骤会创建一个你想要的可控结果。 317 | 318 | ``` 319 | //Translate path`s orign to its center before applying the transform 320 | void ApplyCenteredPathTransform(UIBezierPath *path,CGAffineTransform transform) 321 | { 322 | CGPoint center = PathBoundingCenter(path); 323 | CGAffineTransform t = CGAffineTransformIdentity; 324 | t = CGAffineTransformTranslate(t, center.x, center.y); 325 | t = CGAffineTransformConcat(transfrom, t); 326 | t = CGAffineTransformTranslate(t, -center.x, -center.y); 327 | [path applyTransform:t]; 328 | } 329 | 330 | //Rotate path around its center 331 | void RotatePath(UIBezierPath *path, CGFloat theta) 332 | { 333 | CGAffineTransform t = CGAffineTransformMakeRotation(theta); 334 | ApplyCenteredPathTransform(path, t); 335 | } 336 | ``` 337 | 338 | ##### 其他变换 339 | 和选择一样,如果你想在适当的位置缩放一个物体,确保应用于边界的中心发生变化。图4-8显示了缩放路径的变换:在每个维度减少百分之85。创建这种变换的函数如代码4-5所示,同时还附有一些其他的便捷转换。 340 | 341 | 图4.8 342 | 343 | 代码4-4和4-5实现了许多Quartz中常见的转换绘图任务。除了选择和缩放外,这些功能还能满足您将路径移动到不同的位置,或者进行镜像处理。居中的原点可以确保变换按最常规的模式进行。 344 | ``` 345 | //Scale path to sx, sy 346 | void ScalePath(UIBezierPath *path, CGFloat sx, CGFloat sy) 347 | { 348 | CGAffineTransform t = CGAffineTransformMakeScale(sx, sy); 349 | ApplyCenteredPathTransform(path, t); 350 | } 351 | 352 | //Offset a path 353 | void OffsetPath(UIBezierPath *path, CGSize offset) 354 | { 355 | CGAffineTransform t = CGAffineTransformMakeTranslation(offset.width, offset.height); 356 | ApplyCenteredPathTransform(path, t); 357 | } 358 | 359 | //Move path to a new origin 360 | void MovePathToPoint(UIBezierPath *path, CGPoint destPoint) 361 | { 362 | CGRect bounds = PathBoundingBox(path); 363 | CGSize vector = PointsMakeVector(bounds.origin, destPoint); 364 | OffsetPath(path, vector); 365 | } 366 | 367 | //Center path around a new point 368 | void MovePathCenterToPoint(UIBezierPath *path, CGPoint destPoint) 369 | { 370 | CCRect bounds = PathBoundingCenter(path); 371 | CGSize vector = PointsMakeVector(bounds.origin, destPoint); 372 | vector.width -= bounds.size.width /2.f; 373 | vector.height -= bounds.size.height /2.f; 374 | offsetPath(path, vector); 375 | } 376 | 377 | //Flip horizontally 378 | void MirrorPathHorizontally(UIBezierPath *path) 379 | { 380 | CGAffineTransform t = CGAffineTransformMakeScale(-1, 1); 381 | ApplyCenteredPathTransform(path, t); 382 | } 383 | 384 | //Flip vertically 385 | void MirrorPathVertically(UIBezierPath *path) 386 | { 387 | CGAffineTransform t = CGAffineTransformMakeScale(1, -1); 388 | ApplyCenteredPathTransform(path, t); 389 | } 390 | ``` 391 | ### 拟合贝塞尔曲线 392 | 代码4-6处理了使用贝塞尔曲线最重要的任务之一:以任意点和比例创建路径,并将其绘制在特定的矩形中。基于矢量的拟合可以很好的把您的作品可靠地放入预期的尺寸和目标中。 393 | 代码4-6中的拟合方法,你可能已经在本书中看过好多次了。不同的是,这个版本使用放缩和拟合矩形将变换应用于路径。路径被移动到新中心,然后缩小到合适的尺寸。 394 | 395 | ``` 396 | void FitPathToRect(UIBezierPath *path, CGRect destRect) 397 | { 398 | CGRect bounds = PathBoudingBox(path); 399 | CGRect fitRect = RectByFittingRect(bounds, destRect); 400 | CGFloat scale = AspectScaleFit(bounds, destRect); 401 | 402 | CGPoint newCenter = RectGetCenter(fitRect); 403 | MovePathCenterToPoint(path, newCenter); 404 | ScalePath(path, scale, scale); 405 | } 406 | ``` 407 | ### 从字符串创建贝塞尔曲线 408 | Core Text简化了将字符串转换为贝塞尔曲线的过程。代码4-7提供了简单的转换功能。它可以把字符串转换为单独的核心文本字形,通过对立的CGPath来表示。该函数将每一个字母的路径都添加到生成的贝塞尔曲线中,然后按照字母的大小进行偏移。 409 | 添加完所有的字母之后,路径会在垂直方向产生一个镜像。他把Quartz格式的输出转换为UIKit的图层。你可以像处理其他路径一样处理这些字符串路径,设置线宽,填充颜色和图案,按你喜欢的方式对他们做仿射变换。图4-9展示了一个用粗体的Baskervile字体并填充绿色创建的路径图案。 410 | 411 | 图4.9 412 | 413 | 以下是创建该路径的代码片段: 414 | 415 | ``` 416 | UIFont *font = [UIFont fontWithName:@"Basketville-Bold" size:16]; 417 | UIBezierPath *path = BezierPathFromString(@"Hello World", font); 418 | FitPathToRect(path, targetRect); 419 | [path fill:GreenStripesColor()]; 420 | [path strokeInside:4]; 421 | ``` 422 | 有趣的是,字体大小在这个特定的绘图中不起作用。路径是按比例缩放到目标矩形中的,这样你几乎可以使用任何字体来创建源,如果你想让路径看起来像正常排版的单词,只需要用黑色颜色来填充,不描边即可。这个用绿色填充的示例使用内侧笔来确保路径的边缘保持清晰。 423 | 424 | ``` 425 | UIBeizerPath *BezierPathFromString(NSString *string, UIFont *font) 426 | { 427 | //Initialize path 428 | UIBezierPath *path = [UIBezierPath bezierPath]; 429 | if(!string.length) return path; 430 | 431 | //Create font ref 432 | CTFontRef fontRef = CTFontCreateWithName((__bridge CFStringRef)font.fontName, font.pointSize, NULL); 433 | if(fontRef == NULL) 434 | { 435 | NSLog(@"Error retrieving CTFontRef from UIFont"); 436 | } 437 | 438 | //Create glyphs (that is, individual letter shapes) 439 | CGGlyph *glyphs = malloc(sizeof(CGGlyph) * string.length); 440 | const unichar *chars = (const unichar *)[string cStringUsingEncoding:NSUnicodesStringEncoding]; 441 | BOOL success = CTFontGetGlyphsForCharacters(fontRef, chars, glyphs, string.length); 442 | if(!success) 443 | { 444 | NSLog(@"Error retrieving string glyphs"); 445 | CFRelease(fontRef); 446 | free(glyphs); 447 | return nil; 448 | } 449 | 450 | //Draw each char into path 451 | for(int i = 0; i < string.length; i++) 452 | { 453 | //Glyph to CGPath 454 | CGGlyph glyph = glyphs[i]; 455 | CGPathRef pahtRef = CTFontCreatePathForGlyph(fontRef, glyph, NULL); 456 | 457 | //Append CGPath 458 | [path appendPath:[UIBezierPath bezierPathWithCGPath:pathRef]]; 459 | 460 | //Offset by size 461 | CGSize size = [[string substringWithRange: NSMakeRange(i, 1)] sizeWithAttributes:@{NSFontAttributeName:font}]; 462 | OffsetPath(path, CGSizeMake(-size.width, 0)); 463 | } 464 | 465 | //clean up 466 | free(glyphs); CFRelease(fontRef); 467 | 468 | //Return the path to the UIKit coordinate system 469 | MirrorPathVertically(path); 470 | return path; 471 | } 472 | ``` 473 | 474 | ### 添加虚线 475 | UIKit中添加虚线是非常容易的。比如说,你想添加一个简单的重复模式,如下: 476 | 477 | ``` 478 | CGFloat dashes[] = {6, 2}; 479 | [path setLineDash:dashes count:2 phase:0]; 480 | ``` 481 | 数组以点为单位,定义了绘制边线的时候实与虚的比例。本例中,表示画6点的实线,留两点的空白。当然,虚线也会根据你的上下文的scale和视图的压缩来变化。也就是说,有很多方法可以得到虚线效果。表4-1展示了一些基本选项。 482 | 虚线的phase表示从图形开始便宜的量。在这个例子中整个图案有8点长。把phase从0调整到7,可以动画化虚线。产生环绕贝塞尔曲线路径爬行的蚂蚁效果。你将在第七章中读到如何实现这个效果。 483 | 484 | 表4-1-1 485 | 表4-1-2 486 | 487 | ### 创建多边形路径 488 | 尽管UIBezierPath提供了简单的椭圆和矩形,但是他没能提供简单的多边形。代码4-8填补了这一空白,返回一个任意边数的多边形。一些基本形状如下图所示: 489 | 490 | 图4.10 491 | 492 | 这个函数通过把一个圆(2pi弧度)分成若干等份,然后把各个点连接起来画线来完成形状。最后一步,关闭路径,避免在起点绘制。图4-11显示了当你没有正确的关闭形状时,会遇到的效果。Quartz会把角视为两个线段,而非正确的连接。 493 | 494 | 图4.11 495 | 496 | 所有返回的路径都适用单位尺寸——也就是说,都是适配{0, 0, 1, 1}矩形的。您需要根据需要调整路径的大小。同样的,第一个点总是在形状的顶部,所以,你想返回一个正方形而非棱形,需要选择90度。 497 | 498 | ``` 499 | UIBezierPath *bezierPolygon(NSUInteger numberOfSides) 500 | { 501 | if(numberOfSides < 3) 502 | { 503 | NSlog(@"Error: Please supply at least 3 sides"); 504 | return nil; 505 | } 506 | 507 | UIBezierPath *path = [UIBezierPath bezierPath]; 508 | 509 | //Use a unit rectangle as the destination 510 | CGRect destinationRect = CGRectMake(0, 0, 1, 1); 511 | CGPoint center = RectGetCenter(destinationRect); 512 | CGFloat r = 0.5f;//radius 513 | 514 | BOOL firstPoint = YES; 515 | for(int i = 0; i < (numberOfSides - 1); i++) 516 | { 517 | CGFloat theta = M_PI + i * TWO_PI/ numberOfSides; 518 | CGFloat dTheta = TWO_PI / numberOfSides; 519 | 520 | CGPoint p; 521 | if(firstPoint) 522 | { 523 | p.x = center.x + r *sin(theta); 524 | p.y = center.y + r *cos(theta); 525 | [path moveToPoint:p]; 526 | firstPoint = NO; 527 | } 528 | p.x = center.x + rx * sin(theta + dTheta); 529 | p.y = center.y + ry * cos(theta + dTheta); 530 | [path addLineToPoint:p]; 531 | } 532 | 533 | [path closePath]; 534 | 535 | return path; 536 | } 537 | ``` 538 | 539 | ### 线的连接处 540 | 路径的lineJoinStyle属性定义了两条线相交时如何处理。Quartz提供了三种可以使用的样式,如图4-12所示,默认情况下,kCGLineJoinMiter会创建一个锐利的角。可以用kCGLineJoinRound来代替,让焦点变得圆润。还有一种是kCGLineJoinBevel。他可以让交点处变得平整。 541 | 542 | 图4.12 543 | 544 | 线上不和其他线相交的点有他们自己的特点,被称为线帽。图4-13,显示了三种可能的线帽样式。我添加了灰色的垂直线来表示每条线的自然终点。kCGLineCapButt样式刚好在灰线上停止了。第二个和第三个,超出了灰线一点点的分别是kCGLineCapSquare和kCGLineCapRound,伸长的长度刚好是线宽的一半,线条越宽,伸长的越长。 545 | 546 | 图4.13 547 | 548 | ### 斜接限制 549 | 斜接限制限制了形状的尖角。如图4-14所示,当斜接的对角线长度(即两条线之间的三角形连接)超过路径的极限时,Quartz会将这些点转换为斜角连接。 550 | 551 | 图4.14 552 | 553 | 在左图中,形状具有锐角,他的尖角尚未达到路径的miterLimit。如果降低这个限制,就会如右图所示,产生强制剪切。 554 | 贝塞尔曲线的默认斜切限制是10。这会影响到11度以下的任意角度。更改斜接限制的时候,默认截止点会进行调整。例如,在30度时,必须将极限降低到3.8点以下才能看到效果。在0到11度的范围内,斜度的增长最为迅猛。11度角只有10.4点长的自然斜接,而在5度时立马达到了23点,在2度角时几乎变成了60点。添加合理的限制可以确保随着角度的变小,斜接长度不会超过绘图。 555 | 556 | 附录B更加深入地研究了斜齿轮背后的数学。 557 | 558 | ### 弯曲形状 559 | 图4-14显示了一个围绕一个中心点做一些曲线的简单形状。他是通过构建您在图4-15中看到的形状的相同函数构建的。 560 | 561 | 图4.15 562 | 563 | 如果你觉得代码4-9中的代码和4-8种多边形生成的代码十分相似,那么,你是对的。这两个函数的不同之处在于代码4-9是在两点之间创建曲线,而不是画一条直线。指定要创建曲线的数量,曲线的拐点由两个控制点确定。这些点由函数的百分比拐点参数设置。正数远离中心,在形状周围形成凸起,负数朝向中心——甚至可能超过中心——产生上图中的其他图像样式。 564 | 565 | ``` 566 | UIBezierPath *BezierInflectedShape(NSUInteger, numberOfInflections, CGFloat percentInflection) 567 | { 568 | if(numberOfInflections < 3) 569 | { 570 | NSLog(@"Error: Please supply at least 3 inflections"); 571 | return nil; 572 | } 573 | UIBezierPath *path = [UIBezierPath bezierPath]; 574 | CGRect destinationRect = CGRectMake(0, 0, 1, 1); 575 | CGPoint center = RectGetCenter(destinationRect); 576 | CGFloat r =0.5; 577 | CGFloat rr = r * (1.0 + percentInflection); 578 | 579 | BOOL firstPoint = YES; 580 | for(int i = 0; i < numberOfInflections; i++) 581 | { 582 | CGFloat theta = i *TWO_PI /numberOfInflections; 583 | CGFloat dTheta = TWO_PI / numberOfInflections; 584 | 585 | if(firstPoint) 586 | { 587 | CGFloat xa = center.x + r * sin(theta); 588 | CGFloat ya = center.y + r * cos(theta); 589 | CGPoint pa = CGPointMake(xa, ya); 590 | [path moveToPoint:pa]; 591 | firstPoint = NO; 592 | } 593 | 594 | CGFloat cp1x = center.x + rr *sin(theta + dTheta /3); 595 | CGFloat cp1y = center.y + rr *cos(theta + dTheta /3); 596 | CGPoint cp1 = CGPointMake(cp1x, cp1y); 597 | 598 | CGFloat cp2x = center.x + rr * sin(theta + 2*dTheta/3); 599 | CGFloat cp2y = center.y + rr * cos(theta + 2*dTheta/3); 600 | CGPoint cp2 = CGPointMake(cp2x, cp2y); 601 | 602 | CGFloat xb = center.x + r *sin(theta + dTheta); 603 | CGFloat yb = center.y + r *cos(theta + dTheta); 604 | CGPoint pb = CGPointMake(xb, yb); 605 | 606 | [path addCurveToPoint:pd controlPoint1:cp1 controlPoint2:cp2]; 607 | } 608 | 609 | [path closePath]; 610 | return path; 611 | } 612 | ``` 613 | 调整代码4-9,绘制线段而非曲线,可以创建星型。在代码4-10中,位于每个点中间的单个控制点为星型的角度提供了参考。图4-16展示了使用代码4-10绘制的一些形状。 614 | 615 | 图4.16 616 | 617 | ``` 618 | UIBezierPath *BezierStarShape(NSUInteger numberOfInflections,CGFloat percentInflection) 619 | { 620 | if(numberOfInflections < 3) 621 | { 622 | NSLog(@"Error: Please supply at least 3 inflections"); 623 | return nil; 624 | } 625 | 626 | UIBezierPath *path = [UIBezierPath bezierPath]; 627 | CGRect destinationRect = CGRectMake(0, 0, 1, 1); 628 | CGPoint center = RectGetCenter(destinationRect); 629 | CGFloat r =0.5; 630 | CGFloat rr = r * (1.0 + percentInflection); 631 | 632 | BOOL firstPoint = YES; 633 | for(int i = 0; i < numberOfInflections; i++) 634 | { 635 | CGFloat theta = i *TWO_PI /numberOfInflections; 636 | CGFloat dTheta = TWO_PI / numberOfInflections; 637 | 638 | if(firstPoint) 639 | { 640 | CGFloat xa = center.x + r * sin(theta); 641 | CGFloat ya = center.y + r * cos(theta); 642 | CGPoint pa = CGPointMake(xa, ya); 643 | [path moveToPoint:pa]; 644 | firstPoint = NO; 645 | } 646 | 647 | CGFloat cp1x = center.x + rr *sin(theta + dTheta /2); 648 | CGFloat cp1y = center.y + rr *cos(theta + dTheta /2); 649 | CGPoint cp1 = CGPointMake(cp1x, cp1y); 650 | 651 | CGFloat xb = center.x + r *sin(theta + dTheta); 652 | CGFloat yb = center.y + r *cos(theta + dTheta); 653 | CGPoint pb = CGPointMake(xb, yb); 654 | 655 | [path addLineToPoint:cp1]; 656 | [path addLinetoPoint:pb]; 657 | } 658 | 659 | [path closePath]; 660 | return path; 661 | } 662 | ``` 663 | 664 | 665 | ### 总结 666 | 本章介绍了UIBezierPath类,他提供了基础的类来属性来调整路径。你学习了如何填充和描边,已经如何构建复杂的形状。同时也学习了,常规方式和特殊方式构建新的形状的内容。再继续讨论更高级的路径相关内容之前,注意以下几点: 667 | * 把素材添加到应用中,他创建了一个基于矢量的封装,可以在任何位置,任何尺寸任何角度使用该形状。他们是分辨率无关的。无论他被绘制的有多大,他都能保持清晰度。 668 | * 当你将字符串转换成路径时,你可以在文本中获得很多趣味性的东西。从用颜色填充文本,到对单个字符进行造型,都能够为你的文字提供丰富的表现力。 669 | * 用边界框来计算绘图边界时要格外小心。路径的倾斜和线帽会一定程度上超过路径的基线。 670 | -------------------------------------------------------------------------------- /4-路径基础/table4-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/table4-1-1.png -------------------------------------------------------------------------------- /4-路径基础/table4-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/4-路径基础/table4-1-2.png -------------------------------------------------------------------------------- /5-路径深入/5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-1.png -------------------------------------------------------------------------------- /5-路径深入/5-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-10.png -------------------------------------------------------------------------------- /5-路径深入/5-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-11.png -------------------------------------------------------------------------------- /5-路径深入/5-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-12.png -------------------------------------------------------------------------------- /5-路径深入/5-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-13.png -------------------------------------------------------------------------------- /5-路径深入/5-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-14.png -------------------------------------------------------------------------------- /5-路径深入/5-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-15.png -------------------------------------------------------------------------------- /5-路径深入/5-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-16.png -------------------------------------------------------------------------------- /5-路径深入/5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-2.png -------------------------------------------------------------------------------- /5-路径深入/5-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-3.png -------------------------------------------------------------------------------- /5-路径深入/5-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-4.png -------------------------------------------------------------------------------- /5-路径深入/5-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-5.png -------------------------------------------------------------------------------- /5-路径深入/5-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-6.png -------------------------------------------------------------------------------- /5-路径深入/5-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-7.png -------------------------------------------------------------------------------- /5-路径深入/5-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-8.png -------------------------------------------------------------------------------- /5-路径深入/5-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/5-9.png -------------------------------------------------------------------------------- /5-路径深入/other1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/other1.png -------------------------------------------------------------------------------- /5-路径深入/table5-1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/table5-1-1.png -------------------------------------------------------------------------------- /5-路径深入/table5-1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/5-路径深入/table5-1-2.png -------------------------------------------------------------------------------- /6-绘制渐变/6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-1.png -------------------------------------------------------------------------------- /6-绘制渐变/6-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-10.png -------------------------------------------------------------------------------- /6-绘制渐变/6-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-11.png -------------------------------------------------------------------------------- /6-绘制渐变/6-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-12.png -------------------------------------------------------------------------------- /6-绘制渐变/6-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-13.png -------------------------------------------------------------------------------- /6-绘制渐变/6-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-14.png -------------------------------------------------------------------------------- /6-绘制渐变/6-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-15.png -------------------------------------------------------------------------------- /6-绘制渐变/6-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-16.png -------------------------------------------------------------------------------- /6-绘制渐变/6-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-17.png -------------------------------------------------------------------------------- /6-绘制渐变/6-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-18.png -------------------------------------------------------------------------------- /6-绘制渐变/6-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-19.png -------------------------------------------------------------------------------- /6-绘制渐变/6-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-2.png -------------------------------------------------------------------------------- /6-绘制渐变/6-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-20.png -------------------------------------------------------------------------------- /6-绘制渐变/6-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-21.png -------------------------------------------------------------------------------- /6-绘制渐变/6-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-22.png -------------------------------------------------------------------------------- /6-绘制渐变/6-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-23.png -------------------------------------------------------------------------------- /6-绘制渐变/6-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-3.png -------------------------------------------------------------------------------- /6-绘制渐变/6-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-4.png -------------------------------------------------------------------------------- /6-绘制渐变/6-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-5.png -------------------------------------------------------------------------------- /6-绘制渐变/6-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-6.png -------------------------------------------------------------------------------- /6-绘制渐变/6-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-7.png -------------------------------------------------------------------------------- /6-绘制渐变/6-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-8.png -------------------------------------------------------------------------------- /6-绘制渐变/6-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/6-绘制渐变/6-9.png -------------------------------------------------------------------------------- /6-绘制渐变/6-绘制渐变.md: -------------------------------------------------------------------------------- 1 | # 绘制渐变 2 | 3 | 在iOS中,渐变描绘颜色的逐渐改变。用于遮盖绘制,在计算机生成的图形中模拟真实世界的照明,渐变是一个重要的组件,可以产生强大的视觉效果。本章介绍iOS的渐变,并演示如何使用他们来添加活力。 4 | 5 | ### 渐变 6 | 渐变总是包含至少两种颜色。颜色与开始点和结束点向关联,在0到1之间变化。除此之外,梯度可以十分简单,也可以变得非常复杂如果你需要的话。图6-1展示了这个范围。上图展示了可能是最简单的梯度,他从白色的0变为黑色的1。下图显示了由24种不同的色调构成的渐变。这个复杂的渐变从红色到橙色,黄色到绿色等等。 7 | 8 | 图6-1 9 | 10 | 如果您之前接触过梯度渐变,您会知道您可以绘制线性和辐射状的输出。如果没有,图6-2介绍了这两种风格。左边为线性渐变,从底部白色到顶部黑色。线性渐变沿着指定的轴来绘制颜色。 11 | 相比下,辐射状的渐变从开始到结束会改变其图形的宽度。在右侧,辐射从白色开始(中间),延伸到附近的黑色。在本例中,半径从中间的0开始,在右边沿的范围结束。随着半径增大,颜色变暗,从而产生这里看到的球体。 12 | 13 | 图6-2 14 | 15 | 你可能还没有意识到,图6-2的两幅图使用的是相同的梯度原——如图6-1上图所示的那样。渐变没有形状,位置或任何几何属性。他们简单地描述了颜色是如何改变的。绘制渐变的方式完全取决于你以及你所使用的核心图像的方法。 16 | 17 | ### 封装CGGradientRif类 18 | CGGrandientRif是core foundation风格的类,储存了从0.0到1.0范围内任意数量的颜色。你通过传递两个设置颜色和位置的数组来建立渐变,如本例所示: 19 | 20 | ``` 21 | CGGradientRef CGGradientCreateWithColor(CGColorSapceRef space, CFArrayRef colors, const CGFloat locations[]) 22 | ``` 23 | 在进一步了解核心图像的实现之前,我们先暂停一下,来了解了解Objective-C的解决方法,这个方法有时候是非常有用的。 24 | 总的来说,使用Objective-C封装的渐变要比需要关心内存管理的C语言混合和Core Foundation风格的元素要简单的多。如这里使用的两个数组,因为这里没有UIKit提供简便包装,也没有桥接的基础,我构建了一个Objective-C封装,这就是该解决方案发挥作用的地方。我使用了一点点属性上的诡计,让ARC来管理Core Foundation引用就像一个普通的Cocoa对象。http://llvm.org/ 网站上描述了这一功能。 25 | 26 | > GCC在结构体指针中引入了__attribute__((NSObject)),表示“这是一个对象”。这很有用,因为许多低级数据结构被声明为不透明指针,如CFStringRef,CFArrayRef等。 27 | 28 | 你用这个技巧来创立一个派生类,这里是我为使用Quartz渐变创建的类型定义: 29 | 30 | ``` 31 | typedef __attribute__((NSObject)) CGGradientRef GradientObject; 32 | ``` 33 | 34 | 此声明使你在使用ARC内存管理时能够建立Core Foundation驱动的超出桥接范围的类属性类型。这很重要,因为作为一个规则,Quartz并不是什么都不耗费就可以接入UIKit的。使用派生类需要使用ARC风格的strong管理来创建属性: 35 | ``` 36 | @property (nonatomic, strong) GradientObject storedGradient; 37 | ``` 38 | 当代码6-1中的创建的Gradient实例释放时,在底层的CGGradientRef也被释放了。你不必去创建特殊的dealloc方法来管理Core Foundation的对象。你得到的是一个带有Objective-C接口的渐变类。你可以使用UIColor的数组和NSNumber的数组。 39 | 40 | > 警告:正如你在这里看到的,这种属性方法需要明确的类型定义。避免普遍使用具有其他语言的特点。如__typeof。更多详细信息请查阅LLVM的文档。我觉得使用这种方法很舒服很顺手,因为苹果的工程师也向我介绍了它。 41 | 42 | ``` 43 | @interface Gradient () 44 | @property (nonatomic, strong) GradientObject storedGradient; 45 | @end 46 | 47 | @implementation Gradient 48 | - (CGGradientRef) gradient 49 | { 50 | //Expose the internal GradientObject property 51 | //as a CGGradientRef to the outside world on demand 52 | return _storedGradient; 53 | } 54 | 55 | //Primary entry point for the class. Construct a gradient 56 | //with the supplied colors and locations 57 | + (instancetype)gradientWithColors:(NSArray *)colorArray locations:(NSArray *)locationArray 58 | { 59 | //Establish color space 60 | CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB(); 61 | if(space == NULL) 62 | { 63 | NSLog(@"Error: Unable to create RGB color space"); 64 | return nil; 65 | } 66 | 67 | //Convert NSNumber *locations array to CGFloat * 68 | CGFloat locations[locationArray.count]; 69 | for(int i=0; i < locationArray.count; i++) 70 | locations[i] = fminf(fmaxf([locationArray[i] floatValue], 0), 1); 71 | 72 | //Convert colors array to (id) CGColorRef 73 | NSMutableArray *colorRefArray = [NSMutableArray array]; 74 | for(UIColor *color in colorsArray) 75 | [colorRefArray addObject:(id)color.CGColor]; 76 | 77 | //Build the internal gradient 78 | CGGradientRef gradientRef = CGGradientCreateWithColors(space, (__bridge CFArrayRef) colorRefArray, locations); 79 | CGColorSpaceRelease(space); 80 | if(gradientRef == NULL) 81 | { 82 | NSLog(@"Error:Unable to construct CGGradientRef"); 83 | return nil; 84 | } 85 | 86 | //Build the wrapper, store the gradient, and return 87 | Gradient *gradient = [[self alloc] init]; 88 | gradient.storedGradient = gradientRef; 89 | CGGradientRelease(gradientRef); 90 | return gradient; 91 | } 92 | 93 | + (instancetype)gradientFrom:(UIColor *)color1 to:(UIColor *)color2 94 | { 95 | return [self gradientWithColors:@[color1,color2] locations:@[@(0.0f),@(1.0f)]]; 96 | } 97 | @end 98 | ``` 99 | 100 | ### 绘制渐变 101 | Quartz提供两种绘制渐变的方法:线性和放射性,CGContextDrawLinearGradient()和CGContextDrawRadialGradient()函数在指定的起点和终点之间绘制渐变。本节中的设置都是使用从紫色到绿色的渐变,以及共同的起点和终点。变化的是绘制到上下文的函数和参数。 102 | ##### 绘制线性渐变 103 | 图6-3显示了基本的使用线性渐变函数来绘制的方法: 104 | ``` 105 | void CGContextDrawLinearGradient(CGContextRef context, CGGradientRef gradient, CGPoint startPoint, CGPoint endPoint, CGGradientDrawingOptions options); 106 | ``` 107 | 这个绿色到紫色的渐变是从左上角到右下角绘制的: 108 | 109 | 图6-3 110 | 111 | 最后一个参数是选填的,您可以用它来指定渐变超出其起点和终点属性。使用0(否,图6-3)或kCGGradientDrawsBeforeStartLocation或kCGGradientDrawsAfterEndLocation。图6-4显示了这些选项的设置: 112 | 113 | 图6-4 114 | 115 | ##### 绘制辐射状渐变 116 | 辐射状渐变绘制函数比线性函数多了两个参数。这俩参数指定了图像开始和结束时的半径。图6-5显示了以初始半径20最终半径为50绘制的绿色到紫色的渐变。左图没有使用options选项,右图的版本在开始和结束之前和之后继续绘制。下面的圆圈会被绘图矩形的边界裁剪: 117 | 118 | ``` 119 | void CGContextDrawRadialGradient(CGContextRef context, CGGradientRef gradient, CGPoint startCenter, CGFloat startRadius, CGPoint endCenter, CGFloat endRadius, CGGradientDrawingOptions options); 120 | ``` 121 | 图6-5 122 | 123 | 代码6-2展示了我用Objective-C封装的线性绘制和辐射性绘制的方法。这个方法构成了代码6-1自定义渐变类的一部分。他们提供了简便的方法在使用中UIKit绘图上下文来绘制嵌入的核心图像渐变。 124 | 125 | ``` 126 | //Draw a linear gradient between the two points 127 | - (void) drawFrom:(CGPoint)p1 toPoint:(CGPoint)p2 style:(int)mask 128 | { 129 | CGContextRef context = UIGraphicsGetCurrentContext(); 130 | if(context == NULL) 131 | { 132 | NSLog(@"Error:No context to draw to"); 133 | return; 134 | } 135 | CGContextDrawLinearGradient(context, self.gradient, p1, p2, mask); 136 | } 137 | 138 | //Draw a radial gradient between the two points 139 | - (void) drawRadialFrom:(CGPoint)p1 toPoint:(CGPoint)p2 radii:(CGPoint)radii style:(int)mask 140 | { 141 | CGContextRef context = UIGraphicsGetCurrentContext(); 142 | if(context == NULL) 143 | { 144 | NSLog(@"Error: No context to draw to"); 145 | return; 146 | } 147 | 148 | CGContextDrawRadialGradient(context, self.gradient, p1, radii.x, p2, radii.y, mask); 149 | } 150 | ``` 151 | 152 | ### 创建渐变 153 | 每个渐变都是由两组值组成的: 154 | * 有序的一组颜色 155 | * 颜色变化发生的位置 156 | 例如,你可以定义从红色到绿色到蓝色的渐变,分别在0.0,0.5,1.0,沿着渐变的方向,渐变会在这些点之间进行插值。大约在0.33位置颜色,红色到绿色大约占了整个渐变的百分之66。或者,例如,想象一个简单的黑色到白色的渐变,中间灰色显示在开始和结束之间的位置。 157 | 你可以提供任意颜色的颜色和位置的序列。只要这些颜色在RGB或灰色的色域中(不能使用图案颜色绘制渐变)。位置序列介于0.0和1.0之间。如果您提供的值超出该范围,则创建函数返回NULL。 158 | 最常用的渐变时从白到黑,从白到透明,或者黑色到透明。因为使用了颜色的不同alpha等级。下面是一个方便的宏: 159 | ``` 160 | #define WHITE_LEVEL(_amt_, _alpha_) [UIColor colorWithWhite:(_amt_) alpha:(_alpha_)] 161 | ``` 162 | 此宏返回一个指定的白色程度和透明级别的灰度颜色。白色程度从0(黑色)到1(白色),alpha从0(透明)到1(透明)。 163 | 许多开发者使用颜色之间的默认插值来设置渐变,如代码6-1所示。本例创建一个从透明到黑色的渐变,并从百分之70的点绘制到百分之100覆盖在下面的绿色上。您可以在图6-6左上图看到绘制的结果。对比6-6中的其他图,你会发现“渐变缓冲”这个东西。 164 | 165 | ``` 166 | Gradient *gradient = [Gradient gradientFrom:WHITE_LEVEL(0, 0) to:WHITE_LEVEL(0, 1)]; 167 | 168 | //Calculate the points 169 | CGPoint p1 = RectGetPointAtPercents(path.bounds, 0.7, 0.5); 170 | CGPOint p2 = RectGetPointAtPercents(path.bounds, 1.0, 0.5); 171 | 172 | //Draw a green background 173 | [path fill:greenColor]; 174 | 175 | //Draw the gradient across the green background 176 | [path addClip]; 177 | [gradient drawFrom:p1 toPoint:p2]; 178 | ``` 179 | 图6-6 180 | 181 | ##### 缓冲 182 | 缓冲函数会改变阴影变化的速率。更具你选择的方法,他们提供了更加柔和的渐变效果。我最喜欢“加速进入”(EaseIn)和“加入进入加速退出”(EaseInOut)这两种柔和的效果,如图6-6中的右上和左下图一样。如你所见,这两种方法避免了突然的结束变化。这些严格的变化是由感知带产生的,也叫虚幻马赫带(注:名词illusory mach bands,我随便翻译的,不要纠结。)。马赫带是物理学家恩特斯马赫首先注意到的一种光学错觉,由大脑处理模式自然造成的,当边界出现稍微不同的灰色阴影时就会出现这种现象。他们出现在计算图像中因为绘制会在算法告诉他该停止的地方结束。在图6-6的左上图和右下图中,你可以在渐变的绘制区域看到这种效果。通过快入快出的绘图,可以在底层的颜色和渐变色之间有一个过渡叠加效果,而不会出现终止带。 183 | 图6-7显示了图6-6中渐变的缓冲方法。一组分别是:线性(左上角),快入(右上角),快入快出(左下角),快出(右下角)。缓冲效果使用方法的开始(in)或结束(out)来建立更多渐进的变化。这些方法在很多绘图和动画算法中都会用到。 184 | 185 | 图6-7 186 | 187 | 代码6-3定义了一个Gradient类方法,该方法从函数中构建渐变应用。你传递输入百分比(时间轴)的块,返回一个应用于开始和结束颜色的值(数量轴)。该方法插入颜色,并将值添加到渐变当中去。 188 | 这三个标准渐变缓冲方法使用两个参数:经过的时间和指数。你传递的指数决定了缓冲的类型,对于标准三次缓冲,第二个参数需要传入3,二次缓冲则传入2,传入1则为线性无缓冲。 189 | 你可以在插值块中应用你喜欢的任何函数。如下,使用输入输出三次缓冲曲线构建渐变: 190 | ``` 191 | Gradient *gradient = [Gradient gradientUsingInterpolationBlock:^(CGFloat percent){ 192 | return EaseInOut(percent, 3); 193 | } between:WHITE_LEVEL(0, 0) and:WHITE_LEVEL(0,1)]; 194 | ``` 195 | 196 | ``` 197 | typedef CGFloat(^InterpolationBlock)(CGFloat percent); 198 | 199 | //Build a custom gradient using the supplied block to 200 | //interpolate between the start and end colors 201 | +(instancetype)gradientUsingInterpolationBlock:(InterpolationBlack)block between:(UIColor *)c1 and:(UIColor *)c2 202 | { 203 | if(!block) 204 | { 205 | NSlog(@"Error: No interpolation block"); 206 | return nil; 207 | } 208 | 209 | NSMutableArray *colors = [NSMutableArray array]; 210 | NSMutableArray *locations = [NSMutableArray array]; 211 | int numberOfSamples = 24; 212 | for(int i = 0; i <= numberOfSamples; i++) 213 | { 214 | CGFloat amt = (CGFloat) i / (CGFloat) numberOfSamples; 215 | CGFloat percentage = fmin(fmax(0.0, block(amt)), 1.0); 216 | [colors addObject:InterpolateBetweenColors(c1, c2, percentage)]; 217 | [locations addObject:@(amt)]; 218 | } 219 | return [Gradient gradientWithColors:colors locations:locations]; 220 | } 221 | 222 | //Return an interpolated color 223 | UIColor *InterpolateBetweenColor(UIColor *c1,UIColor *c2, CGFloat amt) 224 | { 225 | CGFloat r1, g1, b1, a1; 226 | CGFloat r2, g2, b2, a2; 227 | 228 | if(CGColorGetNumberOfComponents(c1.CGColor) == 4) 229 | { 230 | [c1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1]; 231 | } 232 | else 233 | { 234 | [c1 getWhite:&r2 alpha:&a2]; 235 | g1 = r1; b1 = r1; 236 | } 237 | 238 | if(CGColorGetNumberOfComponents(c2.CGColor) == 4) 239 | [c2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2]; 240 | else 241 | { 242 | [c2 getWhite:&r2 alpha:&a2]; 243 | g2 = r2; b2 = r2; 244 | } 245 | 246 | CGFloat r = (r2 * amt) + (r1 * (1.0 - amt)); 247 | CGFloat g = (g2 * amt) + (g1 * (1.0 - amt)); 248 | CGFloat b = (b2 * amt) + (b1 * (1.0 - amt)); 249 | CGFloat a = (a2 * amt) + (a1 * (1.0 - amt)); 250 | return [UIColor colorWithRed:r green:g blue:b alpha:a]; 251 | } 252 | 253 | 254 | #pragma mark - Easing Functions 255 | 256 | //Ease only the beginning 257 | CGFloat EaseIn(CGFloat currentTime, int factor) 258 | { 259 | return powf(currentTime, factor); 260 | } 261 | 262 | //Ease only the end 263 | CGFloat EaseOut(CGFloat currentTime, int factor) 264 | { 265 | return 1 - powf((1 - currentTime), factor); 266 | } 267 | 268 | //Ease both beginning and end 269 | CGFloat EaseInOut(CGFloat currentTime, int factor) 270 | { 271 | currentTime = currentTime * 2.0; 272 | if(currentTime < 1) 273 | return (0.5 * pow(currentTime, factor)); 274 | currentTime -= 2.0; 275 | if(factor % 2) 276 | return 0.5 * (pow(currentTime,factor) + 2.0); 277 | return 0.5 * (2.0 - pow(currentTime,factor)); 278 | } 279 | ``` 280 | 281 | ### 添加边缘效果 282 | 放射性渐变可以让你画出有趣的圆形边缘效果。如图6-8,这个一个正弦渐变,而且,他只画在圆的边缘,路径的中间部分保持不变。 283 | 284 | 图6-8 285 | 286 | 代码6-2使用非直觉的方法来实现了这一效果,暂时了一种有趣的渐变应用,正弦函数被压缩到渐变的最后百分之25处。因为渐变是从中心向外绘制的,所以会仅在边缘参数阴影效果。 287 | 288 | ``` 289 | InterpolationBlock block = ^CGFloat (CGFloat percent) 290 | { 291 | CGFloat skippingPercnet = 0.75; 292 | if(percent < skippingPercent) return 0; 293 | CGFloat scaled = (percent - skippingPercent) * (1 / (1 - skippingPercent)); 294 | return sinf(scaled * M_PI); 295 | }; 296 | 297 | Gradient *gradient = [Gradient gradientUsingInterpolationBlock:block between:WHITE_LEVEL(0, 0) and:WHITE_LEVEL(0, 1)]; 298 | 299 | CGContextDrawRadialGradient(UIGraphicsGetCurrentContext(), gradient.gradient, center, 0, center, dx, 0); 300 | ``` 301 | 这样就可以像图6-9那样实现只在边缘应用缓冲效果了。插值块仅在某一个百分比过去之后才会调用缓冲方法——本例中,是径向的百分之五十: 302 | 303 | ``` 304 | InterpolationBlock block = ^CGFloat(CGFloat percent) 305 | { 306 | CGFloat skippingPercent = 0.5; 307 | if (percent < skippingPercent) return 0; 308 | CGFloat scaled = (percent - skippingPercent) *(1 / (1 - skippingPercent)); 309 | return EaseIn(scaled, 3); 310 | } 311 | ``` 312 | 图6-9右侧的图展示了延迟缓冲的效果。上升开始与0.5之后,如你所见,上升的较为缓慢,到0.75开始才真正的开始变慢。 313 | 314 | 图6-9 315 | 316 | ##### 基本渐变背景 317 | 比方说,你正在找一个不错的圆形按钮特效。基础的如6-2所示的辐射状效果也许对于你来说过于圆形了。而图6-9的延迟效果也可能过于平了。事实上,缓冲功能提供了非常好的按钮基础,如图6-10所示。 318 | 319 | 图6-10 320 | 321 | 代码片段如下: 322 | ``` 323 | InterpolationBlock block = ^CGFloat (CGFloat percent) 324 | { 325 | return EaseIn(percent, 3); 326 | }; 327 | ``` 328 | 在本章稍后,你会看到这种方法被再次使用,用于在按钮上建立一个“发光”的中心。 329 | 330 | ### 状态和透明层 331 | 再继续深入了解渐变之前,我们需要往后退一步,介绍一个重要的Quartz绘制功能。此功能在本章的例子中会出现,需要着重解释解释。 332 | 如果你使用过Photoshop(或类似的图像合成和编辑的软件),你可能非常熟悉图层。图层将图像封装为不同的独立容器。堆砌这些图层可以构造出复杂的图形。然后应用图层,为每一层添加阴影,高光和其他的装饰效果。重要的是,这些效果只使用一次就可以应用懂啊所有层中,而不管他们是否独立在层的上下文中进行的绘图操作。 333 | Quartz提供了类似的功能,成为透明层,这些层是您能够将多个绘制操作组合到单个缓冲区中,图6-11展示的是,为什么会希望在应用中使用图层。 334 | 335 | 图6-11 336 | 337 | 此图被渲染到启用阴影的上下文中。在上方的图片中,阴影出现在绘图的所有区域之上,包括“内部”的DogCow。这是因为图片是使用Bezier填充创作出来了: 338 | * 第一个操作填充了粉红色的奶狗的乳房。 339 | * 第二步,填充图中的白色背景 340 | * 第三步,画上了背景上的斑点,眼睛和轮廓 341 | 左下角的图像显示了用于这些绘制认为的贝塞尔曲线的轮廓。当这个路径作为三次操作执行时,上下文会对每个操作使用阴影。要创建一个如图6-11的右下角图所示的单个复合图形时,您需要使用Quartz透明图层来代替。阴影仅会出现在复合图形的边缘,而不是每个元件的边缘。独立于绘图上下文,透明图层将绘图请求分组到不同的缓冲区中。开启一个层(通过调用CGContextBeginTransparenceLayer()),缓冲区会初始化一个完全透明的背景。阴影被禁用,且全局的alpha值设置为1。只有在完成绘制后(通过调用CGContextEndTransparencyLayer())图层的内容才会呈现给父视图。 342 | ##### 透明度块 343 | 和大多数其他的Quartz和UIKit绘图请求一样,图层声明很快会变得凌乱:难以理解,难以阅读,难以维护。思考一下代码6-3展示了图6-11中创建最终DogCow的代码。传递给PushLayerDraw的块确保阴影是为整个图层进行绘制的。 344 | ``` 345 | SetShadow(shadowColor, CGSizeMake(4, 4), 4); 346 | PushLayerDraw(^{ 347 | [udder fill:pinkColor]; 348 | [interior fill:whiteColor]; 349 | [baseMoof fill:blackColor]; 350 | }); 351 | ``` 352 | 代码6-4展示了PushLayerDraw函数。他在一个透明层内执行了一个绘图块操作。这张总方法方便您对基于图层渲染的易用块进行分组。 353 | ``` 354 | typedef void (^DrawingStateBlock) (); 355 | 356 | void PushLayerDraw(DrawingStateBlock block) 357 | { 358 | if(!block) return;//Nothing to do 359 | 360 | CGContextRef context = UIGraphicsGetCurrentContext(); 361 | if (context == NULL) 362 | { 363 | NSLog(@"Error:No context to draw to"); 364 | return; 365 | } 366 | 367 | CGContextBeginTransparencyLayer(context, NULL); 368 | block(); 369 | CGContextEndTransparencyLayer(context); 370 | } 371 | ``` 372 | 透明层的优点是显而易见的:他可以使你的绘图操作成为一个整体。缺点是, 可能存在内存堵塞由于需要额外的绘图缓冲区。您可以通过使用图层前裁剪上下文来缓解这个问题。如果你知道的话,你的小组只会在开始层之前,只绘制到上下文的某一部分并添加裁剪。这迫使图层仅绘制在剪裁区域,从而减少缓冲区大小和相关的内存开销。 373 | 然而,阴影需要特别注意。阴影需要在透明层结束时马上进行绘制。根据经验,您可能会希望在阴影区域加上模糊,所以偏移为(2,4)的阴影加上4的模糊度,在剪辑区域至少要为(6, 8)。 374 | 375 | ##### 状态块 376 | 无论何时使用到临时剪辑或是基于上下文特定状态的操作,都可以通过使用块来完成,这会让事情变得更加简单。如代码6-5所示,类似于代码6-4所示,PushDraw()使用一个块来保存和恢复上下文的状态。 377 | ``` 378 | void PushDraw(DrawingStateBlock block) 379 | { 380 | if(!block) return; //Nothing to do 381 | 382 | CGContextRef context = UIGraphicsGetCurrentContext); 383 | if(context == NULL) 384 | { 385 | NSLog(@"Error: No context to draw to"); 386 | return; 387 | } 388 | CGContextSaveGState(context); 389 | block(); 390 | CGContextRestoreGState(context); 391 | } 392 | ``` 393 | 示例6-4使用代码6-4和6-5的函数来显示以创建6-11中的最终图像。他施行剪切和阴影,并将三条贝塞尔曲线绘制成一组。执行块后,上下文返回完全适应它的预拉伸效果。剪切和阴影的状态都不会改变。 394 | ``` 395 | CGRect clipRect = CGRectInset(destinationRect, -8, -8); 396 | PushDraw(^{ 397 | //Clip path to bounds union with shadow allowance 398 | //to improve drawing performance 399 | [[UIBezierPath bezierPathWithRect:clipRect] addClip]; 400 | 401 | //Set shadow 402 | SetShadow(shadowColor, CGSizeMake(4, 4), 4); 403 | 404 | //Draw as group 405 | PushLayerDraw(^{ 406 | [udder fill:pinkColor]; 407 | [interior fill:whiteColor]; 408 | [baseMoof fill:blackColor]; 409 | }); 410 | }); 411 | ``` 412 | 413 | ### 反转渐变 414 | 渐变模拟自然光。当倒置时,会形成一个视觉的空洞,这个区域是物理世界中缩紧以捕捉倒置的光图案的区域。首先渐变,然后插入,你会看到产生另一种效果。 415 | 416 | 图6-12 417 | 418 | 示例6-2在左侧显示了构建循环示例的代码。他构建了一个浅灰色到深灰色的渐变,首先以较大的形状从上到下绘制,然后反转其方向,使用较小的形状在另一个方向上再次绘制。 419 | 代码6-5在绘制轻微的黑色内部阴影时添加了一个修饰(参考第五章)在较小形状的顶部。这个阴影强调了两者之间的区别,当然这个效果时可选的。 420 | 421 | ``` 422 | UIBezierPath *outerPath = [UIBezierPath bezierPathWithOvalInRect:outerRect]; 423 | UIBezierPath *innerPath = [UIBezierPath bezierPathWithOvalInRect:innerRect]; 424 | 425 | Gradient *gradient = [Gradient gradientFrom:WHITE_LEVEL(0.66, 1) to:WHITE_LEVEL(0.33, 1)]; 426 | 427 | PushDraw(^{ 428 | [outerPath addClip]; 429 | [gradient drawTopToBottom:outerRect]; 430 | }); 431 | 432 | PushDraw(^{ 433 | [innerPath addClip]; 434 | [gradient drawBottomToTop:innerRect]; 435 | }); 436 | 437 | DrawInnerShadow(innerPath, WHITE_LEVEL(0.0, 0.5f), CGSizeMake(0, 2), 2); 438 | ``` 439 | 440 | ### 混合线性和放射的渐变 441 | 没理由你不能把线性渐变和放射效果渐变混合起来。例如,代码片段6-6中在6-5点基础上绘制蓝色放射渐变。这会产生令人赏心悦目的发光按钮效果,如图6-13所示: 442 | 443 | 图6-13 444 | 445 | ``` 446 | CGRect insetRect = CGRectInset(innerRect, 2, 2); 447 | UIBezierPath *bluePath = [UIBezierPath bezierPathWithOvalInRect:insetRect]; 448 | 449 | //Produce an ease-in-out gradient, as in Listing 6-5 450 | Gradient *blueGradient = [Gradient easeInOutGradientBetween:skyColor add:darkSkyColor]; 451 | 452 | //Draw the radial gradient 453 | CGPoint center = RectGetCenter(insetRect); 454 | CGPoint topright = RectGetTopRight(insetRect); 455 | CGFloat width = PointDistanceFromPoint(center, topright); 456 | 457 | PushDraw(^{ 458 | [bluePath addClip]; 459 | CGContextDrawRadialGradient(UIGraphicsGetCurrentContext(), 460 | blueGradinet.gradient, center, 0, center,width, 0); 461 | }) 462 | ``` 463 | ### 在路径边沿绘制渐变 464 | 465 | 经常有人问我,如何在路径的边沿进行绘制。经常被用于测试贝塞尔曲线上的触摸,但有时,开发者会想知道,如何仅在边缘绘制特殊效果。有一个奇怪的小的核心图像的函数,叫做CGPathCreateCopyByThrokingPath()。它构建了一个具有指定宽度的路径,形成在给定的贝塞尔曲线边沿的周围。 466 | 代码6-6使用这个函数在正常的笔画区域周围裁剪路径。你需要提供宽度;他构建并裁减边沿路径。一旦裁剪,你就可以在其中进行渐变的绘制。效果如图6-14所示,左图中,路径被填充渐变。右图中,还在原始路径上额外增加了虚线路径。 467 | 468 | 图6-14 469 | 470 | ``` 471 | - (void) clipToStroke:(NSInteger)width 472 | { 473 | CGPathRef pathRef = CGPathCreateCopyByStrokingPath(self.CGPath, NULL, width, kCGLineCapButt, kCGLineJoinMiter, 4); 474 | UIBezierPath *clipPath = [UIBezierPath bezierPathWithCGPath:pathRef]; 475 | CGPathRelease(pathRef); 476 | [clip addClip]; 477 | } 478 | ``` 479 | ### 绘制3D字母 480 | 代码6-7合并了你正在阅读的构建3D字母效果的方法,如图6-15所示。到目前为止,在本章,您已经阅读了关于透明层,渐变和裁剪路径边沿。单独来说,每一个都可以或无趣或丰满。但综合起来的话,你可以看到他们产生引人注目的结果。 481 | 482 | 图6-15 483 | 484 | 这幅由字母组成,字母由亮到暗渐变。路径被截断,渐变从下到下绘制。接下来,修剪到边缘的路径会添加一个反向路径,每一个字母周围从暗到亮渐变修剪。和透明层一起确保每个字母周围都创建一个阴影,投射到右下角。 485 | 代码6-7还在图中添加了一个内部阴影(见第五章),给了文本每个字母额外的一点形状。这会参数一个额外的高度在每个字母底部,与灰色渐变的轮廓相比。 486 | ``` 487 | #define COMPLAIN_AND_BAIL(_COMPLAINT_, _ARG_) \ 488 | {NSLog(_COMPLAINT_, _ARG_); return;} 489 | 490 | //Brightness scaling 491 | UIColor *ScaleColorBrightness(UIColor *color, CGFloat amount) 492 | { 493 | if(!color) return [UIColor blackColor]; 494 | 495 | CGFloat h, s, v, a; 496 | [Color getHue:&h saturation:&s brightness:&v alpha:&a]; 497 | CGFloat v1 = fmaxf(fminf(v * amount, 1), 0); 498 | return [UIColor colorWithHue:h saturation:s brightness:v1 alpha:a]; 499 | } 500 | 501 | void DrawStrokedShadowedShape(UIBezierPath *path,UIColor *baseColor, CGRect dest) 502 | { 503 | CGContextRef context = UIGraphicsGetCurrentContext(); 504 | if(!context) 505 | COMPLAIN_AND_BAIL(@"No context to draw to", nil); 506 | 507 | PushDraw(^{ 508 | CGContextSetShadow(context, CGSizeMake(4, 4), 4); 509 | 510 | PushLayerDraw(^{ 511 | //Draw letter gradient (to half brightness) 512 | PushDraw(^{ 513 | Gradient *innerGradinet = [Gradinet gradientFrom:baseColor to:ScaleColorBrightness(baseColor, 0.5)]; 514 | [path addClip]; 515 | [innerGradinet drawTopToBottom:path.bounds]; 516 | }); 517 | 518 | //Add the inner shadow with darker color 519 | PushDraw(^{ 520 | CGContextSetBlendMode(context, kCGBlendModeMultiply); 521 | DrawInnerShadow(path, ScaleColorBrightness(baseColor, 0.3), CGSizeMake(0, -2), 2); 522 | }); 523 | 524 | //Stroke with reversed gray gradient 525 | PushDraw(^{ 526 | [path clipToStroke:6]; 527 | [path.inverse addClip]; 528 | 529 | Gradient *grayGradient = [Gradient gradientFrom:WHITE_LEVEL(0.0, 1) to:WHITE_LEVEL(0.5, 1)]; 530 | [grayGradient drawTopToBottom:dest]; 531 | }); 532 | }); 533 | }); 534 | } 535 | ``` 536 | ### 创建缩进图形 537 | 代码6-8对渐变饮用了不同的扭变,如图6-16所示。这个功能使用暗到亮的渐变来产生缩进效果。一对儿阴影——顶部黑色内部阴影和底部的白色阴影——增加了这种错觉。结合渐变和斜面,它会欺骗你的眼睛,让你感觉看到了一个“切口”的形状。 538 | 539 | 图6-16 540 | 541 | ``` 542 | void DrawIndentedPath(UIBezierPath *path, UIColor *primary, CGRect rect) 543 | { 544 | CGContextRef context = UIGraphicsGetCurrentContext(); 545 | if(!context) COMPLAIN_AND_BAIL(@"No context to draw to", nil); 546 | 547 | //Draw the black inner shadow at the top 548 | PushDraw(^{ 549 | CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeMultiply); 550 | DrawInnerShadow(path, WHITE_LEVEL(0, 0.4), CGSizeMake(0, 2), 1); 551 | }); 552 | 553 | //Draw the while shadow at the bottom 554 | DrawShadow(path, WHITE_LEVEL(1, 0.5), CGSizeMake(0, 2), 1); 555 | 556 | //Create a bevel effect 557 | BevelPath(path, WHITE_LEVEL(0, 0.4), 2, 0); 558 | 559 | //Draw a gradient from light (bottom) to dark (top) 560 | PushDraw(^{ 561 | [path addClip]; 562 | CGContextSetAlpha(UIGraphicsGetCurrentContext(), 0.3); 563 | 564 | UIColor *secondary = ScaleColorBrightness(primary, 0.3); 565 | Gradient *gradient = [Gradient gradientFrom:primary to:secondary]; 566 | [gradient drawBottomToTop:path.bounds]; 567 | }); 568 | } 569 | ``` 570 | ### 结合渐变与纹理 571 | 纹理拓展了对象的着色方式,提供一些视觉效果上的着色细节。 572 | 以图6-17为例,在这些图像中,kCGBlendModeColor Quartz混合模式使您能够在背景图像上绘制渐变。此模式从目标上下文中提取图像纹理(亮度值),同时保留渐变颜色的色调和饱和度。 573 | 574 | 图6-17 575 | 576 | 上部分的两幅图显示了从亮(顶部)到略暗的紫色渐变(在底部,亮度降低了百分之25)。最上的图显示了原图像(右)和渐变叠加图(左)。底部图像应用了彩虹梯度。 577 | 578 | 代码6-9显示了用于创建这些图像的DrawGradineteverTexture()方法。 579 | 580 | ``` 581 | void DrawGradientOverTexture(UIBezierPath *path, UIImage *texture, Gradient *gradient, CGFloat alpha) 582 | { 583 | if(!path) COMPLAIN_AND_BAIL(@"Path cannot be nil", nil); 584 | if(!text) COMPLAIN_AND_BAIL(@"Texture cannot be nil", nil); 585 | if(!gradient) COMPLAIN_AND_BAIL(@"Gradient cannot be nil", nil); 586 | CGContextRef context = UIGraphicsGetCurrentContext(); 587 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 588 | 589 | CGRect rect = path.bounds; 590 | PushDraw(^{ 591 | CGContextSetAlpha(context, alpha); 592 | [path addClip]; 593 | PushLayerDraw(^{ 594 | [texture drawInRect:rect]; 595 | CGContextSetBlendMode(context, kCGBlendModeColor); 596 | [gradient drawTopToBottom:rect]; 597 | }); 598 | }); 599 | } 600 | ``` 601 | 602 | ### 添加噪声纹理 603 | 图6-17所示的,是使用kCGBlendModeColor混合模式来为纹理添加色调。为此,需要使用kCGBlendModeScreen混合模式。图6-18显示了这种情况。在顶部,你会看到一个纯紫色创建的普通填充。中间的图像应用了混合噪声模式,引入了微妙的纹理。底部图像缩放到最终的纹理中,以突出显示潜在的变化。 604 | 这种基于噪声的基础在AppStore中现在非常的流行,使用纹理化的界面颜色。尽管iOS7被誉为“扁平”UI,但在许多领域都是用了微妙的纹理在它的原始app中,比如笔记和纹理。但你可能需要十分仔细的观察才能看到这个粒度,但确实是存在的。噪声和其他纹理提供了更令人满意的背景。 605 | 606 | 图6-18 607 | 608 | 当然,屏幕混合不限于噪音。图6-19显示了混合点图案变成了填充的颜色 609 | 610 | 图6-19 611 | 612 | 代码6-10提供了两种贝塞尔曲线的方法,是你能够填充路径。第一个使用一个指定的混合模式的颜色。如图6-19所示。第二个填充带有颜色的路径,然后以噪音模式进行筛选,如图6-18所示,一个简单的噪音颜色生成器。 613 | 614 | > 注意:如果你对噪音的需求比较严格,请访问如下网站http://webstaff.itn.liu.se/~stegu/TNM022-2005/perlinnoiselinks/perlin-noise-math-faq.html。他提供了产生相干噪声内容的方法(即平滑变化的)。 615 | 616 | ``` 617 | //Apply color using the specified blend mode 618 | - (void) fill:(UIColor *)fillColor 619 | { 620 | CGContextRef context = UIGraphicsGetCurrentContext(); 621 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 622 | 623 | PushDraw(^{ 624 | CGContextSetBlendMode(context, blendMode); 625 | [self fill:fillColor]; 626 | }); 627 | } 628 | 629 | //Screen noise into the fill 630 | - (void) fillWithNoise:(UIColor *)fillColor 631 | { 632 | CGContextRef context = UIGraphicsGetCurrentContext(); 633 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 634 | 635 | [self fill:fillColor]; 636 | [self fill:[NoiseColor() colorWithAlphaComponent:0.05f] withMode:kCGBlendModeScreen]; 637 | } 638 | 639 | //Generate a noise pattern color 640 | UIColor *NoiseColor() 641 | { 642 | static UIImage *noise = nil; 643 | if(noise) 644 | return [UIColor colorWithPatternImage:noise]; 645 | 646 | srandom(time(0)); 647 | 648 | CGSize size = CGSizeMake(128, 128); 649 | UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); 650 | for(int j = 0; j < size.height; j++) 651 | for(int i = 0; i< size.height; j++) 652 | { 653 | UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(i, j, 1, 1)]; 654 | CGFloat level = ((double) random() / (double) LONG_MAX); 655 | [path fill:[UIColor colorWithWhite:level alpha:1]]; 656 | } 657 | noise = UIGraphicsGetImageFromCurrentImageContext(); 658 | UIGraphicsEndImageContext(); 659 | 660 | return [UIColor colorWithPatternImage:noise]; 661 | } 662 | ``` 663 | 664 | ### 按钮光泽基础 665 | 许多iOS开发人员将继续使用渐变来给按钮添加假的光泽,尽管iOS7已经来到了一个勇敢全新的扁平化时代。他们明白使用自定义按钮但不废除深度和区别的原则(注:???)。光泽创造了一种3D的感觉,可以应用于多种试图,而不光仅仅是按钮。他们在非系统创建的界面中发挥着重要的作用。如果你的应用主要基于苹果系统的控件,那就别继续使用3D效果了吧——使用无边框按钮和白色边对边设计吧。如果是自定义界面,你将在本节读到的一些技术会创造出一些丰富的效果替代方案。 666 | 简单的玻璃效果由一个沿着按钮一半绘制的线性渐变和其后跟着的一个断开的颜色序列,并继续绘制剩余的距离。图6-20显示了几种不同颜色的光泽效果的样式。 667 | 668 | 图6-20 669 | 670 | 有很多方法都可以创建这种高光渐变,所有方法都是几乎类似的主题。代码6-11是收到很早之前的一个Matt Gallagher所写的帖子的启发(http://www.cocoawithlove.com/)。我这里列出的代码可能有些混乱,但是这不是他的原因,而且输出的结果都是创意非凡的结果都是他的创意。 671 | 672 | ``` 673 | + (instancetype) linearGloss:(UIColor *)color 674 | { 675 | CGFloat r, g, b, a; 676 | [color getRed:&r green:&g blue:&b alpha:&a]; 677 | 678 | //Calculate top gloss as half the core color luminosity 679 | CGFloat l = (0.299f * r + 0.587f *g + 0.114f *b); 680 | CGFloat gloss = pow(1, 0.2) * 0.5; 681 | 682 | //Retrieve color values for the bottom gradient 683 | CGFloat h, s, v; 684 | [color getHue:&h saturation:&s brightness:&v alpha:NULL]; 685 | s = fminf(s, 0.2f); 686 | 687 | //Rotate the color wheel by 0.6 PI. Dark colors 688 | //move toward magenta, light ones toward yellow 689 | CGFloat rhue = ((h< 0.95) && (h > 0.7)) ? 0.67 : 0.17; 690 | CGFloat phi = rHue * M_PI * 2; 691 | CGFloat theta = h * M_PI; 692 | 693 | //Interpolate distance to the reference color 694 | CGFloat dTheta = (theta - phi); 695 | while (dTheta < 0) dTheta += M_PI_2; 696 | while (dTheta > 2 * M_PI) dTheta -= M_PI_2; 697 | CGFloat factor = 0.7 + 0.3 * cosf(dTheta); 698 | 699 | //Build highlight colors by interpolating between 700 | //the source color and the reference color 701 | UIColor *c1 = [UIColor colorWithHue:h * factor + (1 - factor) * rHue saturations:s brightness:v *factor + (1 - factor) alpha:gloss]; 702 | UIColor *c2 = [c1 colorWithAlphaComponent:0]; 703 | 704 | //Build and return the final gradient 705 | NSArray *colors = @[WHITE_LEVEL(1, gloss), WHITE_LEVEL(1, 0.2), c2, c1]; 706 | NSArray *locations = @[@(0.0), @(0.5), @(0.5), @(1)]; 707 | return [Gradient gradientWithColors:colors locations:locations]; 708 | } 709 | 710 | ``` 711 | ##### 修剪光泽 712 | 代码6-12提供了另一种常见的按钮光泽,在这种方法中(见图6-21),偏移路径轮廓,然后切掉一部分渐变叠加。结果是,覆盖的内容急剧转变。 713 | 714 | 图6-21 715 | 716 | 绘图和裁剪都在透明层中进行,这种方法确保了只有预定的覆盖层保留下来,而他的光照投射到原始的绘图上下文中。该方法在透明层完成之前,会清除多余的材料。 717 | 718 | ``` 719 | void DrawButtonGloss(UIBezierPath *path) 720 | { 721 | if(!path) COMPLAIN_AND_BAIL(@"Path cannot be nil", nil); 722 | CGContextRef context = UIGraphicsGetCurrentContext(); 723 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into",nil); 724 | 725 | //Create a simple white to clear gradient 726 | Gradient *gradient = [Gradient grandientFrom:WHITE_LEVEL(1, 1) to:WHITE_LEVEL(1, 0)]; 727 | 728 | //Copy and offset the path by 35% vertically 729 | UIBezierPath *offset = [UIBezierPath bezierPath]; 730 | [offset appendPath:path]; 731 | CGRect bounding = path.calculatedBounds; 732 | OffsetPath(offset, CGSizeMake(0, bounding.size.height * 0.35)); 733 | 734 | //Draw from just over the path to its middle 735 | CGPoint p1 = RectGetPointAtPercents(bounding, 0.5, -0.2); 736 | CGPoint p2 = RectGetPointAtPercents(bounding, 0.5, 0.5); 737 | 738 | PushLayerDraw(^{ 739 | PushDraw(^{ 740 | //Draw the overlay inside the path bounds 741 | [path addClip]; 742 | [gradient drawFrom:p1 toPoint:p2]; 743 | }); 744 | 745 | PushDraw({ 746 | //Add then clear away the offset area 747 | [offset addClip]; 748 | CGContextClearRect(context, bounding); 749 | }); 750 | }); 751 | } 752 | ``` 753 | 754 | ### 添加底部光 755 | 底部光会产生一种环境光反射回图像的错觉。图5-22显示了第五章的内外光例子添加和未添加底部光时的效果。 756 | 757 | 图6-22 758 | 759 | 不想第五种中的那种光泽,这个例子使用了一个至上而下的渐变。代码6-13创建了一个裁剪路径的函数,并且自上而下绘制渐变。您可以指定绘制的百分比。图6-22为百分之20白色沿着外圆角矩形路径向上延伸百分之40来构建底部发光的效果。 760 | 761 | ``` 762 | void DrawBottomGlow(UIBezierPath *path, UIColor *color, CGFloat percent) 763 | { 764 | if(!path) COMPLAIN_AND_BAIL(@"Path cannot be nil", nil); 765 | if(!color) COMPLAIN_AND_BAIL(@"Color cannot be nil", nil); 766 | CGContextRef context = UIGraphicsGetCurrentContext(); 767 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 768 | 769 | CGRect rect = path.calculatedBounds; 770 | CGPoint h1 = RectGetPointAtPercents(rect, 0.5f, 1.0f); 771 | CGPoint h2 = RectGetPointAtPercents(rect, 0.5f, 1.0f - percent); 772 | 773 | Gradient *gradient = [Gradient easeInOutGradientBetween:color and:[color colorWithAlphaComponent:0.0f]]; 774 | 775 | PushDraw(^{ 776 | [paht addClip]; 777 | [gradinet drawFrom:h1 toPoint:h2]; 778 | }); 779 | } 780 | ``` 781 | 782 | 783 | ### 创建椭圆渐变轮廓 784 | 通过椭圆路径渐变覆盖提供了另一种光效和深度效果的方法。这和你在6-11中读到的按钮特效类似。图2-23显示了这种光效的例子。 785 | 786 | 图6-23 787 | 788 | 底层算法其实很简单,如代码6-14所示:绘制一个与原路径等高的椭圆,向两边拓展他的宽度。然后向上移动,通常从顶部到边缘的0.45。绘制之前,将上下文裁剪到原始路径和椭圆的交点。用两个addClip操作来完成请求。然后,以绘制从透明到白色的渐变来完成特效。 789 | 790 | ``` 791 | void DrawIconTopLight(UIBezierPath *path, CGFloat p) 792 | { 793 | if(!path) COMPLAIN_AND_BAIL(@"Path cannot be nil", nil); 794 | CGContextRef context = UIGraphicsGetCurrentContext(); 795 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 796 | 797 | //Establish an ellipse and move it up to cover just 798 | //p percent of the parent path 799 | CGFloat percent = 1.0f - p; 800 | CGRect rect = path.bounds; 801 | CGRect offset = rect; 802 | offset.origin.y -= percent * offset.size.height; 803 | offset = CGRectInset(offset, -offset.size.width * 0.3f, 0); 804 | 805 | //Build an oval path 806 | UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:offset]; 807 | Gradient *gradient = [Gradient gradientFrom:WHITE_LEVEL(1, 0.0) to:WHITE_LEVEL(1, 0.5)]; 808 | 809 | PushDraw(^{ 810 | [path addClip]; 811 | [ovalPath addClip]; 812 | 813 | //Draw gradient 814 | CGPoint p1 = Rect 815 | }); 816 | } 817 | ``` 818 | 819 | ### 总结 820 | 本章介绍了渐变,展示了很多强大的方法来处理iOS绘图任务。下面是一些相关需要注意的地方: 821 | * 渐变为界面对象添加了一个“伪维度”。他们使你能够模拟光照和距离效果。你会在很多Photoshop的教程中发现,如何将3D效果构建到2D绘图中。我们鼓励你去看看这些方法,并试着用Quartz和UIKit来完成吧。 822 | * 本章使用线性和放射性渐变,有到有阴影,有的没有。始终需要注意平衡计算开销和视觉开销,来创建好看的特效。 823 | * iOS7不要求您一定采用“扁平设计”在你的UI里。当然,如果你的app主要围绕系统提供的控件,尝试将您的app与苹果的设计风格结合起来。如果不是的话,用您的想象力来导航,创建多彩的app吧~(注:最后一句有点太浮夸了。。。) 824 | -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-1.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-10.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-2.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-3.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-4.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-5.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-6.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-7.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-8.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/7-遮罩,模糊和动画/7-9.png -------------------------------------------------------------------------------- /7-遮罩,模糊和动画/7-遮罩,模糊和动画.md: -------------------------------------------------------------------------------- 1 | # 遮罩,模糊和动画 2 | 3 | 遮罩,模糊和动画是日常开发中经常会用到的东西。这些技术可以帮助你添加柔软的边界,景深效果,且可以随时间变化进行更新。本章介绍一些这些技术在iOS中的解决方案。 4 | 5 | ### 用块在图像中进行绘制 6 | 第六章介绍了自定义PushDraw()和PushLayerDraw()方法,他们将Quartz绘图和Objective-C结合起来,用于图形状态的管理和透明层。代码7-1同样使用了这个想法,介绍了一个返回图像的新函数。它使用同样的绘图状态块类型来传递一系列绘图块内操作,将他们写入新的图像绘制上下文。 7 | 虽然我最初构建这个函数是为了创建蒙版图像(如代码7-2所示),但我发现在很多情况下都可以使用它。例如,在构建图像视图的上下文时,在构建合成子视图时,在构建颜色选择器时,等等,都可以使用。在本章中,代码7-1将作为一个支持的角色,为你将阅读到的许多认为提供一个很好的起点。 8 | 9 | ``` 10 | UIImage *DrawIntoImage(CGSize size, DrawingStateBlock block) 11 | { 12 | UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); 13 | if(block) block(); 14 | UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 15 | UIGraphicsEndImageContext(); 16 | return image; 17 | } 18 | 19 | ``` 20 | ### 简单的遮罩 21 | 如您在前些章所见到的,裁剪允许你在某个路径区域内部进行绘制。图7-1展示了一个简单的遮罩裁剪的例子。在本例中,仅路径的部分绘制到了图像上下文中。 22 | 23 | 图7-1 24 | 25 | 你可以使用Quartz或UIKit方法来实现这一效果。比如说,你也许使用CGContextClip()来裁剪以处理上下文,或者使用addClip方法来处理UIBezierPath实例。我使用addClip()来创建图7-1。 26 | 代码7-1展示了创建图片的代码。他创建了路径,实现了裁剪,然后在上下文中绘制出图像。 27 | 28 | ``` 29 | //create the clipping path 30 | UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:inset]; 31 | UIBezierPath *inner = [UIBezierPath bezierPathWithOvalInRect:RectInsetByPercent(inset, 0.4)]; 32 | 33 | //The even-odd rule is essential here to establish 34 | //the "inside" of the donut 35 | path.usesEvenOddFillRule = YES; 36 | [path appendPath:inner]; 37 | 38 | //Apply the clip 39 | [path addClip]; 40 | 41 | //Draw the image 42 | UIImage *agate = [UIImage imageNamed:@"agate.jpg"]; 43 | [agate drawInRect:targetRect]; 44 | ``` 45 | ### 复杂的遮罩 46 | 图7-2中展示的遮罩效果远比基础的路径裁剪要复杂的多。每个灰度遮罩不仅表示了每个像素可以或不可以被绘制的位置,还确定了像素被绘制的程度。由于遮罩元素的范围从百到黑,他们的灰度级别描述了最终绘制时的程度。 47 | 48 | 图7-2 49 | 50 | 这些遮罩通过更新上下文状态来工作,使用CGContextClipToMask()函数将您提供的遮罩映射到当前上下文中的矩形: 51 | 52 | ``` 53 | void CGContextClipToMask( 54 | CGContextRef c, 55 | CGRect rect, 56 | CGImageRef mask 57 | ); 58 | ``` 59 | 对于复杂的绘图,请在GState的块中操作遮罩。你可以保存和恢复上下文的GState以临时应用遮罩。这可以让你恢复上下文到未使用遮罩的状态,以便于进一步的操作,如本例: 60 | 61 | ``` 62 | PushDraw({ 63 | ApplyMaskToContext(mask);//See Listing 7-2 64 | [image drawInRect:targetRect]; 65 | }); 66 | ``` 67 | 一个上下文的遮罩可以确定哪些像素点需要绘制,以及绘制的程度。遮罩中的黑色像素完全被遮挡,没有数据可以通过。白色像素允许传递的所有数据通过。介于纯白和纯黑的灰度决定了相应的绘制alpha值透明度。例如,中等灰度对应与百分之50的alpha值。要是用黑白遮罩,遮罩数据必须要有一个灰度数据源(这里有一个很简单的方法,将在本章稍后介绍)。 68 | 代码7-2展示了如何将灰度遮罩应用于当前上下文。该函数先把遮罩转换为没有alpha的灰度颜色空间(参见第三章)。计算上下文尺寸,以便遮罩在整个上下文中覆盖拉伸(参见第一章)。 69 | 为了应用遮罩,需要在Quartz的坐标系中,否则遮罩会上下颠倒。该函数翻转上下文,然后添加遮罩,再然后将上下文反转回UIKit坐标系以便于进一步的绘制。这“翻转-应用-反转”的过程,从上到下应用遮罩,就如同真实的画画一样。 70 | 如果你想找一些遮罩相关的创意,很简单,在网络中搜索“Photoshop masks”,将会返回大量的预见构建好的黑白遮罩,可以直接到iOS应用中使用。使用之前需要看看个人许可条款,不过你会发现很多遮罩都是公用的了。 71 | 72 | ``` 73 | void ApplyMaskToContext(UIImage *mask) 74 | { 75 | if(!mask) 76 | COMPLAIN_AND_BAIL(@"Mask image cannot be nil", nil); 77 | CGContextRef context = UIGraphicsGetCurrentContext(); 78 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to apply mask to", nil); 79 | 80 | //Ensure that mask is grayscale 81 | UIImage *gray = GrayscaleVersionOfImage(mask); 82 | 83 | //Determine the context size 84 | CGSize size = CGSizeMake(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context)); 85 | CGFloat scale = [UIScreen mainScreen].scale; 86 | CGSize contextSize = CGSizeMake(size.width / scale, size.height / scale); 87 | 88 | //Update the CState for masking 89 | FlipContextVertically(contextSize); 90 | CGContextVertically(contextSize)l 91 | CGContextClipToMask(context, SizeMakeRect(contextSize), gray.CGImage); 92 | FlipContextVertically(contextSize); 93 | } 94 | ``` 95 | ### 模糊 96 | 模糊是在计算量很大的情况下非常必要的绘画工具。它能够软化边界来创建遮罩和特效以达到一种伪深度的感觉。图7-3就是一个样例。被称为“bokeh”,指的是这种散焦元素的美感。模糊模仿了摄影镜头捕捉景深以创建多维展示的方式。 97 | 98 | 图7-3 99 | 100 | 虽然模糊是许多绘图算法的一部分,并且在iOS7的UI中运用广泛,但他的实现并不在核心图形货UIKit的API当中。截止本书编写为止,苹果没有发布定制的iOS7模糊API。苹果工程师推荐使用Core Image和Accelerate来为三方开发解决这一问题。代码7-3使用核心图形的方法,这是最初与iOS6一起推出的,这个实现非常简单,只需要几句代码即可。 101 | “可接受”是一个相对的术语。我们鼓励在开发绘制时去考虑GUI是否会过载。(记住这一点:绘图是线程安全的。)随着代码的写入,模糊操作是非常昂贵的。“Core Image”和“Accelerate”往往以相同的开销允许在设备上。Core Image是比较易读的。除了性能检测工具,您还可以在其中使用简单的计时检测代码。绘制前储存当前的日期,并检查绘制后经过的时间间隔。以下是一个例子: 102 | ``` 103 | NSDate *date = [NSDate date]; 104 | //Perform drawing task here 105 | NSLog(@"Elapsed time:%f",[[NSDate date] timeIntervalSinceDate:date]); 106 | ``` 107 | 请记住,大多数绘图都是线程安全的。尽可能不要在主线程使用模糊代码。储存绘制结果到内存或沙盒中以便重复使用。 108 | > 注意:高斯模糊的输出结果在所有部分都比以合适的模糊度进行模糊的图像要大。代码中的Crop模糊保存了原始尺寸 109 | ``` 110 | UIImage *GaussianBlurImage(UIImage *image, NSInteger radius) 111 | { 112 | if(!image) COMPLAIN_AND_BAIL_NIL(@"Mask cannot be nil", nil); 113 | 114 | //Create Core Image blur filter 115 | CIFilter *blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"]; 116 | [blurFilter setValue:@(radius) forKey:@"inputRadius"]; 117 | 118 | //Pass the source image as the input 119 | [blurFilter setValue:[CIImage imageWithCGImage:image.CGImage forKey:@"inputImage"]]; 120 | 121 | CIFilter *crop = [CIFilter filterWithName:@"CICrop"]; 122 | [crop setDefaults]; 123 | [crop setValue:blurFilter.outputImage forKey:@"inputImage"]; 124 | 125 | //Apply crop 126 | CGFloat scale = [[UIScreen mainScreen] scale]; 127 | CGFloat w = image.size.width * scale; 128 | CGFloat h = image.size.height * scale; 129 | CIVector *v = [CIVector vectorWithX:0 Y:0 Z:w W:h]; 130 | [crop setValue:v forKey:@"inputRectangle"]; 131 | 132 | CGImageRef cgImageRef = [[CIContext contextWithOptions:nil] createCGImage:crop.outputImage fromRect:crop.outputImage.extent]; 133 | 134 | //Render the cropped, blurred results 135 | UIGraphicsBeginImageContextWithOptions(image.size, NO, 0.0); 136 | 137 | //Flip for Quartz drawing 138 | FlipContextVertically(image.size); 139 | 140 | //Draw the image 141 | CGContextDrawImage(UIGraphicsGetCurrentContext(), SizeMakeRect(image.size), cgImageRef); 142 | 143 | //Retrieve the final image 144 | UIImage *blurred = UIGraphicsGetImageFromCurrentImageContext(); 145 | UIGraphicsEndImageContext(); 146 | 147 | return blurred; 148 | } 149 | ``` 150 | 151 | ##### 模糊绘图块 152 | 代码7-4再一次返回一个OC的块来封装一系列的绘图操作。在本例中,解决方案会模糊绘图操作,并将其绘制在上下文中。 153 | 为此,函数必须模拟一个透明层,而不能直接使用透明层,因为无法拦截材料,模糊,然后直接指向上下文。作为代替,该函数在一个新的图像中进行块绘制,使用DrawIntoImage()(代码7-1所示),进行模糊操作(如代码7-3),然后将结果绘制在当前上下文中。你可以在图7-3中看到代码7-4的结果。这张图的随机圆由两个绘图请求组成,第一个使用模糊块完成,而第二个不使用: 154 | ``` 155 | DrawAndBlur(4, ^{[self drawRandomCircles:20 withHue:targetColor into: targetRect];}); 156 | [self drawRandomCircles:20 withHue:targetColor into:targetRect]; 157 | ``` 158 | 159 | ``` 160 | //Return the current context size 161 | CGSize GetUIKitContextSize() 162 | { 163 | CGContextRef context = UIGraphicsGetCurrentContext(); 164 | if (context == NULL) return CGSizeZero; 165 | 166 | CGSize size = CGSizeMake(CGBitmapContextGetWidth(context),CGBitmapContextGetHeight(context)); 167 | CGFloat scale = [UIScreen mainScreen].scale; 168 | return CGSizeMake(size.width / scale, size.height / scale); 169 | } 170 | 171 | //Draw blurred block 172 | void DrawAndBlur(CGFLoat radius, DrawingStateBlock block) 173 | { 174 | if(!block) return;//Nothing to do 175 | 176 | CGContextRef context = UIGraphicsGetCurrentContext(); 177 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 178 | 179 | //Draw and blur the image 180 | UIImage *baseImage = DrawIntoImage(GetUIKitContextSize(), block); 181 | UIImage *blurred = GaussianBlurImage(baseIamge, radius); 182 | 183 | //Draw the results 184 | [blurred drawAtPoint:CGPointZero]; 185 | } 186 | 187 | ``` 188 | ##### 模糊遮罩 189 | 当你模糊遮罩时,会时边缘变得更加柔和。图7-4显示了一个圆角矩形路径和一个模糊过的圆角矩形路径(代码7-2)。在上方图片中,路径被填充但是没有模糊。下方图像,使用DrawAndBlur()请求软化填充路径的边缘。柔和的边缘使图像能够在屏幕上平滑地融合。这项技术也被称为羽化,在羽化中,边缘遮罩被软化以在绘制图形和背景之间创建平滑的效果。 190 | 191 | 图7-4 192 | 193 | ``` 194 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:inset cornerRadius:32]; 195 | 196 | UIImage *mask = DrawIntoImage(targetRect.size, ^{ 197 | FillRect(targetRect, [UIColor blackColor]); 198 | DrawAndBlur(8, ^{[path fill:[UIColor whiteColro]];});//blurred 199 | //[path fill:[UIColor whiteColor]]; //non-blurred 200 | }); 201 | 202 | ApplyMaskToContext(mask); 203 | [agate drawInRect:targetRect]; 204 | ``` 205 | ##### 模糊聚光灯 206 | 将“光”绘制到上下文中是模糊的另一个常见用例。你在其中的目标是照亮像素,选择性的添加颜色,混合光线而不模糊已经绘制到上下文的部分。图7-5显示了几种不同的样式,如你所见,他们的差别非常微妙。 207 | 208 | 图7-5 209 | 210 | 图7-5中的所有四个例子都是由一个圆形的路径组成,路径中充满了一种半透明的绿色光。除了右下角图形外,每个样本都应用了如下的模糊代码: 211 | ``` 212 | path = [UIBezierPath bezierPathWithOvalInRect:rect]; 213 | PushDraw(^{ 214 | CGContextSetBlendMode(UIGraphicsGetCurrentContext(),blendMode); 215 | DrawAndBlur(8, ^{[path fill:spotlightColor];}); 216 | }); 217 | ``` 218 | 左上角使用普通混合模式,右上角使用强光模式,左下角使用弱光模式。这里有几件事情需要注意: 219 | * 右上角的kCGBlendModeHardLight产生了一种细微变化的光线,将最简单的高光添加到原始背景上。 220 | * 左下角的kCGBlendModeSoftLight是漫反射最强的,具有更亮的高光显示。 221 | * 左上角的kCGBlendModeNormal 介于两者之间,光场的中心实际上与右下角的样本向匹配——没有模糊,也是使用普通的混合模式。 222 | 223 | ### 绘制倒影 224 | 当绘制倒影时,你需要绘制一个逐渐消失的倒置图像。图7-6展示了这种常见的技术。我添加了一个微小的垂直间距,以突出显示原始图像和反射图像的开始位置。大多数绘制反射的图像都使用这个间隙来模拟原图像与其下方反射表面之间的高程差。 225 | 226 | 图7-6 227 | 228 | 代码7-5展示了构建这个反转镜像的函数。有几个事情需要注意: 229 | * 上下文在反射开始的点垂直反转。这使得反射能够从底部开始,在灌木附近反向绘制,并越过熊的头。 230 | * 与之前的遮罩的例子不同,代码7-5使用矩形参数来限制遮罩和图像的绘制。这使得你可以把反射在更大的背景下绘制成矩形。 231 | * CGContextClipToMake()函数与在代码7-2相比略有不同。这个函数不是将灰度图像遮罩传递给第三个参数,而是传递带有alpha通道的普通RGB图像。当以这种方式使用时,图像充当alpha遮罩。图像的alpha水平决定了裁剪区域的哪些部分收到影响。在这个例子中,绘制的反转图像从上到下逐渐消失。 232 | 233 | ``` 234 | //Draw an image into the target rectangle 235 | //inverted and masked to a gradient 236 | void DrawGradientMaskedReflection(UIImage *image, CGRect rect) 237 | { 238 | CGContextRef context = UIGraphicsGetCurrenContext(); 239 | if(context == NULL) 240 | COMPLAIN_AND_BAIL(@"No context to draw into", nil); 241 | 242 | //Build gradient 243 | UIImage *gradient = GradientImage(rect.size, WHITELEVEL(1.0, 0.5), WHITELEVEL(1.0, 0.0)); 244 | 245 | PushDraw(^{ 246 | //Flip the context vertically with respect 247 | //to the origin of the target rectangle 248 | CGContextTranslateCTM(context, 0, rect.origin.y); 249 | FlipContextVertically(rect.size); 250 | CGContextTranslateCTM(context, 0, -rect.origin.y); 251 | 252 | //Add clipping and draw 253 | CGContextClipToMask(context, rect, gradient.CGImage); 254 | [image drawInRect:rect]; 255 | }); 256 | } 257 | ``` 258 | 259 | 虽然倒影提供了一个有趣的上下文剪切的例子,但是这一特征并不需要总是在应用程序中实现,这是因为CAReplicatorLayer类和图层蒙版也能实现这一效果,此外,他们还提供实时更新的功能——因此,如果视图内容辩护啊,反射也会发生变化。虽然这些这些都使用Quartz也能完成,但总是会出现一些原因让你不这么做。倒影提供了一个很好的规则示范。 260 | 当你专注于图像而不是视图时,你应该用Quartz绘制反射。倒影通常构成绘图序列的一部分,而不是最终的产品。 261 | 262 | ### 为绘图创建DisplayLink 263 | 264 | 视图可能需要随着时间的推移更改其内容。他们可能从一个图像过度到另一个,或提供一系列的更新来制定应用的状态。动画通过一个特殊的计时类来完成绘制。 265 | CADisplayLink类为视图动画提供了一个计时器对象。他设置了一个更新时钟与显示的刷新率同步。这允许你在时刻上重新绘制视图。你可以通过这个时钟来创建Quart为基础的动画效果,比如说移动的蚂蚁,或者以Core Image为基础来转换界面。DisplayLink是QuartzCore框架的一部分。创建这些计时器然后和Run loop关联。 266 | 虽然您可以使用NSTimer来实现类似的效果,但是DisplayLink可以让你不用去猜测理想的刷新间隔。此外,DisplayLink提供了更好的功能保证计时器的准确性(他会准时生效)。苹果公司在文档中写道: 267 | 268 | > 计时器触发的实际时间可能在约定触发之后。 269 | 270 | 代码片段7-3展示了如何创建DisplayLink,你需要使用一个常见的模式(NSRunLoopCommonModes)来得到最小的延迟。在本例中,target是一个视图,selector是setNeedsDisplay,一个系统提供的UIView方法。出发时,该target-selector会告诉系统把整个视图的边界标记为脏的。然后在下一个绘制周期中请求drawRect:重绘。DrawRect:方法使用Quartz和iOS绘图API手动在自定义视图中进行绘制。 271 | ``` 272 | CADisplayLink *link = [CADisplayLink displayLinkWithTarget:view selector:@selector(setNeedsDisplay)]; 273 | [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 274 | ``` 275 | 276 | DisplayLink的frame interval属性是控制其刷新率的。通常默认为1。值为1时,每次触发连接计时器时,Displaylink都会通知target。这会使得更新与显示器的刷新率相匹配。若要调整,需修改Displaylink的整形属性frameInterval。数值越高,刷新速度越慢。设置为2,更新为刷新率的一半,以此类推: 277 | ``` 278 | link.frameInterval = 2; 279 | ``` 280 | 假设一切运行良好,良好的系统以每秒60帧的速度运行(fps)。你可以使用Instruments的核心动画分析器来测试刷新率(详见图7-7),同时在运行的刷新间隔为1的app。这样你可以了解运行动画为你的应用带来了多少负担。如果刷新率降低到12fps或3fps或更低,你需要好好反思反思你是如何完成绘画任务的。 281 | 282 | 图7-7 283 | 284 | > Instruments在确保应用高效运行方面起着关键的作用。该实用程序对内存使用情况进行采样并进行监控。这使您能够识别和定位应用程序中的问题区域,是的在发布前应用可以高效运行。Instruments提供基于时间的图形性能图,显示应用程序在什么位置使用的资源最多。Instrument是围绕由Sun Microsystems开发的开源的DTrance包构建的。在Xcode5中,Debug Navigator使你能够当应用运行时跟踪CPU和内存的负载。 285 | 286 | 当你完成显示循环后,使displaylink失效(使用invalidate)。这会将其从runloop中删除,并且取消target/action操作的关联。 287 | ``` 288 | [link invalidate]; 289 | ``` 290 | 或者,你可以把link的paused属性设置为YES,暂停display link,直到他需要再次被启用。 291 | 292 | ### 创建移动的蚂蚁 293 | 294 | 你刚刚阅读到的display link技术也适用于定期更新绘图。你可以使用这种方法用于多种动画效果。比如说,图7-8显示了一种常见的“移动的蚂蚁”样式。在这个界面中,浅灰色的线条在矩形周围运动。该效果是由Bill Atkinson为MacPaint设计的一种老式的Macintosh line风格,他是以蚂蚁排成一行的想法命名的。这样可以让用户很容易就看到区域的边缘。 295 | 296 | 图7-8 297 | 298 | 代码片段7-4展示了DrawRect:绘制移动的蚂蚁效果的实现。他计算与现实时间向关联的线段的偏移,然后应用于路径。尽管图7-8使用的是矩形,但是你可以把它运用在任何形状的路径上。 299 | 该方法是为了提供一个清晰,轻量的接口。这可以让你外观设置和GUI分割开,您可以轻松的选择隐藏或移除视图。 300 | 这里使用的是12点-3点的长线段短空隙。更重要是,它使用系统的时间,而不是任何特定计时器来建立的动画偏移。这确保了displaylink中的任何故障(尤其是高开销所引起的故障)不会影响每次刷新时的位置,动画将以您的制定的速度运行。 301 | 这里同时存在两个计时因素。首先是刷新率,它控制多久请求一次drawRect:方法来得到视觉更新。第二个控制模式偏移,它指定了线段的移动成程度,并独立于系统时间进行计算。 302 | 为了制作动画,代码7-4计算一个相位。这是UIBezierPath类(更确切的说,是底层的Quartz CGPath)用来表示虚线的偏移。相位可以是正的(逆时针方向移动)或负的(顺时针方向移动),并指定要开始绘制图案的深度,本例为15点长,每隔15点,线段就会回到原来的位置。 303 | 为了计算偏移,该方法使用了一个secondsPerFrame。代码7-4每四分之三秒循环一次。你可以调整以降低或增加图案的速度。 304 | 305 | ``` 306 | // The drawRect: method is called each time the display 307 | //link fires. That`s because calling setNeedsDisplay 308 | //on the view notifies the system that the view contents 309 | //need redrawing on the next drawing cycle. 310 | 311 | - (void) drawRect:(CGRect)rect 312 | { 313 | CGContextRef context = UIGraphicsGetCurrentContext(); 314 | CGContextClearRect(context, rect); 315 | 316 | CGFloat dashes[] = {12, 3}; 317 | CGFloat distance = 15; 318 | CGFloat secondsPerFrame = 0.75f; // Adjust as desired 319 | 320 | NSTimeInterval ti = [NSDate timeIntervalSinceReferenceDate] / secondsPerFrame; 321 | 322 | BOOL goesCW = YES; 323 | CGFLoat phase = distance * (ti - floor(ti)) * (goesCW ? -1 : 1); 324 | [path setLineDash:dashes count:2 phase:phase]; 325 | [path stroke:3 color:WHITE_LEVEL(0.75, 1)]; 326 | } 327 | ``` 328 | 329 | ### 绘制采样数据 330 | 331 | 有许多应用程序用于将iOS绘图和displaylink相结合。最实用的方法之一是从机载传感器采集数据。图7-9显示了一个监控音频级别的应用程序。在每次link回调时,他都会绘制一个UIBezierPath展示最近的100个样本。 332 | 333 | 图7-9 334 | 335 | 当然,你可以使用静态的背景,只需要在网格上绘制数据即可,如果你要使用一些垂直的绘图元素(如7-9中的蓝色虚线),你可能会希望这些元素随着你的数据更新一起变动。简单的方法是创建一个垂直网格,并在数据更新后移动到偏移副本处。 336 | 这里有一段解决这一问题的代码: 337 | 338 | ``` 339 | PushDraw(^{ 340 | UIBezierPath *vPath = [vGrid safeCopy]; 341 | offsetPath(vPath, CGSizeMake(-offset * deltaX, 0)); 342 | AddDashesToPath(vPath); 343 | [vPath stroke:1 color:blueColor]; 344 | }); 345 | ``` 346 | 在该片段中,垂直路径通过X位置的一些负变化来偏移自己,从重复此过程会生成岁时间向左移动的图形。 347 | 348 | ### 应用核心图像转换 349 | 350 | Core Image trasitions是另一种很有用的“计时器与绘制结合”的解决方案。他可以让您创建原图和目标图之间的序列,以便于生动地从一个过渡到另一个视觉效果。 351 | 首先创建一个新的transition filter,例如复印件风格的过渡: 352 | 353 | ``` 354 | transition = [CIFilter filterWithName:@"CICopyMachineTransition"]; 355 | ``` 356 | 357 | 您需要提供输入图像和目标图像,并指定从0.0到1.0的进度上transition的变化。代码7-6定义了一个演示沿着时间轴差值的CIImage的效果。 358 | 359 | ``` 360 | - (CIImage *)imageForTransitionCopyMachine:(float) t 361 | { 362 | CIFilter *crop; 363 | if(!transition) 364 | { 365 | transition = [CIFilter filterWithName:@"CICopyMachineTransition"]; 366 | [transition setDefaults]; 367 | } 368 | 369 | [transition setValue:self.inputImage forKey:@"inputImage"]; 370 | [transition setValue:self.targetImage forKey:@"inputTragetImage"]; 371 | [transition setValue:@(fmodf(t, 1.0f)) forKey:@"inputTime"]; 372 | 373 | //This next bit crops the image to the desired size 374 | CIFilter *crop = [CIFilter filterWithName:@"CICrop"]; 375 | [crop setDefaults]; 376 | [crop setValue:transition.outputImage forKey:@"inputImage"]; 377 | CIVector *v = [CIVector vectorWithX:0 Y:0 Z:_i1.size.width W:_i1.size.width]; 378 | [crop setValue:v forKey:@"inputRectangle"]; 379 | return [crop valueForKey:@"outputImage"]; 380 | } 381 | ``` 382 | 每个CoreImage filter都使用一组自定义的参数,这些参数记录在苹果的Core Image Filter引用中。copy machine sequence是最简单的转换之一。如代码7-6所示,它仅仅输入了两个图像和一个输入时间。你可以在图7-10中看到图像的转变。 383 | 384 | 图7-10 385 | 386 | display link可以启动转换。与7-4不同,代码7-7不实用真实世界的计时——尽管你可以改成这种方法。相反,他跟踪一个进度变量,每次display link递增时开启,移动百分之5(进度 += 1.0f/20.0f)。link触发时,转换方法更新进度并请求重绘。代码7-7中的DrawRect:方法把中间图像加入filter,然后绘制在上下文中。当进度为100%时,display link自动失效。 387 | 388 | ``` 389 | //Begin a new transition 390 | - (void) transition: (int) theType bbis:(NSArray *) items 391 | { 392 | //Disable the GUI 393 | for(UIBarButtonItem *item in (bbitems = items)) 394 | { 395 | item.enabled = NO; 396 | } 397 | 398 | //Reset the current CATransition 399 | transition = nil; 400 | transitionType = theType; 401 | 402 | //Start the transition from zero 403 | progress = 0.0; 404 | link = [CADisplayLink displayLinkWithTarget:self selector:@selector(applyTransition)]; 405 | [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 406 | } 407 | 408 | //This method runs each time the display link fires 409 | - (void)applyTransition 410 | { 411 | progress += 1.0f/20.0f; 412 | [self setNeedsDisplay]; 413 | 414 | if(progress > 1.0f) 415 | { 416 | //Our work here is done 417 | [link invalidate]; 418 | 419 | //Toggle the two images 420 | useSecond = ! useSecond; 421 | 422 | //Re-enable the GUI 423 | for(UIBarButtonItem *item in bbitems) 424 | { 425 | item.enabled = YES; 426 | } 427 | } 428 | } 429 | 430 | //Update the presentation 431 | - (void) drawRect:(CGRect) rect 432 | { 433 | //Fit the results 434 | CGRect r = SizeMakeRect(-i1.size); 435 | CGRect fitRect = RectByFittingRect(r, self.bounds); 436 | 437 | //Retrieve the current progress 438 | CIImage *image = [self imageForTransition:progress]; 439 | 440 | //Draw it(it`s a CIImage, not a CGImage) 441 | if(!cicontext) cicontext = [CIContext contextWithOptions:nil]; 442 | CGImageRef imageRef = [cicontext createCGImage:image fromRect:image.extent]; 443 | 444 | FlipContextVertically(self.bounds.size); 445 | CGContextDrawImage(UIGraphicsGetCurrentContext(),fitRect,imageRef); 446 | } 447 | ``` 448 | 449 | ### 总结 450 | 本章讨论遮罩,模糊和动态绘制上下文。我们阅读到了一些边沿选择,软化效果,使用核心图像转换来完成drawRect代码。这里有一些需要注意的: 451 | 452 | * 不管你想要绘制什么,分析app的性能都是至关重要的。总是预留出时间评估和调整渲染及动画任务。如果你发现在应用中处理时间花费太长,请考虑一些解决方案,比如绘图线程化(UIKit和Quartz在绘制上下文时是线程安全的)以及预先绘制并转换为图像(对于按钮添加预先绘制好的玻璃效果这种类型的效果)。 453 | * 核心图像转换非常有趣,一点点小的变化会发生非常不同的效果。不要多度地在你的app中使用华丽的特效。应用程序始终是为用户服务的,而不需要一些不必要的关注点。在UI中,少往往意味着多。 454 | * 将动画绘制到其他屏幕显示设备时(不论是通过AirPlay还是通过外接线),确保更新display link,就像你在主设备屏幕上使用那样。 455 | * 虽然iOS7使用模糊作为UI的一个重要元素,但在本书编写时,苹果还没有正真公开API。苹果的工程师建议你使用自己的模糊解决方案并缓存结果,特别是用于静态背景时。 456 | * Core Image不仅仅是关于过渡transition的,他提供了许多图像处理和图像类型选择,你会发现他们很有用。越来越多的Filter在iOS中出现。如果你有时间,这一部分也是很值得学习的。 457 | 458 | -------------------------------------------------------------------------------- /8-绘制文本/8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-1.png -------------------------------------------------------------------------------- /8-绘制文本/8-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-10.png -------------------------------------------------------------------------------- /8-绘制文本/8-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-11.png -------------------------------------------------------------------------------- /8-绘制文本/8-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-12.png -------------------------------------------------------------------------------- /8-绘制文本/8-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-13.png -------------------------------------------------------------------------------- /8-绘制文本/8-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-14.png -------------------------------------------------------------------------------- /8-绘制文本/8-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-15.png -------------------------------------------------------------------------------- /8-绘制文本/8-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-16.png -------------------------------------------------------------------------------- /8-绘制文本/8-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-17.png -------------------------------------------------------------------------------- /8-绘制文本/8-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-18.png -------------------------------------------------------------------------------- /8-绘制文本/8-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-19.png -------------------------------------------------------------------------------- /8-绘制文本/8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-2.png -------------------------------------------------------------------------------- /8-绘制文本/8-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-20.png -------------------------------------------------------------------------------- /8-绘制文本/8-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-21.png -------------------------------------------------------------------------------- /8-绘制文本/8-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-22.png -------------------------------------------------------------------------------- /8-绘制文本/8-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-23.png -------------------------------------------------------------------------------- /8-绘制文本/8-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-24.png -------------------------------------------------------------------------------- /8-绘制文本/8-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-25.png -------------------------------------------------------------------------------- /8-绘制文本/8-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-26.png -------------------------------------------------------------------------------- /8-绘制文本/8-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-27.png -------------------------------------------------------------------------------- /8-绘制文本/8-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-28.png -------------------------------------------------------------------------------- /8-绘制文本/8-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-29.png -------------------------------------------------------------------------------- /8-绘制文本/8-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-3.png -------------------------------------------------------------------------------- /8-绘制文本/8-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-30.png -------------------------------------------------------------------------------- /8-绘制文本/8-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-4.png -------------------------------------------------------------------------------- /8-绘制文本/8-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-5.png -------------------------------------------------------------------------------- /8-绘制文本/8-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-6.png -------------------------------------------------------------------------------- /8-绘制文本/8-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-7.png -------------------------------------------------------------------------------- /8-绘制文本/8-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-8.png -------------------------------------------------------------------------------- /8-绘制文本/8-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/a59cfd87713bc143b9c4ac038ec98ebc639913f1/8-绘制文本/8-9.png -------------------------------------------------------------------------------- /8-绘制文本/8-绘制文本.md: -------------------------------------------------------------------------------- 1 | # 绘制文本 2 | 在本书的前几章,你已经看过一些处理字符串的内容,只是这些内容并不是那些章节的重点。 3 | 在本章,你会发现字符串远不止如此,在绘图上下文中,选择一个点来绘制文本或将字符串转换为贝塞尔曲线。本章将深入讲解这些内容,涵盖:绘制,测量,布局。 4 | 5 | ### 绘制字符串 6 | 在iOS中,使用label和text view来显示字符串,然而,当你的文本作为一个更大的布局或绘图的一部分时,这时候就需要直接对字符串进行绘制了。这张做可以解锁一个更广泛多样和强大的文本布局工具,使你能够将字符串元素添加到上下文中。 7 | 在光谱的最简单的一端,你可以通过发送字符串绘制请求来完成文本绘制。DrawAtPoint:withAttributes:方法可以将任何字符串绘制在当前的上下文中。代码8-1演示了这种方法,指定了字体和颜色。 8 | 9 | ``` 10 | NSString *string = @"Hello World"; 11 | UIFont *font = [UIFont fontWithName:@"Futura" size:36.0f]; 12 | 13 | //Starting in iOS 7, all string drawing uses attributes 14 | NSDictionary *attributes = @{NSFontAttributeName:Font, NSForegroundColorAttributeName:[UIColor grayColor]}; 15 | 16 | //Draw the string 17 | [string drawAtPoint:drawingPoint withAttributes:attributes]; 18 | ``` 19 | 系统使用UIKit几何(从上到下)绘制字符串。图8-1显示了代码8-1的输出:用灰色绘制的大字母文本 20 | 21 | 图8-1 22 | 23 | ##### Point vs Rect 方法 24 | 字符串绘制提供两种API调用:点和矩阵。两者都适用于NSString和NSAttributedString类。经验是: 25 | * 像代码8-1提供的点方法,绘制一行——不管任何你定义的换行属性。渲染区域的宽度被视为无限大。 26 | * rect版本的绘制,必须绘制在你指定的边界内。任何超出边界的部分都会被裁剪。 27 | 28 | ### iOS7的变化 29 | 代码8-1中所使用的DrawAtPoint:withAttributes:方法属于iOS7的新方法。较早的字符串绘制方法,如drawAtPoint:withFont:,已被弃用(如图8-2)。新的技术,如布局管理器,动态文本等,在这个领域带来了一场革命。但是正如你将看到的,并非所有的方法都可以直接绘制上下文。 30 | 31 | 图8-2 32 | 33 | ##### 动态文本 34 | 动态文本是一种自动调整字体家族来填充用户界面角色的技术。例如,“headline”字体在屏幕上介绍材料部分。对于有视力障碍的用户,该字体可能比同一视力良好的“headline”大得多。当用户调整尺寸偏好时,字体角色也会改变——同时改变大小和整体重量以保证文本来可读范围内简单缩放。 35 | 这些新功能并不能与UIKit绘图相匹配。绘图创建的是静态图像。用于绘制每个上下文的尺寸会影响字体相对于设备的总尺寸。当绘图上下文与屏幕并非完美契合时,字体可能会被拉伸,压扁,或者有别于理想的尺寸。 36 | 此外,绘制元素不能自动更新为动态文本通知。当用户调整文本设置时,都会生成这些通知。如果元素需要适应动态文本,避免绘制他们。而是使用label或textview来代替。 37 | 当你要制作pdf,应用艺术或其他图像输出素材时,避免使用动态文本。使用特定的字体和尺寸。这就是本章在实例中应用精确页面和尺寸的原因。 38 | 39 | ##### Text Kit 40 | Text Kit是iOS7另一个令人兴奋的发展,它允许使用Core Text风格的文本样式排版。Core Text是苹果基于C语言的从代码中创建灵活而强大的排版的解决方案。Text Kit,在CoreText的顶部创建,将该功能拓展到了UIKit视图中。 41 | 你也可以使用Text Kit来绘制UIKit的上下文。如图8-3所示,当你想做一些有挑战性的尝试时,比如在某个形状中绘制或者竖着绘制在预设的UITextView目标上,目标可能会出问题。TextKit依旧非常新,年轻,有些地方可能会有bug。 42 | 43 | 图8-3 44 | 45 | 本章讨论Core Text解决方案而不是Text Kit。尽管他是以C语言为基础的API,Core Text依然是复杂文本很有效切且可靠的解决方案。 46 | 47 | ### 文本属性 48 | 49 | iOS设置的大部分内容都是理解属性。属性是应用于特定范围内的文本的特征的集合,如字体选择或者文本颜色。属性文本,如其名所示,为选中的子字符串添加特征。每个属性字符串包含了应用于该字符串的原文本和特定的范围属性。为了明了属性是如何工作和组合的。这里展示了图8-4,它显示了字符串前景颜色和阴影属性。 50 | 51 | 图8-4 52 | 53 | ##### 创建属性字符串 54 | 当创建具有排版特征的文本时,您需要使用NSAttributedString类,更多的时候,使用它的表亲NSMuatableAttributedString,可变版本往往更灵活,允许单独分层属性,而不必一次添加所有特征。 55 | 要创建一个不可更改的属性字符串,你可以用一个文本和属性字典来初始化,代码8-2绘制了你在图8-1中看到的同样的灰色Hello World。但这次它使用的是属性字符串,而不是NSString实例。drawAtPoint:方法从字符串中获得所有的信息,如字体颜色,字体。 56 | 57 | ``` 58 | //Create an attributes dictionary 59 | NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; 60 | 61 | //Set the font 62 | attributes[NSFontAttributeName] = [UIFont fontWithName:@"futura" size:36.0f]; 63 | 64 | //Set the foreground color 65 | attributes[NSForegroundColorAttributeName] = [UIColor grayColor]; 66 | 67 | //Build an attributed string with the dictionary 68 | attributedString = [[NSAttributedString alloc] initWithString:@"Hello World" attributes:attributes]; 69 | 70 | //Draw the attributed string 71 | [attributedString drawAtPoint:drawingPoint]; 72 | ``` 73 | 74 | ##### 可变属性字符串 75 | 可变的属性字符串能让你单独添加每个属性到整个字符串中(如代码8-3所示)或某一个子范围(如图8-4所示)。使用addAttribute:value:range:请求定义属性,范围和值。 76 | 其他的方法可以满足你通过字典来设置属性,就像不可变方法那样的(setAttributes:range:),或删除属性(removeAttributes:range)。也可以插入和附加属性字符串(insertAttributedString:atIndex:和 appendAttributedString:)来创建复杂的实例。 77 | 最后,代码8-3绘制了和图8-1同样的灰色的“hello world”输出,使用和8-2一样的drawAtPoint:入口。 78 | 79 | ``` 80 | //Build mutable attributed string 81 | attributedString = [[NSMutableAttributedString alloc] initWithString:@"Hello World"]; 82 | 83 | //Set the range for adding attributes 84 | NSRange r = NSMakeRange(0, attributedString.length); 85 | 86 | //Set font 87 | [attributedString addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Futura" size:36.0f] range:r]; 88 | 89 | //Set the Color 90 | [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor grayColor] range:r]; 91 | 92 | //Draw the attributed string 93 | [attributedString drawAtPoint: inset.origin]; 94 | ``` 95 | 96 | ### 属性种类 97 | iOS排版属性定义了文本在上下文中的绘制和样式。接下来的几节列举了你可能会用到的属性和他们的赋值。 98 | 99 | ##### 字体Fonts 100 | 101 | > Attribute: NSFontAttributeName 102 | 103 | 申请一个UIFont对象来设置文本的字体。代码8-2和8-3中把该属性设置为36大小的“Futura”属性。图8-5展示了多种字体(Chalkboard,Helvetica,Times New Roman)。 104 | 105 | 图8-5 106 | 107 | ##### 文本颜色 108 | 109 | > Attributes: NSForegroundColorAttributeName 和 NSBackgroundColorAttributeName 110 | 111 | UIColor对象设置文本的颜色和文本的背景色。图8-6显示了在紫色背景上用绿色前景色绘制的文本。 112 | 113 | 图8-6 114 | 115 | > Attribute:NSStrokeColorAttributeName 116 | 117 | 用一个UIColor指定stroke的颜色。但在很多区域还是和前景色相同,当你在指定画笔宽度属性后才会生效。仅当使用负数笔画宽度时,才会与前景色有区别。下一节会讲到。 118 | 119 | ##### 笔画样式 120 | 121 | > Attribute:NSStrokeWidthAttributeName 122 | 123 | 设置一个NSNumber对象储存一个定义线宽的浮点值,作为字体点尺寸的百分比。比如图8-7,你可以看到分别为线宽(1,4,8)的实现。 124 | 125 | 图8-7 126 | 127 | 负数会同时描边(使用stroke color)和填充(使用foreground color)文本,正数会创建一个“空心”的样式,仅在字符图像的边缘进行描画。 128 | 129 | 图8-8 130 | 131 | ##### 删除线 132 | 133 | > Attribute: NSStrikethoughStyleAttributeName 134 | 135 | 此关键字定义了项目是否使用删除线。使用0表示不使用删除线,1表示使用。 136 | 137 | > Attribute:NSStrikethroughColorAttributeName 138 | 139 | 为该属性赋值一个颜色,以指定删除线的颜色。 140 | 141 | 删除线是一种排印惯例,在文本中添加水平直线,表示材料已经被编辑掉了。图8-9显示了删除线,突出显示了iOS7中指定删除线颜色的新属性。 142 | 143 | 图8-9 144 | 145 | ##### 下划线 146 | 147 | > Attribute: NSUnderlineStyleAttributeName 148 | 149 | iOS7引入了各种新的下划线样式。包括单线,双线,粗线,虚线,点线,逐字线。你可以使用一个NSNumber来确定下划线的样式选项。 150 | 属性字符串的下划线属性(NSUnderlineStyleAttributeName)提供四个基础的风格。分别是NSUnderlineStyleNone(0,基础的没有下划线),NSUnderlineStyleSingle(1),NSUnderlineStyleThick(2),NSUnderlineStyleDouble(9)。如图8-10所示。 151 | 152 | 图8-10 153 | 154 | 除了基本的样式以外,您可能还会想添加其他的下划线样式。你还可以选择以下几种,实心(默认情况,NSUnderlinePatternDot),点(NSUnderlinePatternDot),线段(NSUnderlinePatternDash),线段-点(NSUnderlinePatternDashDot),以及线段-点-点(NSUnderlinePatternDashDotDot)。将这些选项与之前提到的基础选项一起使用,如下: 155 | 156 | ``` 157 | attributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleThick|NSUnderlinePatternDash); 158 | ``` 159 | 160 | 图8-11显示了这些模式使用后的样子,他们都是和单个下划线样式一起使用的。 161 | 162 | 图8-11 163 | 164 | 最后一个选项是NSUnderlineByWord,当你输入它为属性选项时,他会分别为每个单词加入下划线。如图8-12所示。 165 | 166 | 图8-12 167 | 168 | > Attribute:NSUnderlineColorAttributeName 169 | 170 | 传入一个颜色实例,为下划线设置颜色,如图8-13所示。 171 | 172 | 图8-13 173 | 174 | ##### 阴影 175 | 176 | > Attribute:NSShadowAttributeName 177 | 178 | 传入一个NSShadow对象,这个类可以设置阴影的颜色,偏移和模糊半径,参考图8-14,阴影的设置和在上下文中设置是一样的。你需要提供阴影的大小,模糊半径,阴影颜色的UIColor: 179 | 180 | ``` 181 | NSShadow *shadow = [[NSShadow alloc] init]; 182 | shadow.shadowBlurRadius = 2.0f; 183 | shadow.shadowOffset = CGSizeMake(2.0f, 2.0f); 184 | shadow.shadowColor = [UIColor grayColor]; 185 | attributes[NSShadowAttributeName] = shadow; 186 | ``` 187 | 188 | 图8-14 189 | 190 | > 注意:在OS X上,使用NSShadow实例,将其应用在上下文中,该功能呢尚未迁移到iOS中。 191 | 192 | ##### 基线 193 | 194 | > Attribute: NSBaselineOffsetAttributeName 195 | 196 | 该属性需要设置一个NSNumber,添加与普通文本之间的偏移位置,如图8-15所示,可以将其用于一些需要垂直放置的元素,比如说上标和下标。 197 | 198 | ``` 199 | [string addAttribute:NSBaselineOffsetAttributeName value:@(20) range:NSMakeRange(6, 5)]; 200 | ``` 201 | 图8-15 202 | 203 | ##### 文本特效 204 | 205 | > Attribute: NSTextEffectAttributeName 206 | 207 | 通过预设的NSString来设置特效。 208 | iOS7引入了一种新的文本属性,将其应用于字体。他只有一个选项,“letterpress”效果(NSTextEffectLetterpressStyle)。这个属性创建了一个轻微3D的文本效果,如图8-16所示: 209 | 210 | ``` 211 | attributes[NSTextEffectAttributeName] = NSTextEffectLettterPrestyle; 212 | ``` 213 | 这个图使用黑色背景来展示两个例子之间的区别。浅色背景下,凹凸特效可能很难识别出来。上图没有添加特效,下图添加了。其他的文本特效可能会在之后的iOS更新中添加。 214 | 215 | 图8-16 216 | 217 | ##### 倾斜和膨胀。 218 | 219 | > Attribute: NSObliqueneseAttributeName 220 | 221 | 设置一个@(-1.0)到@(1.0)的NSNumber值。 222 | iOS7新增了NSObliquenessAttributeName属性,以添加倾斜的文本。您可以选择从-1到1的斜率。图8-17显示了一个未倾斜和两个倾斜的文本例子。 223 | 224 | 图8-17 225 | 226 | > Attribute:NSExpansionAttributeName 227 | 228 | 设置一个大于@(0.0)的NSNumber值。 229 | 该属性也是iOS7引入的新属性,可以在水平方向上拉伸文本,如图8-18所示,为膨胀后的结果。 230 | 231 | 图8-18 232 | 233 | ##### 连字和字距微调 234 | 235 | > Attribute: NSLigatureAttributeName 236 | 237 | 该属性可以设置为0或1(默认),分别表示“不使用连字”和“使用连字”。 238 | 连字是指单个字形(字符图片)可以绑定来一起的方式,例如“f”和“i”,如图8-19所示,启用后,iOS会将单独的字母替换为特定序列的单一组合图像。常见为一个字母延伸到另一个字母所在的空间中。常见的英文连字包括fi,fj,fl,ff,ffi和ffl。具体实现根据字体的不同也有区别。 239 | 240 | 图8-19 241 | 242 | > Attribute: NSKernAttirbuteName 243 | 244 | 该属性可以设置为0或1(默认),分别表示“禁用字距微调”和“启用字距微调” 245 | 字距微调允许人工调整字母间的距离,是他们自然重叠,例如,将大写的A放到大写的V旁边时,图8-20,显示了微调的效果。 246 | 247 | 图8-20 248 | 249 | ##### 段落样式 250 | 251 | > Attribute: NSParagraphStyleAttributeName 252 | 253 | NSParagraphStyle对象用于指定许多有关段落的设置,比如对齐,换行方式,缩紧等。 254 | 段落样式储存在自身的NSParagraphStyle类中,您可以使用类NSMutableParagraphStyle来设置样式细节。代码8-4创建了一个超大尺寸段落分割和慷慨的第一行缩进,如图8-21所示。 255 | 256 | ``` 257 | NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; 258 | paragraphStyle.alignment = NSTextAlignmentLeft; 259 | paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; 260 | paragraphStyle.firstLineHeadIndent = 36.f; 261 | paragraphStyle.lineSpacing = 8.0f; 262 | paragraphStyle.paragraphSpacing = 24.f;//Big! 263 | [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:r]; 264 | ``` 265 | 266 | 图8-21 267 | 268 | 大多数的这些值都会值点距,如行距和段落距离。如果你够仔细,你可以逐段设置这些基础功能。样式对象是assign而不是copy。确保为每个对象分配不同的样式段落对象。如果不这样的话,就会之前的我一样发现一些问题,你可能会创建一个属性化的结果,所有段落都指向同一个对象,一个更新了,所有的都更新了。 269 | 270 | ##### 段落样式的属性 271 | 272 | 下方列举了段落样式属性,将这些应用于属性化的字符串,以设定如何绘制iOS文本上下文。: 273 | 274 | * aligment —— 段落对其方式,NSTextAlignment值。一边来说,有left,right,center和justifiede; 275 | * firstLineHeadIndent —— 每段第一行的缩进。需要提供一个非负的值。代码8-4使用36点进行首行缩进。 276 | * headIndent —— leading边沿的缩进——即,从左到右的语言中的左边,如英语,从右到左的语言中的右边,如阿拉伯语和希伯来语。通常处理引用块和其他缩进材料,该属性可以让您移离文本容器。 277 | * tailIndent ——与头部缩进相反,文本绘制容器尾部边沿的缩进。和其他的缩进一样,使用非负的浮点值。 278 | * maximumLineHeight 和 minimumLineHeight —— 最大和最小行高。 279 | * lineHeightMultiple —— 根据苹果的文档,“自然行高会在该属性行高倍数被定义之后才会使用maximumLineHeight和mimimumLineHeight” 280 | * lineSpacing —— 段落行间的空间,以点为单位。 281 | * paragraphSpacing —— 一段和下一段之间的额外空间,以点为单位,在8-4中,被设置为24。 282 | * paragraphSpacingBefore —— 在绘制开始的第一段之前的额外距离。 283 | * lineBreakMode —— 该属性可以指定换行的方式,wrapping或truncation或clipping,包括word wrapping(整个单词换行),character wrapping(单词中间可能断开),clipping,head truncation(比如,“···lo world”),tail truncation(比如,“hello wor···”),middle truncation(比如,“hel···orld”)。 284 | * hyphenationFactor —— 断字因子,苹果文档里说,“连字符会出现在(不使用连字符断开)文本的宽度与行尾宽度所占比例小于断字因子时。当段落断字因子为0.0时,布局管理里使用断字因子代替。当两者都是0.0时,断字被禁用”(注:???) 285 | * baseWritingDirection——当设置自然方向时,段落默认为——不论在与当前语言相关的区域设置了什么值——从左到右或者从右到左。 286 | 287 | ### 使用Core Text进行绘制。 288 | iOS 7的Text Kit在高端文本排版技术方面与传统的Core Text结合提供了很有用的路径。Text Kit提供了一个Objective-C为基础的类和协议为UITextView提供排版功能。Core Text,相反,使用C语言API。这两种排版都可以直接绘制在上下文中。如图8-3所示,Text Kit并不是十分完备,因此,本章使用Core Text而不是Text Kit。 289 | 在Core Text中,文本可以绘制在任何形状的贝塞尔曲线中。 290 | 291 | 图8-22 292 | 293 | 图8-22使用的是character wrapping,所以部分单词被分为了几行,但是布局中并没有缺少一个字符或者单词,所有的文本都会绘制在边界内,包围成特定形状。 294 | 当然,通常情况下,你不会吧文字画成这样奇怪的形状,我们这样做是想要演示代码8-1中的函数。对他输入一个路径和属性字符串,他把字符串绘如路径中,由Core Text提供,创建一个绘制文本的容器(“frame”)。 295 | Core Text框架提供了强大的文本布局和字体管理功能集。适用于处理文本领域的应用程序。Core text包括以下工具,如frame setter帮助你定义绘制复杂的字符串布局在几何目标中。这些布局也适用段落风格,如alignment对齐,tab stops制表位,line spacing行距,和其他通过attributed string创建的属性。 296 | 多亏了Core Text框架,所有绘图都可以在Quartz空间中进行,CGPath你还必须定义在Quartz的坐标系中,这就是为什么我选择了一个如8-11这样不是垂直对称的图。代码8-1使用以下方式处理绘制问题,复制所有路径,然后镜像副本。如果没有这一步,你最终会得到8-23所示的结果,文本依然是顶部到地步,而路径却在Quartz的坐标系中。 297 | 298 | 图8-23 299 | 300 | ``` 301 | void DrawAttributedStringInBezierPath(UIBezierPath *path,NSAttributedString *attributedString) 302 | { 303 | CGContextRef context = UIGraphicsGetCurrentContext(); 304 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 305 | 306 | //Mirror a copy of the path 307 | UIBezierPath *copy = [path safeCopy]; 308 | MirrorPathVerticallyInContext(copy); 309 | 310 | //Build a framesetter and extract a frame destination 311 | CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attributedString); 312 | CTFrameRef theFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,attributedString.length), copy.CGPath, NULL); 313 | 314 | //Draw into the frame 315 | PushDraw(^{ 316 | CGContextSetTextMatrix(context, CGAffineTransformIdentity); 317 | FlipContextVertically(GetUIKitContextSize()); 318 | CTFrameDraw(theFrame, UIGraphicsGetCurrentContext()); 319 | }); 320 | 321 | CFRelease(theFrame); 322 | CFRelease(framesetter); 323 | } 324 | 325 | //Flip the path vertically with respect to the context 326 | void MirrorPathVerticallyInContext(UIBezierPath *path) 327 | { 328 | CGContextRef context = UIGraphicsGetCurrentContext(); 329 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 330 | 331 | CGSize size = GetUIKitContextSize(); 332 | CGRect contextRect = SizeMakeRect(size); 333 | CGPoint center = RectGetCenter(contextRect); 334 | 335 | //Flip path with respect to the context size 336 | CGAffineTransform t = CGAffineTransformIdentity; 337 | t = CGAffineTransformTranslate(t, center.x, center.y); 338 | t = CGAffineTransformScale(t, 1, -1); 339 | t = CGAffineTransformTranslate(t, -center.x, -center.y); 340 | [path applyTransform:t]; 341 | } 342 | 343 | ``` 344 | 345 | ### 文本矩阵 346 | 文本矩阵定义了每个字符的变换,而不是这个那个上下文呢。他们被储存在每个上下文的GState中。当UIKit和Core Text结合绘制文本时,确保重制核心文本中的矩阵。 347 | 代码8-1在PashDraw()块中进行文本调用。让我们来看看如何执行更新。以下调用会重置所有上下文中的文本转换: 348 | 349 | ``` 350 | CGContextSetTextMatrix(context, CGAffineTransformIdentity); 351 | ``` 352 | 要理解为什么必须执行次步骤,请参考图8-24,此图中的文本绘制了两次——首先使用UIKit属性字符串绘制,然后使用Core Text。Core Text绘制的文本输出都是颠倒显示的。每个字母都是独立垂直倒影的。 353 | 354 | 图8-24 355 | 356 | 会出现这种奇怪的效果是因为UIKit字符串绘制改变了上下文的文本矩阵。你可以在代码8-5中看到具体是怎么发生的。 357 | 358 | ``` 359 | UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); 360 | CGAffineTransform t; 361 | 362 | //Retrieve the initial text matrix 363 | t = CGContextGetTextMatrix(UIGraphicsGetCurrentContext()); 364 | 365 | NSLog(@"Before: %f",atan2f(t.c, t.d)); 366 | 367 | //Draw the string 368 | [string drawInRect:targetRect]; 369 | 370 | //Retrieve the changed text matrix 371 | t = CGContextGetTextMatrix(UIGraphicsGetCurrentContext()); 372 | NSLog(@"After:%f", atan2f(t.c, t.d)); 373 | 374 | UIGraphicsEndImageContext(); 375 | ``` 376 | 你可能希望两个日志都不报循环。然而,实际上却会产生。得到的180度旋转的结果解释了如下的输出: 377 | 378 | ``` 379 | 2013-05-10 09:38:06.434 HelloWorld[49888:c07] Before: 0.00000 380 | 2013-05-10 09:38:06.438 HelloWorld[49888:c07] After: 3.141593 381 | ``` 382 | 不幸的是,你不能使用通过保存和恢复上下文来解决这一问题,据苹果称,“注意:文本矩阵不是图像状态的一部分——保存或恢复怼文本矩阵没有影响,文本矩阵是当前图像上下文的属性,而不是当前字体的属性。” 383 | 作为代替,当你切换到Core Text文本绘图时,显示重置文本矩阵,如下所示: 384 | 385 | ``` 386 | CGContextSetTextMatrix(context, CGAffineTransformIdentity); 387 | ``` 388 | 389 | ### 绘制列 390 | 图8-25展示了Core Text中的一个基本问题,这涉及到Core text列绘制。当布局列时,文本应该在每列的边缘换行而不是边缘的中间。例如,在右边的顶部列中,单词sitting应该出现在第一列的第二行。然而,他被放在了第二列的右边,文本在列间流动,然后向下流动,而不是全部沿着一列想在,然后沿着下一列向下。 391 | 392 | 图8-25 393 | 394 | 问题的核心是文本帧设置器处理整个Bezier路径(如图8-26)为一个单一的形状。他的两个垂直矩阵旨在显示独立的文本列。然而,真实情况,Core Text探测器只使用一个测试构建他的框架。他确定路径内的点和路径外的点。省略了所有其他考虑的事项,Core Text和iOS一般来说都没有列的概念。所以默认技术不支持我们对列的期望——逐列布局。 395 | 396 | 图8-26 397 | 398 | 图8-27显示了我想要这个布局做什么。这里,sitting得到了想要的换行。本文从第一列往下,然后继续到第二列。这种布局将两列视为单个数据流。文本从一列移动到下一列。 399 | 400 | 图8-27 401 | 402 | 比较图8-25和8-27的视觉风格。图8-27看起来更像是“文本”——有一系列结构良好的简短段落。图8-25显示了很多间隔线,且右侧的文本较少。他的布局不如图8-27看起来那么吸引人。 403 | 我通过调用代码8-1中的DrawAttributedStringInBezierPath()生成了图8-25中那样“之前的”图像。从图8-25的错误布局到图8-27的正确布局,实际上只需要很少的函数来调整。如代码8-2所示。 404 | 新函数称为DrawAttributedStringIntoSubpath()。他在一个子路径上工作——并用属性字符串来更新到上下文无法绘制的字符串上。为此,他查询Core Text字符串可见范围,该函数计算属性化字符串剩余的部分——即不可见的部分——并将其分配给其余参数。 405 | 代码中的第二个函数是DrawAttributedStringInBeziersSubPaths()。这个接口便利路径的子路径,在每个阶段检索“剩余”字符串并将其应用于绘图的下一个阶段。当函数完成绘制子路径时,或者剩余长度降为0时,函数返回。 406 | ``` 407 | void DrawAttributedStringIntoSubpath(UIBezierPath *path, NSAttributedString *attributedString, NSAttributedString **remainder) 408 | { 409 | CGContextRef context = UIGraphicsGetCurrentContext(); 410 | if(context == NULL) 411 | COMPLAIN_AND_BAIL(@"No context to draw into", nil); 412 | 413 | //Handle vertical mirroring 414 | UIBezierPath *copy = [path safeCopy]; 415 | MirrorPathVerticallyInContext(copy); 416 | 417 | //Establish the framesetter and retrieve the frame 418 | CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attributedString); 419 | CTFrameRef theFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedString.length), copy.CGPath, NULL); 420 | 421 | //If the remainder can be dereferenced, calculate 422 | //the remaining attributed string 423 | if(remainder) 424 | { 425 | CTRange range = CTFrameGetVisibleStringRange(theFrame); 426 | NSInteger extent = attributedString.length - startLocation; 427 | NSAttributedString *substring = [attributedString attributedSubstringFromRange:NSMakeRange(startLocation, extent)]; 428 | *remainder = substring; 429 | } 430 | 431 | //Perform the drawing in Quartz coordinates 432 | PushDraw(^{ 433 | FlipContextVertically(GetUIKitContextSize());( 434 | CTFrameDraw(theFrame, UIGraphicsGetCurrentContext()); 435 | }); 436 | 437 | //Clean up the Core Text objects 438 | CFRelease(theFrame); 439 | CFRelease(framesetter); 440 | } 441 | 442 | 443 | void DrawAttributedStringInBezierSubpaths(UIBezierPath *path, NSAttributedString *attributedString) 444 | { 445 | NSAttributedString *string; 446 | NSAttributedString *remainder = attributedString; 447 | 448 | //Iterate through subpaths, drawing the 449 | //attributed string into each section 450 | for(UIBezierPath *subpath in path.subpaths) 451 | { 452 | string = remainder; 453 | DrawAttributedStringIntoSubpath(subpath, string, &remainder); 454 | if(remainder.length == 0) return; 455 | } 456 | } 457 | ``` 458 | ### 图像剪切 459 | 代码8-2最适合于绘制图像上下文的列或独立元素。但它不适用于奇偶填充规则的带孔布局。如果你使用DrawAttributedStringInBezierSubpaths()函数,你会在整个路径上绘制文本,然后再次进入空中,因为外部路径和内部路径被分为了两个子路径。 460 | 要使用奇偶填充的复杂路径,请使用自己使用拓扑方式分解的路径。然后,调用DrawAttributeStringToSuppth()。 461 | 也就是说,奇偶填充规则确实能在路径中创建一些简单的切口。这可以容纳图像绘制。如图8-28所示,为了绘制该图像,我创建了一个内部矩形,并将其添加到我的路径中。这就形成了一个足够大的洞,可以绘制图像。 462 | 463 | 图8-28 464 | 465 | ### 沿着路径绘制属性化文本 466 | 沿着路径绘制文本是另一个常见的排版任务。图8-29显示了画在星型路径上的字符串,强调一下,输入的是属性字符串,在进行绘制之前,我已经随机给每个字母着色了。 467 | 468 | 图8-29 469 | 470 | 代码8-3计算了每个字母在属性字符串中的渲染大小,确定边界的高度和宽度。知道了这个尺寸就可以确定每个字符(或“字型”,(按照Core Text术语))消耗了多路径: 471 | 472 | ``` 473 | CGRect bounding = [item boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:0 context:nil]; 474 | ``` 475 | 这个边界矩形如果排列成一行在该行上的中心位置。代码8-3使用该距离来计算路径长度百分比,以及第五章中的路径插值返回的位置和坡度以供放置。正如你在第一章看到的,你可以位移和旋转上下文来精确定位定向你的文本。这允许字符串使用NSAttributedString的drawAtPoint:方式进行渲染。 476 | 遍历整个路径后,程序停止,将剪切掉任何剩下的字符;他们只是简单的没有被绘制。如果你想要保存整个字符串出现,你需要修改字体来配合路径长度。 477 | 478 | ``` 479 | @implementation UIBezierPath (TextUtilities) 480 | - (void) drawAttributedString:(NSAttributedString *)string 481 | { 482 | if(!string) return; 483 | CGContextRef context = UIGraphicsGetCurrentContext(); 484 | if(context == NULL) COMPLAIN_AND_BAIL(@"No context to draw into", nil); 485 | 486 | //Check the points 487 | if(self.elements.count < 2) return; 488 | 489 | //Keep a running tab of how far the glyphs have traveled to 490 | //be able to calculate the percent along the point path 491 | float glyphDistance = 0.0f; 492 | float lineLength = self.pathLength; 493 | 494 | for(int loc = 0; loc < string.length; loc++) 495 | { 496 | //Retrieve the character 497 | NSRange range = NSMakeRange(loc, 1); 498 | NSAttributedString *item = [string attributedSubstringFromRange:range]; 499 | 500 | //Start halfway through each character 501 | CGRect bounding = [item boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX) options:0 context:nil]; 502 | glyphDistance += bounding.size.width / 2; 503 | 504 | //Find new point on path 505 | CGPoint slope; 506 | CGFloat percentConsumed = glyphDistance / lineLength; 507 | CGPoint targetPoint = [self pointAtPercent:percentConsumed withSlope:&slope]; 508 | 509 | //Accommodate the forward progress 510 | glyphDistance += bounding.size.width / 2; 511 | if(percentConsumed >= 1.0f) break; 512 | 513 | //Calculate the rotation 514 | float angle = atan(slope.y /slope.x); 515 | if(slope.x < 0) angle += M_PI; 516 | 517 | //Draw the glyph 518 | PushDraw(^{ 519 | //Translate to target on path 520 | CGContextTranslateCTM(context, targetPoint.x, targetPoint.y); 521 | 522 | //Rotate along the slope 523 | CGContextRotateCTM(context, angle); 524 | 525 | //Adjust for the character size 526 | CGContextTranslateCTM(context, -bounding.size.width /2, -item.size.height /2); 527 | 528 | //Draw the character 529 | [item drawAtPoint:CGPointZero]; 530 | }); 531 | } 532 | } 533 | ``` 534 | 535 | ### 适配文本 536 | 很多UIKit类都带有size-to-fit选项。然而,并没有相同的方法来完成常规的绘制任务。你可以使用一种“适配”字符串的字体来达到目的。比如矩形。几乎所有的字体都可以适配矩形,如果字体足够小的话。不幸的是,这回导致极小的单行文字。最大的挑战是找到最大的字体来适配目标,让绘制的文本在矩形各个方向都适配。图8-30显示了不同的文本字符串。在尝试适配时,通过不停调整字体大小来适配矩形。 537 | 538 | 图8-30 539 | 540 | 代码8-4介绍了我用来创建图8-30中输出的算法。这并不是十分准确的,因为我通过容限参数增加了一些灵活性。正因如此,这个实现有时会稍微超出矩形。因此,考虑到调用这个函数,他的矩形实际上比你期望使用的要小一点。 541 | 该函数迭代选择字体。测试他的输出大小,当输出接近目标矩形,算法停止并返回最近的合适的字体。 542 | 和本章中的所有其他节一样,代码8-4已经更新为使用iOS7的请求了。新方法无法部署到iOS7之前到系统。 543 | 544 | ``` 545 | UIFont *FontForWrappedString(NSString *string, NSString *fontFace, CGRect rect, CGFloat tolerance) 546 | { 547 | if(rect.size.height < 1.0f) return nil; 548 | 549 | CGFloat adjustedWidth = tolerance * rect.size.width; 550 | CGSize measureSize = CGSizeMake(adjsutedWidth, CGFLOAT_MAX); 551 | 552 | //Initialize the proposed font 553 | CGFloat fontSize = 1; 554 | UIFont *proposedFont = [UIFont fontWithName:fontFace size:fontSize]; 555 | 556 | NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; 557 | paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping; 558 | 559 | NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; 560 | attributes[NSParagraphStyleAttributeName] = paragraphStyle; 561 | attributes[NSFontAttributeName] = proposedFont; 562 | 563 | //Measure the target 564 | CGSize targetSize = [string boundingRectWithSize:measureSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size; 565 | 566 | //Double until the size is exceeded 567 | while (targetSize.height <= rect.size.height) 568 | { 569 | //Establish a new proposed font 570 | fontSize *= 2; 571 | proposedFont = [UIFont fontWithName:fontFace size:fontSize]; 572 | 573 | //Measure the target 574 | attributes[NSFontAttributeName] = proposedFont; 575 | targetSize = [string boundingRectWithSize:measureSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size; 576 | 577 | //Break when the calculated height is too much 578 | if(targetSize.height > rect.size.height) 579 | break; 580 | } 581 | 582 | //Search between the previous and current font sizes 583 | CGFloat minFontSize = fontSize /2; 584 | CGFloat maxFontSize = fontSize; 585 | while(1) 586 | { 587 | //Get the minpoint between the two 588 | CGFloat minPoint = (minFontSize + (maxFontSize - minFontSize) / 2); 589 | proposedFont = [UIFont fontWithName:fontFace size:midPoint]; 590 | attributes[NSFontAttributeName] = proposedFont; 591 | targetSize = [string boundingRectWithSize:measureSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size; 592 | 593 | //Look up one font size 594 | UIFont *nextFont = [UIFont fontWithName:fontFace size:midPoint +!]; 595 | attributes[NSFontAttributeName] = nextFont; 596 | CGSize nextTargetSize = [string boundingRectWithSize:measureSize options:NSStringDrawUsesLineFragmentOrigin attributes:attributes context:nil].size; 597 | 598 | //Test both fonts 599 | CGFloat tooBig = targetSize.height > rect.size.height; 600 | CGFloat nextIsTooBig = nextTargetSize.height > rect.size.height; 601 | 602 | //If the current is sized right 603 | //but the next is too big. it`s a win 604 | if(!tooBig && nextIsTooBig) 605 | return [UIFont fontWithName:fontFace size:minPoint]; 606 | 607 | //Adjust the search space 608 | if(tooBig) 609 | maxFontSize = minPoint; 610 | else 611 | minFontSize = minPoint; 612 | } 613 | 614 | //Should never get here 615 | return [UIFont fontWithName:fontFace size:fontSize / 2]; 616 | } 617 | 618 | ``` 619 | 620 | ### 总结 621 | 本章讨论在iOS中绘制文本时会遇到的挑战,你已经了解了属性字符串和他的特点,以及如何使用Core Text将字符串绘制到路径文本中。下面是关于该话题最后需要注意的地方: 622 | 623 | * 这一章并不是我写这本书的原始章节,iOS 7 发布后,我的团队和我觉得应该更新这些素材会更有价值。出于这个原因,我修改了所有过时的素材,比如sizeWithFont:和drawInRect:withFont:。这意味着本章的绝大部份内容都不能直接在iOS6之前安装运行。除非进行改装。 624 | * 早在我想出第四章的string-to-UIBezierPath解决方案之前,我就想到代码8-4中的拟合算法的原型。这些天,当使用短字符串时,我更多地将字符串转换为路径并将其拟合为矩形。但是,对于袋包装的长字符串,代码8-4还是我最喜欢的。 625 | * 不要忘记第二章所提到的居中字符串绘制的解决方案。他提供了在矩形范围内绘制到目标矩形中间或形状上的简单方法。 626 | 627 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS Drawing Practical UIKit Solutions翻译 2 | >在阅读完 [ZslsMe](https://github.com/ZsIsMe)在github上分享的[iOS Core Animation Advanced Techniques](https://github.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques.git)一书的翻译之后, 快速的学习了很多实用iOS开发技巧, 避免了原本一个单词一个单词翻译的低效率的学习....作为iOS三件套,已经学习了Auto Layout和Core Animation的翻译本, 却始终找不到完整的Drawing的翻译本, 所以这里准备向ZslsMe学习, 整理一下本书的翻译...笔者英语水平也有限, 有某处翻译有歧义或不通顺, 或技术错误, 烦请指正. 3 | 4 | ## 已更新完成 5 | 6 | ## 目录 7 | 8 | * [1-绘制上下文](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/1-绘制上下文/1-绘制上下文.md) 9 | 10 | * [2-几何语言](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/2-几何语言/2-几何语言.md) 11 | 12 | * [3-绘制图像](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/3-绘制图像/3-绘制图像.md) 13 | 14 | * [4-路径基础](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/4-路径基础/4-路径基础.md) 15 | 16 | * [5-路径深入](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/5-路径深入/5-路径深入.md) 17 | 18 | * [6-绘制渐变](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/6-绘制渐变/6-绘制渐变.md) 19 | 20 | * [7-遮罩,模糊和动画](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/7-遮罩,模糊和动画/7-遮罩,模糊和动画.md) 21 | 22 | * [8-绘制文本](https://github.com/wangdicen/iOS-Drawing-Practical-UIKit-Soluations-Translation/blob/master/8-绘制文本/8-绘制文本.md) 23 | --------------------------------------------------------------------------------