CoreGraphic框架解析 (五)—— 基于CoreGraphic的一个简单绘制示例 (一)

版本记录

版本号 时间
V1.0 2018.10.21 星期日

前言

quartz是一个通用的术语,用于描述在iOSMAC OS X 中整个媒体层用到的多种技术 包括图形、动画、音频、适配。Quart 2D 是一组二维绘图和渲染APICore Graphic会使用到这组APIQuartz Core专指Core Animation用到的动画相关的库、API和类。CoreGraphicsUIKit下的主要绘图系统,频繁的用于绘制自定义视图。Core Graphics是高度集成于UIView和其他UIKit部分的。Core Graphics数据结构和函数可以通过前缀CG来识别。在app中很多时候绘图等操作我们要利用CoreGraphic框架,它能绘制字符串、图形、渐变色等等,是一个很强大的工具。感兴趣的可以看我另外几篇。
1. CoreGraphic框架解析(一)—— 基本概览
2. CoreGraphic框架解析(二)—— 基本使用
3. CoreGraphic框架解析(三)—— 类波浪线的实现
4. CoreGraphic框架解析(四)—— 基本架构补充

开始

首先看一下写作环境

Swift 4, iOS 11, Xcode 9

想象一下,你已经完成了你的应用程序,它工作正常,但界面缺乏风格。 您可以在Photoshop中绘制所有自定义控件图像的几种尺寸,并希望Apple不会出现@ 4x视网膜屏幕...或者,您可以提前思考并使用Core Graphics在代码中创建一个图像,可以清晰地缩放任何图像设备尺寸。

Core Graphics是Apple的矢量绘图框架 - 它是一个强大而强大的API,需要学习很多东西。 但是从不担心 - 这几篇教程将通过简单的开始让您轻松进入它,最后您将能够创建可在您的应用中使用的令人惊叹的图形。

这是一个全新的系列,采用现代方法教授Core Graphics。 该系列还包括@IBDesignable@IBInspectable等酷炫功能,使学习Core Graphics变得轻松有趣。

是时候开始吧!


Introducing Flo – One glass at a time

您将创建一个完整的应用程序来跟踪您的饮水习惯。

具体而言,它可以轻松跟踪您喝多少水。 “他们”告诉我们,每天喝八杯水是健康的,但几杯后很容易失去跟踪。 这就是Flo的用武之地;每当你喝掉一杯清爽的水,点击柜台。 您还会看到之前七天消费的图表。

在本系列的第一部分中,您将使用UIKit的绘图方法创建三个控件。

然后在第二部分中,您将深入了解Core Graphics上下文并绘制图形。

在第三部分中,您将创建一个带图案的背景,并为自己颁发一张自制的Core Graphics奖牌。

您的首要任务是创建自己的Flo应用程序。 没有下载可以让你前进,因为如果你从头开始构建它,你会学到更多。

创建一个新项目(File \ New \ Project ...),选择模板iOS \ Application \ Single View App并单击Next。

填写项目选项。 将Product Name设置为Flo,将语言设置为Swift,然后单击Next

在最后一个屏幕上,取消选中Create Git repository并单击Create

您现在有一个带有故事板和视图控制器的初学者项目。


Custom Drawing on Views - 视图上的自定义绘图

自定义绘图有三个步骤:

  • 1) 创建一个UIView子类。
  • 2) 覆盖draw(_ :)并添加一些Core Graphics绘图代码。
  • 3) 没有第3步 - 就是这样!

你可以通过制作一个自定义绘制的加号按钮来尝试这个,如下所示:

创建一个新文件(File \ New \ File ...),选择iOS \ Source \ Cocoa Touch Class,单击Next。 在此屏幕中,将新类命名为PushButton,使其成为UIButton的子类,并确保语言为Swift。 单击Next,然后单击Create

UIButton是UIView的子类,因此UIView中的所有方法(例如draw(_ :))也可以在UIButton中使用。

Main.storyboard中,将UIButton拖到视图控制器的视图中,然后选择Document Outline中的按钮。

Identity Inspector中,更改类以使用您自己的PushButton

1. Auto Layout Constraints - 自动布局约束

