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框架解析(四)—— 基本架构补充
5. CoreGraphic框架解析 (五)—— 基于CoreGraphic的一个简单绘制示例 (一)

开始

在第二部分中,您将深入研究Core Graphics,了解绘制渐变和使用CGContexts进行变换操作。

你现在要离开UIKit的舒适世界,进入Core Graphics的黑社会。

Apple的这张图片从概念上描述了相关的框架:

UIKit是最顶层的,也是和开发者最容易接触的的。 您已经使用了UIBezierPath,它是Core GraphicsCGPath的UIKit包装器。

Core Graphics框架基于Quartz高级绘图引擎。 它提供低级,轻量级的2D渲染。 您可以使用此框架来处理基于路径的绘图,转换,颜色管理等等。

关于下层Core Graphics对象和函数的一件事是它们总是有前缀CG,所以它们很容易识别。

当你到本教程结束时,你将创建一个如下所示的图形视图:

在绘制图表视图之前,您将在故事板中进行设置,并创建动画转换的代码以显示图表视图。

完整的视图层次结构如下所示:

前一篇我们已经做到了下面这个程度。 唯一的区别是在Main.storyboard中,CounterView位于另一个视图(带黄色背景)内。 构建并运行,这将是您将看到的:

转到File \ New \ File ...,选择iOS \ Source \ Cocoa Touch Class模板,然后单击Next。输入名称GraphView作为类名,选择子类UIView并将语言设置为Swift。单击Next,然后单击Create

现在,在Main.storyboard中,单击Document Outline中黄色视图的名称两次以重命名,然后将其命名为Container View。将新UIView从对象库拖动到Counter View下面的Container View内部。

Identity Inspector中将新视图的类更改为GraphView。剩下的唯一事情就是为新的GraphView添加约束,类似于在本教程前一部分中的操作:

  • 选中GraphView后,按住Control键从中心稍微左侧(仍在视图中)进行拖动,然后从弹出菜单中选择Width
  • 同样,按住Control键在选中GraphView的情况下,从中心稍微向上(仍然在视图中)进行控制 - 拖动,然后从弹出菜单中选择Height
  • 按住Control键从视图内部向左拖动到视图外部,然后选择Center Horizontally in Container
  • 按住Control键从视图内部向上拖动到视图外部,然后选择Center Vertically in Container

Size Inspector中编辑约束常量以匹配以下内容:

你的Document Outline应该像下面这样

您需要容器视图的原因是在Counter ViewGraph View之间进行动画过渡。

转到ViewController.swift并为ContainerGraph Views添加属性outlets

@IBOutlet weak var containerView: UIView!
@IBOutlet weak var graphView: GraphView!

这为容器视图和图形视图创建了一个outlet。 现在将它们连接到您在故事板中创建的视图。

返回Main.storyboard并将Graph ViewContainer View连接到outlet


Seting up the Animated Transition - 设置动画转场

仍然在Main.storyboard中,将Tap Gesture Recognizer从对象库拖到Document Outline中的Container View

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

 
var isGraphViewShowing = false

这只是标记当前是否显示图表视图。

现在添加tap方法来进行转场:

 
@IBAction func counterViewTap(_ gesture: UITapGestureRecognizer?) {
  if (isGraphViewShowing) {
    //hide Graph
    UIView.transition(from: graphView,
                      to: counterView,
                      duration: 1.0,
                      options: [.transitionFlipFromLeft, .showHideTransitionViews],
                      completion:nil)
  } else {
    //show Graph
    UIView.transition(from: counterView,
                      to: graphView,
                      duration: 1.0,
                      options: [.transitionFlipFromRight, .showHideTransitionViews],
                      completion: nil)
  }
  isGraphViewShowing = !isGraphViewShowing
}

UIView.transition(from:to:duration:options:completion :)执行水平翻转过渡。 其他过渡是交叉溶解,垂直翻转和向上或向下卷曲。 转换使用.showHideTransitionViews常量,这意味着您不必删除视图以防止它在转换中hidden后显示。

pushButtonPressed(_ :)的末尾添加此代码:

