CoreGraphic框架解析 (二十二) —— Gradients 和 Contexts的简单示例(一)

版本记录

版本号 时间
V1.0 2020.07.29 星期三

前言

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的一个简单绘制示例 (一)
6. CoreGraphic框架解析 (六)—— 基于CoreGraphic的一个简单绘制示例 (二)
7. CoreGraphic框架解析 (七)—— 基于CoreGraphic的一个简单绘制示例 (三)
8. CoreGraphic框架解析 (八)—— 基于CoreGraphic的一个简单绘制示例 (四)
9. CoreGraphic框架解析 (九)—— 一个简单小游戏 (一)
10. CoreGraphic框架解析 (十)—— 一个简单小游戏 (二)
11. CoreGraphic框架解析 (十一)—— 一个简单小游戏 (三)
12. CoreGraphic框架解析 (十二)—— Shadows 和 Gloss (一)
13. CoreGraphic框架解析 (十三)—— Shadows 和 Gloss (二)
14. CoreGraphic框架解析 (十四)—— Arcs 和 Paths (一)
15. CoreGraphic框架解析 (十五)—— Arcs 和 Paths (二)
16. CoreGraphic框架解析 (十六)—— Lines, Rectangles 和 Gradients (一)
17. CoreGraphic框架解析 (十七)—— Lines, Rectangles 和 Gradients (二)
18. CoreGraphic框架解析 (十八) —— 如何制作Glossy效果的按钮(一)
19. CoreGraphic框架解析 (十九) —— 如何制作Glossy效果的按钮(二)
20. CoreGraphic框架解析 (二十) —— Curves and Layers(一)
21. CoreGraphic框架解析 (二十一) —— Curves and Layers(二)

开始

首先看下主要内容:

在此Core Graphics教程中,学习如何开发具有先进的Core Graphics功能(例如gradients and transformations)的现代iOS应用。内容来自翻译

下面看下写作环境:

Swift 5, iOS 13, Xcode 11

接着,下面就是正文啦。

在这部分中,您将进一步研究Core Graphics,学习有关绘制渐变和通过转换操作CGContext的知识。

Core Graphics

现在,您将离开舒适的UIKit世界,进入Core Graphics的底层社会。

苹果公司的这张图片从概念上描述了相关的框架:

UIKit是顶层,也是最容易接近的。 您已使用UIBezierPath,它是Core Graphics CGPathUIKit包装。

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

关于底层Core Graphics对象和函数的一件事是,它们始终具有前缀CG,因此易于识别。

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

在图形视图上进行绘制之前,您需要在storyboard中对其进行设置,并创建使过渡动画化以显示它的代码。

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

打开起始项目,您会发现它几乎是您在上一教程中遗漏的位置。 唯一的区别是,在Main.storyboard中,CounterView在另一个带有黄色背景的视图的内部。 构建并运行,您将看到:


Creating the Graph

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

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

Identity inspector中将新视图的类更改为GraphView。剩下的唯一事情就是为新的GraphView添加约束(constraints),类似于在教程的上一部分中添加约束的方式:

  • 选中GraphView时,按住Control并拖动鼠标到中间一点,仍然在视图内,然后从弹出菜单中选择Width
  • 在仍选择GraphView的情况下,按住Control键从中心稍微向上拖动,仍然在视图内,然后从弹出菜单中选择Height
  • 按住Control键从视图内部向左拖动到视图外部,然后选择Center Horizontally in Container
  • 控制从视图内部拖动到视图外部,然后选择Center Vertically in Container

Size inspector中编辑约束常量以使其匹配:

你的Document Outline应该如下所示:

需要Container View的原因是在Counter ViewGraph View之间进行动画过渡。

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

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

这将为Container and Graph views创建一个outlet。 现在将它们连接到您在storyboard中创建的视图。

返回Main.storyboard,然后将Graph ViewContainer View连接到其相应的outlet


Setting Up the Animated Transition

仍在Main.storyboard中时,将Tap Gesture RecognizerObject Library拖动到Document Outline中的Container视图:

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

var isGraphViewShowing = false

这只是标记当前是否显示Graph View