现在,您将设置自动布局约束(文本说明如下):

  • 1) 选中该按钮后,按住Control键从按钮中心稍微向左拖动(仍然在按钮内),然后从弹出菜单中选择Width
  • 2) 同样,选择按钮后,按住Control键从按钮中心稍微向上控制 - 拖动(仍然在按钮内),然后从弹出菜单中选择Height
  • 3) 按住Control键从按钮内部向左拖动到按钮外部,然后选择Center Vertically in Safe Area
  • 4) 最后按住Control键从按钮内部向上拖动到按钮外部,然后选择Center Horizontally in Safe Area

这将创建四个必需的自动布局约束;您现在可以在Size Inspector中看到它们:

单击Align center Y约束上的Edit,并将其常量设置为100。这会将按钮的垂直位置从中心移动到中心下方的100个点。 将WidthHeight约束常量更改为等于100。 最终约束应如下所示:

Attributes Inspector中,删除默认标题Button

如果您愿意,可以在此时构建和运行,但是现在您只能看到一个空白屏幕。 是时候解决这个问题了!


Drawing the Button - 绘制按钮

回想一下你想要制作的按钮是圆形的:

要在Core Graphics中绘制形状,您可以定义一条路径,告诉Core Graphics要跟踪的线(如加号的两条直线)或要填充的线(如此处应填充的圆)。 如果您熟悉Photoshop中的Illustrator或矢量形状,那么您将很容易理解路径。

关于路径有三个基本要点:

  • 可以描边和填充路径。
  • stroke概述了当前描边颜色的路径。
  • 填充将填充具有当前填充颜色的闭合路径。

创建Core Graphics路径的一种简单方法是通过一个名为UIBezierPath的便捷类。 这使您可以使用用户友好的API轻松创建路径,无论您是要基于线,曲线,矩形还是一系列连接点创建路径。

尝试使用UIBezierPath创建路径,然后用绿色填充它。 为此,请打开PushButton.swift并添加此方法:

override func draw(_ rect: CGRect) {
  let path = UIBezierPath(ovalIn: rect)
  UIColor.green.setFill()
  path.fill()
}

首先,创建一个椭圆形的UIBezierPath,它是传递给它的矩形的大小。 在这种情况下,它将是您在故事板中定义的100×100按钮的大小,因此“椭圆”实际上将是一个圆圈。

路径本身不会绘制任何东西。 您可以定义没有可用绘图上下文的路径。 要绘制路径,请在当前上下文中设置填充颜色(下面有更多内容),然后填充路径。

构建并运行应用程序,您将看到绿色圆圈。

到目前为止,您已经发现制作自定义形状的视图是多么容易。您已经通过创建UIButton子类,覆盖draw(_ :)并将UIButton添加到故事板来完成此操作。


Behind the Scenes in Core Graphics - 在Core Graphics的幕后

每个UIView都有一个图形上下文(context),视图的所有绘图在传输到设备的硬件之前呈现在此上下文中。

每当视图需要更新时,iOS都会通过调用draw(_ :)来更新上下文。这种情况发生在:

  • 该视图是屏幕上的新视图。
  • 它顶部的视图被移动了。
  • 视图的hidden属性已更改。
  • 您的应用程序显式调用视图上的setNeedsDisplay()setNeedsDisplayInRect()方法。

注意:在draw(_ :)中完成的任何绘图都会进入视图的图形上下文。请注意,如果您在draw(_ :)之外开始绘制绘图,正如您将在本教程的最后部分所做的那样,您将必须创建自己的图形上下文。

您还没有在本教程中使用过Core Graphics,因为UIKit包含许多Core Graphics函数的包装器。例如,UIBezierPathCGMutablePath的包装器,CGMutablePath是较低级别的Core Graphics API

注意:永远不要直接调用draw(_ :)。如果您的视图未更新,请在视图上调用setNeedsDisplay()

setNeedsDisplay()本身不调用draw(_ :),但它将视图标记为'dirty',在下一个屏幕更新周期使用draw(_ :)触发重绘。即使你在同一个方法中调用setNeedsDisplay()五次,你也只能实际调用draw(_ :)一次。


@IBDesignable – Interactive Drawing - 交互式绘图

创建代码来绘制路径,然后运行应用程序以查看它看起来像油漆干燥一样令人兴奋,但你有选择。 实时渲染(Live Rendering)允许视图通过运行draw(_ :)方法在故事板中更准确地绘制自己。 更重要的是,故事板将立即更新为draw(_ :)中的更改。 您只需要一个属性!