if isGraphViewShowing {
  counterViewTap(nil)
}

如果用户在显示图表时按下加号按钮,显示屏将向后摆动以显示计数器。

最后,要使此转换工作,请返回Main.storyboard并将您的点击手势连接到新添加的counterViewTap(gesture:)方法:

构建并运行应用程序。 目前,您在启动应用时会看到图表视图。 稍后,您将隐藏图表视图,因此计数器视图将首先出现。 点按它,您将看到转换翻转。


Analysis of the Graph View - 图表分析

还记得第1部分中的Painter’s Model吗? 它解释了使用Core Graphics绘图是从图像背面到前面完成的,因此在编码之前需要记住顺序。 对于Flo的图,那将是:

  • 渐变背景视图
  • 图下的剪裁渐变
  • 图线
  • 图表的圆圈指向
  • 水平图线
  • 图表标签

Drawing a Gradient - 绘制梯度

您现在将在图表视图中绘制渐变。

转到GraphView.swift并将代码替换为:

import UIKit

@IBDesignable class GraphView: UIView {
  
  // 1
  @IBInspectable var startColor: UIColor = .red
  @IBInspectable var endColor: UIColor = .green

    override func draw(_ rect: CGRect) {
      
      // 2
      let context = UIGraphicsGetCurrentContext()!
      let colors = [startColor.cgColor, endColor.cgColor]
      
      // 3
      let colorSpace = CGColorSpaceCreateDeviceRGB()
      
      // 4
      let colorLocations: [CGFloat] = [0.0, 1.0]
      
      // 5
      let gradient = CGGradient(colorsSpace: colorSpace,
                                     colors: colors as CFArray,
                                  locations: colorLocations)!
      
      // 6
      let startPoint = CGPoint.zero
      let endPoint = CGPoint(x: 0, y: bounds.height)
      context.drawLinearGradient(gradient,
                          start: startPoint,
                            end: endPoint,
                        options: [])
    }
}

这里有几件事要做:

  • 1) 您可以将渐变的开始和结束颜色设置为@IBInspectable属性,以便您可以在故事板中更改它们。
  • 2) CG绘图函数需要知道它们将绘制的上下文,因此您使用UIKit方法UIGraphicsGetCurrentContext()来获取当前上下文。这是draw(_:)绘制的地方。
  • 3) 所有上下文都有颜色空间。这可能是CMYK或灰度,但在这里你使用的是RGB色彩空间。
  • 4) 颜色停止描述渐变中的颜色变换的位置。在这个例子中,你只有两种颜色,红色变为绿色,但你可以有一个三个数的数组,并且红色变为蓝色变为绿色。停止点位于0和1之间,其中0.33是通过渐变的三分之一。
  • 5) 创建实际渐变,定义颜色空间,颜色和颜色停止点。
  • 6) 最后,绘制渐变。 CGContextDrawLinearGradient()采用以下参数:
    • 要绘制的CGContext
    • CGGradient具有色彩空间,颜色和停止
    • 起点
    • 终点
    • 用于扩展渐变的选项标志

渐变将填充整个draw(_:)rect

设置Xcode,以便使用Assistant Editor (Show Assistant Editor…\Counterparts\Main.storyboard)对您的代码和故事板进行并排查看,您将看到渐变显示在图表视图上。

在故事板中,选择Graph View。 然后在Attributes Inspector中,将Start Color更改为RGB(250,233,222),将End Color更改为RGB(252,79,8)(单击颜色,然后单击Other\Color Sliders):

现在做一些清理工作。 在Main.storyboard中,依次选择每个视图(ViewController主视图除外),并将Background Color设置为Clear Color。 您不再需要黄色,按钮视图也应该具有透明背景。

构建并运行应用程序,您会发现图形看起来更好,或者至少是它的背景。


Clipping Areas - 剪裁区域

刚刚使用渐变时,您填充了整个视图的上下文区域。 但是,您可以创建用作剪切区域的路径,而不是用于绘制。 剪切区域允许您定义要填充的区域,而不是整个上下文。

转到GraphView.swift

首先,在GraphView的顶部添加这些常量,我们稍后将使用它们进行绘制:

