iOS翻译-Core Graphics教程1:入门
翻译了一篇raywenderlich的文章,Core Graphics的入门教程。(有部分省略,剩下的都是主要过程。)
原文链接: http://www.raywenderlich.com/90690/modern-core-graphics-with-swift-part-1
想象一下你开发完成了你的app,并且运行良好,但是界面不太好看。你可以用Photoshop绘制多个size的控件图片,希望Apple不会出@4x retina的屏幕。。
或者你可以提前预想到使用 Core Graphice 代码创建一个image,这样就能自动适应各种尺寸显示在设备上。
Core Graphics 是Apple的矢量绘图框架。它非常强大,API功能齐全,里面有许多需要学习的知识。但不用害怕,这里三部分可以引导你入门,最后你将在app里面创建一个好看的图形。
这是一个全新的系列,用先进的方法来教开发者使用 Core Graphice 。这个系列全部在Xcode6用Swift写(现在Xcode7了,测试可以通过),包含了新的功能,例如 @IBDesignable 和 @IBInspectable ,可以让 Core Graphics 学起来更加有趣和容易。
带着你最喜欢的饮料,让我们开始吧!
介绍Flo - 每次喝一杯
你将创建一个完整的app来记录你喝水的习惯。
这个app很容易记录你喝了多少水。 它会告诉我们一天喝8杯水是健康的,但很容易在记录几杯后就忘记了。这就是写Flo的原因。每次你干完一杯新鲜的水,点击计数器。你也可以在里面看见7天的记录。
在这部分里面,你将使用 UIKit 的绘制方法创建3个控件。
在第二部分,你将深入了解 Core Graphice 内容,绘制图形。
在第三部分,你将创建带图案的背景,奖励自己一枚自己绘制的金牌.
创建自定义视图
当你想自定义绘制图形的时候,你需要三个步骤:
- 1、创建 UIView 的子类
- 2、覆盖 drawRect 方法,在里面写一些绘制的代码
- 3、没有第三步了
让我们尝试做一个自定义的加号按钮
先差创建一个button,命名为 PushButtonView
UIButton 是 UIView 的子类,所以在 UIButton 里面能够使用 UIView 的所有方法,例如 drawRect
打开 Identity Inspector ,修改class为自定义的 PushButtonView
坐标和大小是X=250, Y=350, Width=100, and Height=100
增加约束
使用Auto Layout增加约束
会创建4个约束,你可以在 Size Inspector 里面看见下面的内容
移除默认的Button的title
绘制Button
首先需要明白3个原理,绘制图片的路径:
- 1、路径是可以绘制和填充的
- 2、路径轮廓的颜色是当前绘制的颜色
- 3、使用当前的填充颜色填满封闭的路径
创建 Core Graphice 路径可以通过 UIBezierPath ,他提供了友好的API创建路径。无论你想要线、曲线、矩形、还有一些列的连接点。
在 PushButtonView.swift 下面添加方法
override func drawRect(rect: CGRect) { var path = UIBezierPath(ovalInRect: rect) UIColor.greenColor().setFill() path.fill() }
这里用 ovalInRect 椭圆形方法,传递了一个矩形的大小。生成一个100*100的button在storyboard.所以椭圆形实际上是圆形。
路径本身不知道如何绘制。你可以定义一个路径但是不会绘制任何有用的内容。为了绘制路径,你可以给当前内容一个填充的颜色(fill color)
运行程序将看见绿色的圆形.
到目前为止,你会发现创建一个自定义的图形是多么容易,你已经创建了一个button的子类,覆盖了 drawRect 方法,并且添加了 UIButton 在你的Storyboard上面
Core Graphics绘制的原理
每一个 UIView 都有一个 graphics context (绘图上下文),在设备硬件显示前,绘制的所有视图都会被渲染到这个上下文中.
iOS 在任何时候需要更新视图都是通过调用 drawRect 方法。发生在
- 1、视图是在屏幕上是新的
- 2、顶部视图被移除
- 3、视图的hidden属性改变
- 4、明确调用 setNeedsDisplay() 和 setNeedsDisplayInRect() 方法
注意:所有 drawRect 里面绘制,在完成之后会放到view 的graphics context中。如果你在 drawRect 外部绘制,你需要在最后面创建自己的graphics context
你还不必使用 Core Graphics 因为UIKit封装了很多 Core Graphics 的方法。例如 UIBezierPath 封装了 CGMutablePath (这是Core Graphics底层的API)
注意:不要直接调用 drawRect . 如果你需要更新视图,调用 setNeedsDisplay() 方法
setNeedsDisplay() 不会自己调用 drawRect 方法,但是会标记视图,让视图通过 drawRect 重绘在下一次循环更新的时候。 所以当你在一个方法里面多次调用 setNeedsDisplay() 的时候,你实际上也只是调用了一次 drawRect
代码创建路径去绘制,运行app去看结果看起来就像等颜料干一样精彩。但是你有其他的选择,Xcode6允许一个视图通过 @IBDesignable 设置属性。 可以在storyboard上面实时更新。
在 PushButtonView.swift ,在class声明前添加 @IBDesignable
打开 Assistant Editor ,通过下面视图查看
最后屏幕是这个样子的
修改显示的颜色,改成blueColor
UIColor.blueColor().setFill()
你会发现屏幕会立即改变
下面我们来添加”+”符号的线
绘制到Context
Core Graphics使用的是”画家绘画的模式”(原文是”Core Graphics uses a “painter’s model.”,一开始不太明白是什么意思,但是看下文的图再来看这句话就明白是什么意思了)
当你画一个内容的时候,就像在制作一幅画。你绘制了一个路径并且填满它,然后你在这上面又绘制了另外一个路径填满它。 你不可能改变绘制的像素,但是你可以覆盖他们。
下面这张图片来自苹果的官方文档,描述了是如何工作的。正如你在一块画板上绘制图片,决定样式的是你绘制的顺序
你的 + 号在蓝色圆形的上面,所以首先你需要写绘制蓝色圆形的代码,然后才是加号的绘制。
你可以画两个矩形实现加号,但是你同样可以画一个路径然后用同样的厚度描边
修改 drawRect() 的代码
//set up the width and height variables //for the horizontal stroke let plusHeight: CGFloat = 3.0 let plusWidth: CGFloat = min(bounds.width, bounds.height) * 0.6 //create the path var plusPath = UIBezierPath() //set the path's line width to the height of the stroke plusPath.lineWidth = plusHeight //move the initial point of the path //to the start of the horizontal stroke plusPath.moveToPoint(CGPoint( x:bounds.width/2 - plusWidth/2, y:bounds.height/2)) //add a point to the path at the end of the stroke plusPath.addLineToPoint(CGPoint( x:bounds.width/2 + plusWidth/2, y:bounds.height/2)) //set the stroke color UIColor.whiteColor().setStroke() //draw the stroke plusPath.stroke()
可以在storyboard中看到是这样的结果
在iPad2和iPhone 6 Plus模拟器上运行。你会发现下面的情况
点和像素
点和像素占据相同的空间和大小,本质上是相同的事情。当retain的iPhone面世的时候,相同点上有4个像素
同样的,iPhone6 Plus再一次把每个点的像素提升了。
具体学习可以参考 这篇 文章
这里有一个12 * 12 像素的宫格,点是灰色和白色的。iPad2是点和像素直接映射。iPhone6是2x retain屏幕,4个像素是一个点。第三个iPhone6 Plus 是3x retain屏幕,9个像素是一个点.
刚刚画得线是3个点的高度。线从路径的中间开始描绘,所以1.5个点会描绘在线的两边。
这个图片展示了将回执3个点的线在设备上的情况。iPad2和iPhone6 Plus结果是需要跨越半个像素。iOS在两种颜色中当一种颜色只有半边像素填充的时候会抗锯齿,所以线会看得模糊
实际的情况是,iPhone6 Plus有很多个像素,所以可能看不到模糊的情况。尽管如此,你需要检查在真机上检查你的app。但是假如你在不是retain的屏幕上(iPad2 or iPad mini),你可以避免抗锯齿。
如果你的直线大小是单数,你应该把她们的点增加或减少0.5为了预防锯齿。在iPad2上将移动半个像素点,在iPhone6上,刚好充满整个像素。在iPhone6 Plus,刚好充满1.5个像素.
修改后的代码
//move the initial point of the path //to the start of the horizontal stroke plusPath.moveToPoint(CGPoint( x:bounds.width/2 - plusWidth/2 + 0.5, y:bounds.height/2 + 0.5)) //add a point to the path at the end of the stroke plusPath.addLineToPoint(CGPoint( x:bounds.width/2 + plusWidth/2 + 0.5, y:bounds.height/2 + 0.5))
iOS 将清晰的渲染直线在三个设备上,因为你改变了路径的半个点
注意:为了线展现完美的像素,你可以用 UIBezierPath(rect:) 用fill填充,取代直接画线。使用 contentScaleFactor 计算矩形的高度和宽度。 不像从路径中心像两边描绘,fill只会向路径里面填充 (这个东西好重要呀。。。)
接下来化垂直的线
//Vertical Line //move to the start of the vertical stroke plusPath.moveToPoint(CGPoint( x:bounds.width/2 + 0.5, y:bounds.height/2 - plusWidth/2 + 0.5)) //add the end point to the vertical stroke plusPath.addLineToPoint(CGPoint( x:bounds.width/2 + 0.5, y:bounds.height/2 + plusWidth/2 + 0.5))
结果是这样子的
<h2>@IBInspectable 自定义Storyboard属性</h2> <p>@IBInspectable 定义的属性能够在IB里面可见。这意味着你可以不用代码,在IB里面设置button的颜色 </p>
@IBInspectable var fillColor: UIColor = UIColor.greenColor() @IBInspectable var isAddButton: Bool = true
在 drawRect 里面修改
UIColor.blueColor().setFill()
变成
fillColor.setFill()
最后修改好的代码是
import UIKit @IBDesignable class PushButtonView: UIButton { @IBInspectable var fillColor: UIColor = UIColor.greenColor() @IBInspectable var isAddButton: Bool = true override func drawRect(rect: CGRect) { var path = UIBezierPath(ovalInRect: rect) fillColor.setFill() path.fill() //set up the width and height variables //for the horizontal stroke let plusHeight: CGFloat = 3.0 let plusWidth: CGFloat = min(bounds.width, bounds.height) * 0.6 //create the path var plusPath = UIBezierPath() //set the path's line width to the height of the stroke plusPath.lineWidth = plusHeight //move the initial point of the path //to the start of the horizontal stroke plusPath.moveToPoint(CGPoint( x:bounds.width/2 - plusWidth/2 + 0.5, y:bounds.height/2 + 0.5)) //add a point to the path at the end of the stroke plusPath.addLineToPoint(CGPoint( x:bounds.width/2 + plusWidth/2 + 0.5, y:bounds.height/2 + 0.5)) //Vertical Line if isAddButton { //move to the start of the vertical stroke plusPath.moveToPoint(CGPoint( x:bounds.width/2 + 0.5, y:bounds.height/2 - plusWidth/2 + 0.5)) //add the end point to the vertical stroke plusPath.addLineToPoint(CGPoint( x:bounds.width/2 + 0.5, y:bounds.height/2 + plusWidth/2 + 0.5)) } //set the stroke color UIColor.whiteColor().setStroke() //draw the stroke plusPath.stroke() } }
isAddButton 的设置可以标识是否需要添加竖线,也就是表明是加号还是减号
改变fill颜色RGB(87, 218, 213), isAddButton 为off
显示出来的结果是
UIBezierPath 画圆弧
下面我们自定义的视图是这样子的
这看起来像一个填充的形状,但是这个圆弧实际上是一个大的描边。外部的线是另外一个路径的描边组成的2个圆弧。
创建一个 CounterView 类,这个事UIView的子类
import UIKit let NoOfGlasses = 8 let π:CGFloat = CGFloat(M_PI) @IBDesignable class CounterView: UIView { @IBInspectable var counter: Int = 5 @IBInspectable var outlineColor: UIColor = UIColor.blueColor() @IBInspectable var counterColor: UIColor = UIColor.orangeColor() override func drawRect(rect: CGRect) { } }
NoOfGlasses :是一个数字表明每天喝水的杯数。
counter : 记录了喝水的杯数
在刚刚PushButtonView上面放置一个视图,所属类是 CounterView ,坐标大小是
数学知识
画这个圆弧需要根据单位园来画
红色箭头表示开始与结束的点,顺时针绘画。 从3π/4弧度开始画。相当于135°,顺时针到π/4弧度,也就是45°
画弧度
在 CounterView.swift 里面, drawRect 方法
// 1 let center = CGPoint(x:bounds.width/2, y: bounds.height/2) // 2 let radius: CGFloat = max(bounds.width, bounds.height) // 3 let arcWidth: CGFloat = 76 // 4 let startAngle: CGFloat = 3 * π / 4 let endAngle: CGFloat = π / 4 // 5 var path = UIBezierPath(arcCenter: center, radius: radius/2 - arcWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: true) // 6 path.lineWidth = arcWidth counterColor.setStroke() path.stroke()
- 1、设置中心点
- 2、计算视图最大尺寸的半径
- 3、计算扇形的厚度
- 4、设置开始和结束的弧度
- 5、根据中心点、半径、还有度数画路径
- 6、设置线的宽度和颜色,最后把路径绘制出来。
注意:这里有画弧的更详细的介绍 Core Graphics Tutorial on Arcs and Paths
最后实现的效果是
圆弧的轮廓
//Draw the outline //1 - first calculate the difference between the two angles //ensuring it is positive let angleDifference: CGFloat = 2 * π - startAngle + endAngle //then calculate the arc for each single glass let arcLengthPerGlass = angleDifference / CGFloat(NoOfGlasses) //then multiply out by the actual glasses drunk let outlineEndAngle = arcLengthPerGlass * CGFloat(counter) + startAngle //2 - draw the outer arc var outlinePath = UIBezierPath(arcCenter: center, radius: bounds.width/2 - 2.5, startAngle: startAngle, endAngle: outlineEndAngle, clockwise: true) //3 - draw the inner arc outlinePath.addArcWithCenter(center, radius: bounds.width/2 - arcWidth + 2.5, startAngle: outlineEndAngle, endAngle: startAngle, clockwise: false) //4 - close the path outlinePath.closePath() outlineColor.setStroke() outlinePath.lineWidth = 5.0 outlinePath.stroke()
- 1、 outlineEndAngle 是轮廓结束的度数。根据当前的 counter 来计算
- 2、 outlinePath 是轮廓的路径。
- 3、添加一个内置的圆弧。有相同的度数,不过要反着来绘画(clockWise要设置为false)
- 4、自动闭合路径,画线。
最后的结果是这样的
让它工作起来
在Storyboard里面调整Counter Color为 RGB(87, 218, 213) ,Outline Color为 RGB(34, 110, 100)
在 ViewController.swift 里面增加这些属性和方法
//Counter outlets @IBOutlet weak var counterView: CounterView! @IBOutlet weak var counterLabel: UILabel!
@IBAction func btnPushButton(button: PushButtonView) { if button.isAddButton { counterView.counter++ } else { if counterView.counter > 0 { counterView.counter-- } } counterLabel.text = String(counterView.counter) }
在 viewDidLoad 里面设置counterLabel的更新值
counterLabel.text = String(counterView.counter)
最后在Storyboard里面连线
为了能够点击按钮后能够重新绘制,需要修改 CounterView 里面的 counter 属性的setter方法,调用 setNeedsDisplay
@IBInspectable var counter: Int = 5 { didSet { if counter <= NoOfGlasses { //the view needs to be refreshed setNeedsDisplay() } } }
最后app可以运行啦
总结:
1、学习了绘图的基本原理
2、如何使用 @IBDesignable 和 @IBInspectable3、抗锯齿问题是如何解决的
4、绘图的顺序,以及扇形的基本知识,如何去绘制扇形
来自: http://www.liuchendi.com/2016/01/07/iOS/31_CoreGraphics_1/