仍然在PushButton.swift中,就在类声明之前,添加:

@IBDesignable

这就是启用实时渲染所需的全部内容。 回到Main.storyboard并注意到,现在,您的按钮显示为绿色圆圈,就像您构建和运行时一样。

现在设置你的屏幕,以便你有故事板和代码并排。

通过选择PushButton.swift显示代码来执行此操作,然后在右上角单击Assistant Editor - 看起来像两个交织在一起的环的图标。 然后故事板应显示在右侧窗格中。 如果没有,则必须在窗格顶部的痕迹路径中选择故事板:

关闭故事板左侧的文档大纲以释放一些空间。 通过拖动文档大纲窗格的边缘或单击故事板底部的按钮来执行此操作:

完成所有操作后,您的屏幕应如下所示:

PushButtondraw(_:)中,改变

UIColor.green.setFill()

UIColor.blue.setFill()

你会(几乎)立即看到故事板中的变化。 太酷了!

现在,您将为加号创建行。


Drawing Into the Context - 绘制到上下文

Core Graphics使用“绘制模型(painter’s model)”。 当你画一个上下文时,它几乎就像画一幅画。 你铺设一条路并填充它,然后在顶部另一条路径上铺设路径并填满它。 您无法更改已放置的像素,但可以在上面覆盖“绘制”它们。

Apple的文档中的这张图片描述了它的工作原理。 正如您在画布上绘画时一样,绘制的顺序至关重要。

你的加号是在蓝色圆圈的顶部,所以首先你编码蓝色圆圈然后加号。

您可以为加号绘制两个矩形,但是更容易绘制路径然后用所需的厚度描边它。

PushButton中添加此结构和这些常量:

private struct Constants {
  static let plusLineWidth: CGFloat = 3.0
  static let plusButtonScale: CGFloat = 0.6
  static let halfPointShift: CGFloat = 0.5
}
  
private var halfWidth: CGFloat {
  return bounds.width / 2
}
  
private var halfHeight: CGFloat {
  return bounds.height / 2
}

现在在draw(_ :)方法的末尾添加此代码以绘制加号的水平短划线:

//set up the width and height variables
//for the horizontal stroke
let plusWidth: CGFloat = min(bounds.width, bounds.height) * Constants.plusButtonScale
let halfPlusWidth = plusWidth / 2

//create the path
let plusPath = UIBezierPath()

//set the path's line width to the height of the stroke
plusPath.lineWidth = Constants.plusLineWidth

//move the initial point of the path
//to the start of the horizontal stroke
plusPath.move(to: CGPoint(
  x: halfWidth - halfPlusWidth,
  y: halfHeight))

//add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(
  x: halfWidth + halfPlusWidth,
  y: halfHeight))

//set the stroke color
UIColor.white.setStroke()

//draw the stroke
plusPath.stroke()

在此块中,您设置UIBezierPath,为其指定一个起始位置(圆圈的左侧)并绘制到结束位置(圆圈的右侧)。 然后用白色描绘路径轮廓。 此时,您应该在Storyboard中看到这一点:

在你的故事板中,你现在将有一个蓝色圆圈,中间有一个破折号:

注意:请记住,路径只包含点。 这是一个简单的方法来掌握这个概念:创建路径时想象你手中拿着笔。 在页面上放两个点,然后将笔放在起点,然后通过画线画一条线到下一个点。

这基本上就是你使用move(to :)addLine(to :)来处理上面的代码。

现在在iPad 2或iPhone 6 Plus模拟器上运行应用程序,你会发现破折号并不像应该的那样清晰。 它有一条淡蓝色的线环绕着它。


Points and Pixels - 点和像素

回到第一批iPhone的时代,点和像素占据了相同的空间并且大小相同,这使得它们基本上是相同的。 当视网膜iPhone出现时,屏幕上突然出现了相同数量点数的四倍像素。

同样,iPhone 6 Plus再次增加了相同点的像素数量。

注意:以下是概念性的 - 实际硬件像素可能不同。 例如,在渲染3x后,iPhone 6 Plus会进行缩减采样以在屏幕上显示完整图像。 要了解有关iPhone 6 Plus下采样的更多信息,请查看这篇精彩文章

这是一个12×12像素的网格,其中的点以灰色和白色显示。 第一个(iPad 2)是点到像素的直接映射。 第二个(iPhone 6)是2x视网膜屏幕,其中一个点有4个像素,第三个(iPhone 6 Plus)是一个3x视网膜屏幕,其中有一个点有9个像素。