private struct Constants {
  static let cornerRadiusSize = CGSize(width: 8.0, height: 8.0)
  static let margin: CGFloat = 20.0
  static let topBorder: CGFloat = 60
  static let bottomBorder: CGFloat = 50
  static let colorAlpha: CGFloat = 0.3
  static let circleDiameter: CGFloat = 5.0
}

draw(_:)的顶部添加下面代码

let path = UIBezierPath(roundedRect: rect,
                  byRoundingCorners: .allCorners,
                        cornerRadii: Constants.cornerRadiusSize)
path.addClip()

这将创建一个约束渐变的剪切区域。 您将很快使用相同的技巧在图线下绘制第二个渐变。

构建并运行应用程序,看看你的图表视图有漂亮的圆角:

注意:使用Core Graphics绘制静态视图通常足够快,但如果您的视图移动或需要频繁重绘,则应使用Core Animation层。 Core Animation经过优化,因此GPU(而不是CPU)可以处理大部分处理。相反,CPU处理Core Graphicsdraw(_ :)中执行的视图绘制。

您可以使用CALayer的cornerRadius属性创建圆角,而不是使用剪切路径,但您应该针对您的情况进行优化。


Tricky Calculations for Graph Points - 图形点的棘手计算

现在,您将从绘图中稍作休息来制作图表。你会绘制7个点,x轴将是“星期几”,y轴将是“喝的杯水的数量”。

首先,设置本周的样本数据。

仍然在GraphView.swift中,在类的顶部添加以下属性:

//Weekly sample data
var graphPoints = [4, 2, 6, 4, 5, 8, 3]

这包含代表七天的样本数据。 忽略关于将其更改为let值的警告,因为稍后我们需要将其作为var

将此代码添加到draw(_:)的顶部

let width = rect.width
let height = rect.height

并将此代码添加到draw(_:)结束

//calculate the x point
    
let margin = Constants.margin
let graphWidth = width - margin * 2 - 4
let columnXPoint = { (column: Int) -> CGFloat in
  //Calculate the gap between points
  let spacing = graphWidth / CGFloat(self.graphPoints.count - 1)
  return CGFloat(column) * spacing + margin + 2
}

x轴点由7个等间距点组成。 上面的代码是一个闭包表达式。 它可以作为函数添加,但对于像这样的小型计算,您可以将它们保持内联。

columnXPoint将列作为参数,并返回一个值,其中该点应位于x轴上。

添加代码来计算draw(_:)结束时的y轴点:

// calculate the y point
    
let topBorder = Constants.topBorder
let bottomBorder = Constants.bottomBorder
let graphHeight = height - topBorder - bottomBorder
let maxValue = graphPoints.max()!
let columnYPoint = { (graphPoint: Int) -> CGFloat in
  let y = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight
  return graphHeight + topBorder - y // Flip the graph
}

columnYPoint也是一个闭包表达式,它将星期几数组中的值作为参数。 它返回y位置,介于0和最大杯水数之间。

由于Core Graphics中的原点位于左上角,并且您从左下角的原点绘制图形,因此columnYPoint会调整其返回值,以使图形朝向您期望的方向。

继续在draw(_:)结束时添加线条绘图代码

// draw the line graph

UIColor.white.setFill()
UIColor.white.setStroke()
    
// set up the points line
let graphPath = UIBezierPath()

// go to start of line
graphPath.move(to: CGPoint(x: columnXPoint(0), y: columnYPoint(graphPoints[0])))
    
// add points for each item in the graphPoints array
// at the correct (x, y) for the point
for i in 1..<graphPoints.count {
  let nextPoint = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
  graphPath.addLine(to: nextPoint)
}

graphPath.stroke()

在此块中,您将为图形创建路径。 UIBezierPath是从graphPoints中每个元素的x和y点构建的。

故事板中的图表视图现在应如下所示:

既然您已经验证了线条的正确绘制,请从draw(_:)结束时删除它

graphPath.stroke()

这只是为了您可以查看故事板中的行并验证计算是否正确。


A Gradient Graph - 梯度图