现在添加此tap方法进行转换:

 
@IBAction func counterViewTap(_ gesture: UITapGestureRecognizer?) {
  // Hide Graph
  if isGraphViewShowing {
    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.toggle()
}

UIView.transition(from:to:duration:options:completion :)执行水平翻转过渡。 其他可用的过渡效果包括交叉溶解,垂直翻转和向上或向下卷曲(cross dissolve, vertical flip and curl up or down)。 过渡使用.showHideTransitionViews,因此您不必删除视图以防止该视图在过渡中“隐藏”后显示。

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

if isGraphViewShowing {
  counterViewTap(nil)
}

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

现在,要使此转换生效,请返回Main.storyboard并将您的点击手势连接到新添加的counterViewTap(gesture :)

构建并运行。 目前,启动应用程序时,您会看到Graph View。 稍后,您将Graph View设置为隐藏,这样计数器视图将首先出现。 点按它,您会看到翻转的过渡。

image

Analyzing the Graph View

image

还记得第1部分中的画家模型吗? 它说明您在Core Graphics中从背面到正面绘制图像。 因此,在编码之前,您需要牢记顺序。 对于Flo的图形,应为:

  • 1) Gradient background view
  • 2) Clipped gradient under the graph
  • 3) Graph line
  • 4) Circles for the graph points
  • 5) Horizontal graph lines
  • 6) Graph labels

Drawing a Gradient

现在,您将在Graph View中绘制一个渐变。

打开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
    guard let context = UIGraphicsGetCurrentContext() else {
      return
    }
    let colors = [startColor.cgColor, endColor.cgColor]
    
    // 3
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    
    // 4
    let colorLocations: [CGFloat] = [0.0, 1.0]
    
    // 5
    guard let gradient = CGGradient(
      colorsSpace: colorSpace,
      colors: colors as CFArray,
      locations: colorLocations
    ) else {
      return
    }
    
    // 6
    let startPoint = CGPoint.zero
    let endPoint = CGPoint(x: 0, y: bounds.height)
    context.drawLinearGradient(
      gradient,
      start: startPoint,
      end: endPoint,
      options: []
    )
  }
}

您需要从上面的代码中了解以下内容:

  • 1) 您需要将渐变的开始和结束颜色设置为@IBInspectable属性,以便可以在storyboard中对其进行更改。
  • 2) CG绘制函数需要知道它们将在其中绘制的上下文,因此您可以使用UIKit方法UIGraphicsGetCurrentContext()来获取当前上下文。这就是draw(_ :)绘制到的对象。
  • 3) 所有上下文都有一个色彩空间。这可能是CMYK或灰度,但是这里您使用的是RGB颜色空间。
  • 4) 色标描述渐变中的颜色在何处转换。在此示例中,您只有两种颜色,红色变为绿色,但是可能有三个停止点的数组,而红色变为蓝色而变为绿色。停止点在01之间,其中0.33是渐变的三分之一。
  • 5) 然后,您需要创建实际的渐变,定义颜色空间,颜色和色标。
  • 6) 最后,您需要绘制渐变。 drawLinearGradient(_:start:end:options :)采用以下参数:
    • 具有颜色空间,颜色和停止点的CGGradient
    • 起点
    • 终点
    • 选项标记以扩展渐变

渐变将填充传递给draw(_ :)的整个rect

打开Main.storyboard,您会看到渐变出现在Graph View中。

storyboard中,选择Graph View。 然后在Attributes inspector中,将Start Color更改为RGB(250,233,222),将End Color更改为RGB(252,79,8)。 为此,请单击颜色,然后单击Custom

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

构建并运行,您会发现该图看起来更好,或者至少它的背景看起来更好。


Clipping Areas

刚才使用渐变时,您会填充视图的整个上下文区域。 但是,如果您不想填充整个区域,则可以创建路径来裁剪绘图区域。

要查看实际效果,请转到GraphView.swift

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

private enum 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()

这将创建一个限制渐变的裁剪区域。 稍后,您将使用相同的技巧在图形线下绘制第二个渐变。

构建并运行,然后查看您的Graph View具有漂亮的圆角:

注意:使用Core Graphics绘制静态视图通常足够快,但是如果视图四处移动或需要频繁重绘,则应使用Core Animation层。 对Core Animation进行了优化,以便GPU(而不是CPU)处理大多数处理。 相比之下,CPU会在draw(_ :)中处理由Core Graphics执行的绘图。

如果您使用的是Core Animation,则将使用CALayercornerRadius属性,而不是clipping。 有关此概念的优质教程,请查看适用于iOS和Swift的自定义控件教程: Custom Control Tutorial for iOS and Swift: A Reusable Knob,您将在其中使用Core Animation创建自定义控件。


Calculating Graph Points

现在,您需要短暂的绘画时间来制作图形。 您会得到7个点; x轴为“星期几”,y轴为Number of Glasses Drunk

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

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

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

这将保存代表7天的样本数据。

将此代码添加到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轴上。