你刚刚画出的线高3点。 线条从路径的中心开始划线,因此在路径中心线的两侧绘制1.5个点。

此图显示了在每个设备上绘制3点线。 您可以看到iPad 2和iPhone 6 Plus导致线条被划分为半个像素 - 这当然是无法完成的。 因此,iOS使用两种颜色之间的颜色对半填充像素进行反锯齿处理,并且该线看起来模糊。

实际上,iPhone 6 Plus拥有如此多的像素,您可能不会注意到它的模糊性,尽管您应该在设备上查看自己的应用程序。 但是,如果你正在开发像iPad 2或iPad mini这样的非视网膜屏幕,你应该尽一切可能避免抗锯齿。

如果你有奇怪的直线,你需要将它们放在正负0.5点以防止消除锯齿。 如果你看一下上面的图表,你会发现iPad 2上的一半点会将线条向上移动半个像素,在iPhone 6上,向上移动整个像素,在iPhone 6 Plus上,向上移动一个半像素。

draw(_ :)中,将move(to :)addLine(to :)代码行替换为:

//move the initial point of the path
//to the start of the horizontal stroke
plusPath.move(to: CGPoint(
  x: halfWidth - halfPlusWidth + Constants.halfPointShift,
  y: halfHeight + Constants.halfPointShift))
    
//add a point to the path at the end of the stroke
plusPath.addLine(to: CGPoint(
  x: halfWidth + halfPlusWidth + Constants.halfPointShift,
  y: halfHeight + Constants.halfPointShift))

iOS现在将在所有三个设备上边缘清晰的渲染线条,因为您现在将路径移动了半个点。

注意:对于像素完美线条,您可以绘制和填充UIBezierPath(rect :)而不是线,并使用视图的contentScaleFactor计算矩形的宽度和高度。 与从路径中心向外绘制的笔划不同,填充仅在路径内绘制。

在前两行代码之后,在draw(_:)中设置笔触颜色之前,添加加号的垂直笔划。 我敢打赌,你可以自己弄清楚如何做到这一点,因为你已经绘制了一个水平笔划:

//Vertical Line
 
plusPath.move(to: CGPoint(
  x: halfWidth + Constants.halfPointShift,
  y: halfHeight - halfPlusWidth + Constants.halfPointShift))
      
plusPath.addLine(to: CGPoint(
  x: halfWidth + Constants.halfPointShift,
  y: halfHeight + halfPlusWidth + Constants.halfPointShift))

这与您用于在按钮上绘制水平线的代码基本相同。

您现在应该在故事板中看到加号按钮的实时渲染。 这样就完成了加号按钮的绘制。


@IBInspectable – Custom Storyboard Properties - 自定义sb属性

你需要为用户提供一个减号按钮。

减号按钮与加号按钮相同,只是它没有垂直条并且颜色不同。 您将对减号按钮使用相同的PushButton类,并在将其添加到故事板时声明它是什么类型的按钮及其颜色。

@IBInspectable是一个可以添加到属性的属性,使Interface Builder可以读取它。 这意味着您将能够在故事板中而不是在代码中配置按钮的颜色。

PushButton类的顶部,添加以下两个属性:

@IBInspectable var fillColor: UIColor = UIColor.green
@IBInspectable var isAddButton: Bool = true

更改draw(_:)顶部的填充颜色代码

UIColor.blue.setFill()

fillColor.setFill() 

该故事板视图中的按钮将变为绿色。

使用if语句将draw(_ :)中的垂直行代码包围:

//Vertical Line

if isAddButton {
  //vertical line code move(to:) and addLine(to:)
}
//existing code
//set the stroke color
UIColor.white.setStroke()
plusPath.stroke()

这使得只有在设置了isAddButton时才绘制垂直线 - 这样按钮可以是加号或减号按钮。

完成的PushButton看起来像这样:

import UIKit

@IBDesignable
class PushButton: UIButton {
  
  private struct Constants {
    static let plusLineWidth: CGFloat = 3.0
    static let plusButtonScale: CGFloat = 0.6
    static let halfPointShift: CGFloat = 0.5
  }
  
  private var halfWidth: CGFloat {
    return bounds.width / 2
  }
  