现在,您将使用路径作为剪切路径在此路径下创建渐变。

首先在draw(_:)结束时设置剪切路径:

//Create the clipping path for the graph gradient

//1 - save the state of the context (commented out for now)
//context.saveGState()
    
//2 - make a copy of the path
let clippingPath = graphPath.copy() as! UIBezierPath
    
//3 - add lines to the copied path to complete the clip area
clippingPath.addLine(to: CGPoint(x: columnXPoint(graphPoints.count - 1), y:height))
clippingPath.addLine(to: CGPoint(x:columnXPoint(0), y:height))
clippingPath.close()
    
//4 - add the clipping path to the context
clippingPath.addClip()
    
//5 - check clipping path - temporary code
UIColor.green.setFill()
let rectPath = UIBezierPath(rect: rect)
rectPath.fill()
//end temporary code

上述代码的逐节细分:

  • 1) context.saveGState()暂时被注释掉了 - 一旦你理解了它的作用,你马上就会回到这一点。
  • 2) 将绘制的路径复制到新路径,该路径定义要用渐变填充的区域。
  • 3) 完成带角点的区域并关闭路径。 这会添加图表的右下角和左下角。
  • 4) 将剪切路径添加到上下文。 填充上下文时,实际只填充剪切的路径。
  • 5) 填充上下文。 请记住,rect是传递给draw(_ :)的上下文区域。

故事板中的图表视图现在应如下所示:

接下来,您将使用从用于背景渐变的颜色创建的渐变替换可爱的绿色。

draw(_:)结束中删除带有绿色填充的临时代码,然后添加以下代码:

let highestYPoint = columnYPoint(maxValue)
let graphStartPoint = CGPoint(x: margin, y: highestYPoint)
let graphEndPoint = CGPoint(x: margin, y: bounds.height)
        
context.drawLinearGradient(gradient, start: graphStartPoint, end: graphEndPoint, options: [])
//context.restoreGState()

在这个区块中,您会发现杯水的最大数量,并将其作为渐变的起点。

您无法像使用绿色一样填充整个rect。 渐变将从上下文的顶部而不是从图的顶部填充,并且不会显示所需的渐变。

记下注释掉的context.restoreGState() - 在绘制绘图点的圆圈后,您将删除注释。

draw(_:)结束时添加以下内容:

//draw the line on top of the clipped gradient
graphPath.lineWidth = 2.0
graphPath.stroke()

此代码绘制原始路径。

你的图表现在正在形成:


Drawing the Data Points - 绘制数据点

draw(_:)结束时,添加以下内容:

//Draw the circles on top of the graph stroke
for i in 0..<graphPoints.count {
  var point = CGPoint(x: columnXPoint(i), y: columnYPoint(graphPoints[i]))
  point.x -= Constants.circleDiameter / 2
  point.y -= Constants.circleDiameter / 2
      
  let circle = UIBezierPath(ovalIn: CGRect(origin: point, size: CGSize(width: Constants.circleDiameter, height: Constants.circleDiameter)))
  circle.fill()
}

此代码绘制绘图点并不是什么新鲜事。 它在计算的x和y点处为数组中的每个元素填充圆形路径。

嗯......但是故事板上出现的不是很好的圆形圆点!这是怎么回事?


Context States - 上下文状态

图形上下文可以保存状态。设置许多上下文属性(如填充颜色,变换矩阵,颜色空间或剪辑区域(fill color, transformation matrix, color space or clip region))时,实际上是为当前图形状态设置它们。

您可以使用context.saveGState()来保存状态,它将当前图形状态的副本推送到状态堆栈。您还可以更改上下文属性,但是当您调用context.restoreGState()时,原始状态将从堆栈中取出,并且上下文属性将还原。这就是为什么你看到了你的点的奇怪问题。

仍然在GraphView.swift中,在draw(_ :)中,取消注释在创建剪切路径之前发生的context.saveGState(),并取消注释在使用剪切路径之后发生的context.restoreGState()

