iOS翻译-Core Graphics教程1:入门

jopen 9年前

翻译了一篇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()  }
</div>

这里用 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

<h2>@IBDesignable - 交互式绘制</h2>

代码创建路径去绘制,运行app去看结果看起来就像等颜料干一样精彩。但是你有其他的选择,Xcode6允许一个视图通过 @IBDesignable 设置属性。 可以在storyboard上面实时更新。

在 PushButtonView.swift ,在class声明前添加 @IBDesignable

打开 Assistant Editor ,通过下面视图查看

最后屏幕是这个样子的

修改显示的颜色,改成blueColor

UIColor.blueColor().setFill()
</div>

你会发现屏幕会立即改变

下面我们来添加”+”符号的线

绘制到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()
</div>

可以在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))
</div>

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))
</div>

结果是这样子的

<h2>@IBInspectable 自定义Storyboard属性</h2> <p>@IBInspectable 定义的属性能够在IB里面可见。这意味着你可以不用代码,在IB里面设置button的颜色 </p>
@IBInspectable var fillColor: UIColor = UIColor.greenColor()  @IBInspectable var isAddButton: Bool = true
</div>

在 drawRect 里面修改

UIColor.blueColor().setFill()
</div>

变成

fillColor.setFill()
</div>

最后修改好的代码是

 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()       }     }
</div>

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) {       }  }
</div>

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()
</div>
  • 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()
</div>
  • 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!
</div>
@IBAction func btnPushButton(button: PushButtonView) {    if button.isAddButton {      counterView.counter++    } else {      if counterView.counter > 0 {        counterView.counter--      }    }    counterLabel.text = String(counterView.counter)  }
</div>

在 viewDidLoad 里面设置counterLabel的更新值

counterLabel.text = String(counterView.counter)
</div>

最后在Storyboard里面连线

为了能够点击按钮后能够重新绘制,需要修改 CounterView 里面的 counter 属性的setter方法,调用 setNeedsDisplay

 @IBInspectable var counter: Int = 5 {    didSet {      if counter <=  NoOfGlasses {        //the view needs to be refreshed        setNeedsDisplay()      }    }  }
</div>

最后app可以运行啦

总结:

1、学习了绘图的基本原理

2、如何使用 @IBDesignable 和 @IBInspectable

3、抗锯齿问题是如何解决的

4、绘图的顺序,以及扇形的基本知识,如何去绘制扇形

</div>

来自: http://www.liuchendi.com/2016/01/07/iOS/31_CoreGraphics_1/