  private var halfHeight: CGFloat {
    return bounds.height / 2
  }
  
  @IBInspectable var fillColor: UIColor = UIColor.green
  @IBInspectable var isAddButton: Bool = true
  
  override func draw(_ rect: CGRect) {
    let path = UIBezierPath(ovalIn: rect)
    fillColor.setFill()
    path.fill()
    
    //set up the width and height variables
    //for the horizontal stroke
    let plusWidth: CGFloat = min(bounds.width, bounds.height) * Constants.plusButtonScale
    let halfPlusWidth = plusWidth / 2
    
    //create the path
    let plusPath = UIBezierPath()
    
    //set the path's line width to the height of the stroke
    plusPath.lineWidth = Constants.plusLineWidth
    
    //move the initial point of the path
    //to the start of the horizontal stroke
    plusPath.move(to: CGPoint(
            x: halfWidth - halfPlusWidth + Constants.halfPointShift,
            y: halfHeight + Constants.halfPointShift))
        
    //add a point to the path at the end of the stroke
    plusPath.addLine(to: CGPoint(
            x: halfWidth + halfPlusWidth + Constants.halfPointShift,
            y: halfHeight + Constants.halfPointShift))

    if isAddButton {
      //move the initial point of the path
      //to the start of the horizontal stroke
      plusPath.move(to: CGPoint(
        x: halfWidth - halfPlusWidth + Constants.halfPointShift,
        y: halfHeight + Constants.halfPointShift))
      
      //add a point to the path at the end of the stroke
      plusPath.addLine(to: CGPoint(
        x: halfWidth + halfPlusWidth + Constants.halfPointShift,
        y: halfHeight + Constants.halfPointShift))
    }
    
    //set the stroke color
    UIColor.white.setStroke()
    plusPath.stroke()
  }
}

在故事板中,选择按钮视图。 使用@IBInspectable声明的两个属性显示在Attributes Inspector的顶部:

Fill Color更改为RGB(87, 218, 213),并将Is Add Button更改为off。 通过转到Fill Color\Other…\Color Sliders并在颜色旁边的每个输入框中输入值来更改颜色,所以它看起来像这样:

更改将立即在故事板中进行:

很酷,嗯? 现在将Is Add Button更改为on,将按钮返回到加号按钮。


A Second Button - 第二个按钮

将新的UIButton添加到故事板并选择它。 将其类更改为PushButton,就像使用上一个类一样:

绿色加号按钮将在您的旧加号按钮下绘制。

在Attributes Inspector中,将Fill Color更改为RGB(238,77,77)并将Is Add Button更改为off

删除默认标题Button。

与以前的方式类似,为新视图添加自动布局约束:

  • 选择按钮后,按住Control键从按钮中心向左轻微拖动(仍在按钮内),然后从弹出菜单中选择Width
  • 同样,在选中按钮的情况下,按住Control键从按钮中心稍微向上拖动(仍在按钮内),然后从弹出菜单中选择Height
  • 控按住Control键从按钮内部向左拖动到按钮外部,然后选择Center Horizontally in Safe Area
  • 按住Control键从底部按钮向上拖动到顶部按钮,然后选择Vertical Spacing

添加约束后,在Size Inspector中编辑它们的常量值以匹配以下值:

构建并运行应用程序。 您现在拥有可重复使用的可自定义视图,您可以将其添加到任何应用程序中。 它在任何尺寸的设备上都清脆锐利。 这是在iPhone 4S上。


Arcs with UIBezierPath - 使用UIBezierPath的弧

您将创建的下一个自定义视图是:

这看起来像一个填充的形状,但弧实际上只是一个胖的描边路径。 轮廓是由两个弧组成的另一条描边路径。

创建一个新文件File \ New \ File ...,选择Cocoa Touch Class,并将新类命名为CounterView。 使它成为UIView的子类,并确保语言为Swift。 单击Next,然后单击Create

将代码替换为:

import UIKit

@IBDesignable class CounterView: UIView {
  
  private struct Constants {
    static let numberOfGlasses = 8
    static let lineWidth: CGFloat = 5.0
    static let arcWidth: CGFloat = 76
    
    static var halfOfLineWidth: CGFloat {
      return lineWidth / 2
    }
  }
  
  @IBInspectable var counter: Int = 5
  @IBInspectable var outlineColor: UIColor = UIColor.blue
  @IBInspectable var counterColor: UIColor = UIColor.orange
  