通过这样做,你:

  • 1) 使用context.saveGState()将原始图形状态推送到堆栈。
  • 2) 将剪切路径添加到新的图形状态。
  • 3) 在剪切路径中绘制渐变。
  • 4) 使用context.restoreGState()恢复原始图形状态。这是您添加剪切路径之前的状态。

你的图形线和圆圈现在应该更加清晰:

draw(_:)结束时,添加代码以绘制三条水平线:

//Draw horizontal graph lines on the top of everything
let linePath = UIBezierPath()

//top line
linePath.move(to: CGPoint(x: margin, y: topBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: topBorder))

//center line
linePath.move(to: CGPoint(x: margin, y: graphHeight/2 + topBorder))
linePath.addLine(to: CGPoint(x: width - margin, y: graphHeight/2 + topBorder))

//bottom line
linePath.move(to: CGPoint(x: margin, y:height - bottomBorder))
linePath.addLine(to: CGPoint(x:  width - margin, y: height - bottomBorder))
let color = UIColor(white: 1.0, alpha: Constants.colorAlpha)
color.setStroke()
    
linePath.lineWidth = 1.0
linePath.stroke()

此代码中没有任何内容是新的。 你所做的只是移动到一个点并绘制一条水平线。


Adding the Graph Labels - 添加图形标签

现在,您将添加标签以使图形用户友好。

转到ViewController.swift并添加这些outlet属性:

//Label outlets
@IBOutlet weak var averageWaterDrunk: UILabel!
@IBOutlet weak var maxLabel: UILabel!
@IBOutlet weak var stackView: UIStackView!

这会为您想要动态更改文本的两个标签(平均水量标签,最大水量标签)以及带有日期名称标签的StackView添加outlets

现在转到Main.storyboard并将以下视图添加为图表视图的子视图:

  • 1) UILabel文字“Water Drunk”
  • 2) UILabel,文字Average
  • 3) UILabel,文本“2”,旁边是Average标签
  • 4) UILabel,文本“99”,右侧对齐图形顶部
  • 5) UILabel,文本“0”,右对齐到图形的底部
  • 6) 一个水平StackView,每周的每一天都有标签 - 每个文本的代码都会更改。 中心对齐。

按住Shift键选择所有标签,然后将字体更改为Avenir Next Condensed, Medium style

averageWaterDrunkmaxLabelstackView连接到Main.storyboard中的相应视图。 按住Control键从View Controller拖动到正确的标签,然后从弹出窗口中选择outlet

现在您已完成图形视图的设置,在Main.storyboard中选择Graph View并选中Hidden,以便在应用程序首次运行时不显示图形。

转到ViewController.swift并添加此方法以设置标签:

 
func setupGraphDisplay() {

  let maxDayIndex = stackView.arrangedSubviews.count - 1
  
  //  1 - replace last day with today's actual data
  graphView.graphPoints[graphView.graphPoints.count - 1] = counterView.counter
  //2 - indicate that the graph needs to be redrawn
  graphView.setNeedsDisplay()
  maxLabel.text = "\(graphView.graphPoints.max()!)"
    
  //  3 - calculate average from graphPoints
  let average = graphView.graphPoints.reduce(0, +) / graphView.graphPoints.count
  averageWaterDrunk.text = "\(average)"
    
  // 4 - setup date formatter and calendar
  let today = Date()
  let calendar = Calendar.current
    
  let formatter = DateFormatter()
  formatter.setLocalizedDateFormatFromTemplate("EEEEE")
  
  // 5 - set up the day name labels with correct days
  for i in 0...maxDayIndex {
    if let date = calendar.date(byAdding: .day, value: -i, to: today),
      let label = stackView.arrangedSubviews[maxDayIndex - i] as? UILabel {
      label.text = formatter.string(from: date)
    }
  }
}

这看起来有点繁琐,但需要设置日历并检索一周中的当前日期:

  • 1) 您将今天的数据设置为图形数据数组中的最后一项。在最终项目中,您将通过将其替换为60天的样本数据来扩展它,并且您将包含一个方法,该方法可以分割出最后x天的数据。数组,但这超出了本次会议的范围。
  • 2) 如果今天的数据有任何变化,请重新绘制图表。
  • 3) 在这里你使用Swift的reduce来计算本周喝的杯水量,这是一个非常有用的方法来总结数组中的所有元素。
  • 4) 此部分以一种方式设置DateFormatter,它将获得一天名称的第一个字母。
  • 5) 这个循环遍历stackView中的所有标签,我们为日期格式化程序中的每个标签设置文本。