添加代码以计算y轴点到draw(_ :)的末尾:

// Calculate the y point
    
let topBorder = Constants.topBorder
let bottomBorder = Constants.bottomBorder
let graphHeight = height - topBorder - bottomBorder
guard let maxValue = graphPoints.max() else {
  return
}
let columnYPoint = { (graphPoint: Int) -> CGFloat in
  let yPoint = CGFloat(graphPoint) / CGFloat(maxValue) * graphHeight
  return graphHeight + topBorder - yPoint // 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中每个元素的xy点构建的。

storyboard中的Graph View现在应如下所示:

现在您已经验证了线条绘制正确,将其从draw(_:)的末尾删除

graphPath.stroke()

只是为了您可以在storyboard中看到该条线,并验证计算是否正确。

1. Creating the Gradient for the 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
guard let clippingPath = graphPath.copy() as? UIBezierPath else {
  return
}
    
// 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(_ :)的上下文区域。

现在,storyboard中的Graph View应如下所示:

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

用以下代码替换注释#5下的临时代码:

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()

此代码绘制了原始路径。

您的图现在已经真正成形:

2. 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()
}

在上面的代码中,您通过在计算出的xy点填充数组中每个元素的圆路径来绘制绘图点。

嗯...那些圈子是什么?他们看起来不太圆!


Considering Context States

圆怪异的原因与状态state有关。图形上下文可以保存状态。因此,当您设置许多上下文属性(例如填充颜色,转换矩阵,颜色空间或剪辑区域)时,实际上是将它们设置为当前图形状态。

您可以使用context.saveGState()保存状态,该状态将当前图形状态的副本推入状态堆栈(state stack)。您还可以更改上下文属性,但是当调用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!

这增加了用于动态更改平均喝水标签,最大喝水标签以及堆栈视图的日期名称标签的文本的outlets

现在转到Main.storyboard并添加以下视图作为Graph View的子视图:

前五个子视图是UILabel。 第四个子视图在图形的顶部旁边右对齐,第五个子视图在图形的底部右侧对齐。 第六个子视图是水平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() ?? 0)"
    
  // 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) 将今天的数据设置为图形数据数组中的最后一项。
  • 2) 重新绘制图形以说明对今天数据的任何更改。
  • 3) 使用Swiftreduce来计算一周的平均醉酒量; 这是一种将数组中所有元素求和的非常有用的方法。
  • 4) 本节将DateFormatter设置为返回每天的第一个字母。
  • 5) 此循环遍历stackView内的所有标签。 由此,您可以为date formatter中的每个标签设置文本。

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

setupGraphDisplay()

构建并运行,然后单击计数器。


Mastering the Matrix

您的应用看起来真的很锋利! 不过,您可以通过添加标记来指示要喝的每一杯来改善计数器视图:

现在,您已经对CG函数进行了一些练习,接下来将使用它们来旋转和平移图形上下文。

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

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

乍一看,这似乎令人困惑,但是在完成这些练习之后,它将变得更有意义。 transformations的顺序很重要,因此这里有一些图表来说明您将要做的事情。

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

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

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

绘制counter view的标记时,在旋转上下文之前,请先translate

在此图中,矩形标记位于上下文的最左上方。 蓝线概述了translated后的上下文。 红色虚线表示旋转。 此后,您将再次转换上下文。

将红色矩形绘制到上下文中时,将使它以一定角度出现在视图中。

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

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


Drawing the Marker

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

// Counter View markers
guard let context = UIGraphicsGetCurrentContext() else {
  return
}
  
// 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 centered 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 centered context for the next rotate
  context.restoreGState()
}

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

在上面的代码中,您:

  • 1) 操作上下文的矩阵之前,请保存矩阵的原始状态。
  • 2) 定义路径的位置和形状,尽管您尚未绘制它。
  • 3) 移动上下文,以便围绕上下文的原始中心进行旋转,如上图中的蓝线所示。
  • 4) 保存每个标记的居中上下文状态。
  • 5) 使用先前计算的单个角度确定每个标记的角度。 然后旋转并translate上下文。
  • 6) 在旋转和转换后的上下文的左上方绘制标记矩形。
  • 7) 恢复居中上下文的状态。
  • 8) 在进行任何旋转或平移之前,请还原上下文的原始状态。

干得不错。 现在,构建并运行并欣赏Flo精美而内容丰富的UI:

如果您想了解有关自定义布局的更多信息,请考虑以下资源:

后记

本篇主要讲述了GradientsContexts的简单示例,感兴趣的给个赞或者关注~~~

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