  override func draw(_ rect: CGRect) {
    
  }
}

在这里,您还可以创建一个包含常量的结构体。这些常数将在绘图时使用,奇数一个 - numberOfGlasses - 是每天饮用的目标水杯数。达到此数字时,计数器将达到最大值。

您还可以创建三个可以在故事板中更新的@IBInspectable属性。变量counter跟踪消耗的杯水数量,它是一个@IBDesignable属性,因为它能够在故事板中更改它,这对于测试计数器视图非常有用。

转到Main.storyboard并在加上PushButton上方添加一个UIView。与以前的方式类似,为新视图添加自动布局约束:

  • 1) 选择视图后,按住Control键从按钮中心稍微向左拖动(仍然在视图中),然后从弹出菜单中选择Width
  • 2) 同样,在选择视图的情况下,按住Control键从按钮中心稍微向上(仍在视图中)进行控制 - 拖动,然后从弹出菜单中选择Height
  • 3) 按住Control键从视图内部向左拖动到视图外部,然后选择Center Horizontally in Safe Area
  • 4) 按住Control键从视图向下拖动到顶部按钮,然后选择Vertical Spacing

Size Inspector中编辑约束常量,如下所示:

Identity Inspector中,将UIView的类更改为CounterView。 您在draw(_ :)中编码的任何绘图现在都会显示在视图中(但您还没有添加任何图形!)。


Impromptu Math Lesson - 即兴数学课

我们暂时打断了这个教程的简短介绍,希望在高中水平数学方面不可怕。

上下文中的绘图基于此单位圆。 单位圆是半径为1.0的圆。

红色箭头显示弧的开始和结束位置,以顺时针方向绘制。 你将从3π/ 4弧度的位置绘制一个弧 - 相当于135º,顺时针到π/ 4弧度 - 即45º。

弧度通常用于编程而不是度数,并且能够以弧度进行思考是有用的,这样您每次想要使用圆时都不必转换为度数。 稍后你需要弄清楚弧长,这是弧度发挥作用的时候。

单位圆中的圆弧长度(半径为1.0)与弧度中的角度测量值相同。 例如,查看上图,弧度从0º到90º的长度为π/ 2。 要计算实际情况下弧的长度,请取单位圆弧长度并将其乘以实际半径。

要计算上面红色箭头的长度,您只需要计算它跨越的弧度数:

 2π – end of arrow (3π/4) + point of arrow (π/4) = 3π/2

转换为度数就是

 360º – 135º + 45º = 270º

Back to Drawing Arcs - 回到绘制弧

CounterView.swift中,添加此代码到draw(_:)以绘制弧:

// 1
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)

// 2
let radius: CGFloat = max(bounds.width, bounds.height)

// 3
let startAngle: CGFloat = 3 * .pi / 4
let endAngle: CGFloat = .pi / 4

// 4
let path = UIBezierPath(arcCenter: center,
                           radius: radius/2 - Constants.arcWidth/2,
                       startAngle: startAngle,
                         endAngle: endAngle,
                        clockwise: true)

// 5
path.lineWidth = Constants.arcWidth
counterColor.setStroke()
path.stroke()

下面进行细分:

  • 1) 定义视图的中心点,您可以在其中旋转圆弧。
  • 2) 根据视图的最大尺寸计算半径。
  • 3) 定义弧的起始角和终止角。
  • 4) 根据刚刚定义的中心点,半径和角度创建路径。
  • 5) 在最终描边路径之前设置线宽和颜色。

想象一下用指南针绘制它 - 你将指南针的点放在中心,将手臂打开到你需要的半径,用粗笔装上它并旋转它以画出你的弧线。

在此代码中,center是指南针的点,radius是指南针打开的宽度(减去笔宽度的一半),弧宽是指笔的宽度。

在故事板中以及运行应用程序时,您将看到以下内容:


Outlining the Arc

当用户表示他们已经享用了一杯水时,counter上的轮廓显示了朝向八杯水的目标的进展。

该轮廓将包括两个弧,一个外部和一个内部,以及两条连接它们的线。

CounterView.swift中,将此代码添加到draw(_:)结束:

//Draw the outline