仍然在ViewController.swift中,从counterViewTap(_ :)调用这个新方法。在条件的else部分,注释显示show graph,添加以下代码:

setupGraphDisplay()

运行该应用程序,然后单击计数器,查看效果:


Mastering the Matrix - 掌握矩阵

你的应用看起来非常好! 您在第一部分中创建的计数器视图可以进行改进,例如添加标记以指示每个要喝的杯水:

现在您已经对CG函数进行了一些实践,您将使用它们来旋转和转换绘图上下文。

请注意,这些标记从中心辐射:

除了绘制上下文之外,您还可以选择通过旋转,缩放和转换上下文的变换矩阵来操纵上下文。

起初,这看起来很令人困惑,但在你完成这些练习后,它会更有意义。 变换的顺序很重要,因此首先我将概述您将使用图表做什么。

下图是旋转上下文然后在上下文中心绘制一个矩形的结果。

在旋转上下文之前绘制黑色矩形,然后是绿色矩形,然后是红色矩形。 有两点需要注意:

  • 1) 上下文在左上角旋转(0,0)
  • 2) 矩形仍在上下文的中心绘制,但在上下文旋转后。

当您绘制计数器视图的标记时,您将首先变换上下文,然后旋转它。

在此图中,矩形标记位于上下文的最左上角。 蓝线勾勒出变换的上下文,然后上下文旋转(红色虚线)并再次变换。

当红色矩形标记最终被绘制到上下文中时,它将以一定角度出现在视图中。

旋转上下文并平移以绘制红色标记后,需要将其重置为中心,以便可以旋转上下文并再次平移以绘制绿色标记。

就像在Graph View中使用剪切路径保存上下文状态一样,每次绘制标记时,都将使用变换矩阵保存和恢复状态。

转到CounterView.swift并将此代码添加到draw(_:)结束以将标记添加到计数器:

//Counter View markers
let context = UIGraphicsGetCurrentContext()!
  
//1 - save original state
context.saveGState()
outlineColor.setFill()
    
let markerWidth: CGFloat = 5.0
let markerSize: CGFloat = 10.0

//2 - the marker rectangle positioned at the top left
let markerPath = UIBezierPath(rect: CGRect(x: -markerWidth / 2, y: 0, width: markerWidth, height: markerSize))

//3 - move top left of context to the previous center position  
context.translateBy(x: rect.width / 2, y: rect.height / 2)
    
for i in 1...Constants.numberOfGlasses {
  //4 - save the centred context
  context.saveGState()
  //5 - calculate the rotation angle
  let angle = arcLengthPerGlass * CGFloat(i) + startAngle - .pi / 2
  //rotate and translate
  context.rotate(by: angle)
  context.translateBy(x: 0, y: rect.height / 2 - markerSize)
   
  //6 - fill the marker rectangle
  markerPath.fill()
  //7 - restore the centred context for the next rotate
  context.restoreGState()
}

//8 - restore the original state in case of more painting
context.restoreGState()

这就是你刚才所做的:

  • 1) 在操作上下文的矩阵之前,您可以保存矩阵的原始状态。
  • 2) 定义路径的位置和形状 - 但您还没有绘制它。
  • 3) 移动上下文,以便在上下文的原始中心周围进行旋转。 (上图中的蓝线。)
  • 4) 对于每个标记,首先保存居中的上下文状态。
  • 5) 使用先前计算的单个角度,确定每个标记的角度并旋转和转换上下文。
  • 6) 在旋转和变换的上下文的左上角绘制标记矩形。
  • 7) 恢复居中上下文的状态。
  • 8) 恢复没有旋转或变换的上下文的原始状态。

现在构建并运行应用程序,并欣赏Flo的美丽且信息丰富的UI:

后记

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

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

推荐阅读更多精彩内容