//1 - first calculate the difference between the two angles
//ensuring it is positive
let angleDifference: CGFloat = 2 * .pi - startAngle + endAngle
//then calculate the arc for each single glass
let arcLengthPerGlass = angleDifference / CGFloat(Constants.numberOfGlasses)
//then multiply out by the actual glasses drunk
let outlineEndAngle = arcLengthPerGlass * CGFloat(counter) + startAngle

//2 - draw the outer arc
let outlinePath = UIBezierPath(arcCenter: center,
                                  radius: bounds.width/2 - Constants.halfOfLineWidth,
                              startAngle: startAngle,
                                endAngle: outlineEndAngle,
                               clockwise: true)

//3 - draw the inner arc
outlinePath.addArc(withCenter: center,
                       radius: bounds.width/2 - Constants.arcWidth + Constants.halfOfLineWidth,
                   startAngle: outlineEndAngle,
                     endAngle: startAngle,
                    clockwise: false)
    
//4 - close the path
outlinePath.close()
    
outlineColor.setStroke()
outlinePath.lineWidth = Constants.lineWidth
outlinePath.stroke()

这里要介绍几件事:

  • 1) outlineEndAngle是弧应该结束的角度,使用当前counter值计算。
  • 2) outlinePath是外弧。 半径被赋予UIBezierPath()以计算弧的实际长度,因为该弧不是单位圆。
  • 3) 向第一个弧添加内弧。 它具有相同的角度但反向绘制(顺时针设置为false)。 此外,这会自动在内弧和外弧之间画一条线。
  • 4) 关闭路径会自动在弧的另一端绘制一条线。

CounterView.swift中的counter属性设置为5,您的CounterView现在应该在故事板中如下所示:

打开Main.storyboard,选择CounterView,在Attributes Inspector中,更改Counter属性以检查绘图代码。 你会发现它是完全互动的。 尝试将计数器调整为大于8且小于零。 你稍后会解决这个问题。

Counter Color更改为RGB(87,218,213),并将Outline Color更改为RGB(34,110,100)


Making it All Work

恭喜! 你有控制,您所要做的就是将它们连接起来,以便加号按钮递增计数器,减号按钮递减计数器。

Main.storyboard中,将UILabel拖动到Counter View的中心,并确保它是Counter View的子视图。 它在文档大纲中看起来像这样:

添加约束以垂直和水平居中标签。 最后,标签应该具有如下所示的约束:

Attributes Inspector中,将Alignment更改为center,将font size更改为36,将默认标签标题更改为8。

转到ViewController.swift并将这些属性添加到类的顶部:

//Counter outlets
@IBOutlet weak var counterView: CounterView!
@IBOutlet weak var counterLabel: UILabel!

仍然在ViewController.swift中,将此方法添加到类的末尾:

@IBAction func pushButtonPressed(_ button: PushButton) {
  if button.isAddButton {
    counterView.counter += 1
  } else {
    if counterView.counter > 0 {
      counterView.counter -= 1
    }
  }
  counterLabel.text = String(counterView.counter)
}

在这里,您可以根据按钮的isAddButton属性递增或递减计数器,确保计数器不会降至零以下 - 没有人可以喝负水。 您还可以更新标签中的计数器值。

还要将此代码添加到viewDidLoad()的末尾,以确保counterLabel的初始值将更新:

counterLabel.text = String(counterView.counter)  

Main.storyboard中,连接CounterView outlet和UILabel outlet。 将方法连接到两个PushButtonTouch Up Inside事件。

运行该应用程序,看看您的按钮是否更新了计数器标签。 他们应该。

但是等等,为什么计数器视图不更新?

想想回到本教程的开头,以及如何在移动其上的其他视图,或者更改其hidden属性,或者视图是屏幕新视图或应用程序调用时,视图上的setNeedsDisplay()setNeedsDisplayInRect()方法就会调用draw(_ :)方法。

但是,只要计数器属性更新,计数器视图就需要更新,否则用户会认为您的应用程序已被破坏。

转到CounterView.swift并将counter属性声明更改为:

@IBInspectable var counter: Int = 5 {
  didSet {
    if counter <=  Constants.numberOfGlasses {
      //the view needs to be refreshed
      setNeedsDisplay()
    }
  }
}

此代码使得仅当计数器小于或等于用户的目标杯水数时视图才会刷新,因为轮廓仅上升到8。

再次运行您的应用。 现在一切都应该正常运作。

后记

本篇主要讲述了基于CoreGraphic的一个简单绘制示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容