- CoreGraphic框架解析 (六)—— 基于CoreGrap
- CoreGraphic框架解析 (五)—— 基于CoreGrap
- CoreGraphic框架解析 (七)—— 基于CoreGrap
- CoreGraphic框架解析 (八)—— 基于CoreGrap
- CoreGraphic框架解析 (二十四) —— 基于Core
- CoreGraphic框架解析 (二十五) —— 基于Core
- CoreGraphic框架解析 (二十) —— Curves a
- CoreGraphic框架解析(二)—— 基本使用
- CoreGraphic框架解析(一)—— 基本概览
- CoreGraphic框架解析 (十二)—— Shadows 和
版本记录
| 版本号 | 时间 |
|---|---|
| V1.0 | 2018.10.21 星期日 |
前言
quartz是一个通用的术语,用于描述在iOS和MAC OS X中整个媒体层用到的多种技术 包括图形、动画、音频、适配。Quart 2D是一组二维绘图和渲染API,Core Graphic会使用到这组API,Quartz Core专指Core Animation用到的动画相关的库、API和类。CoreGraphics是UIKit下的主要绘图系统,频繁的用于绘制自定义视图。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 Graphics的CGPath的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 View和Graph View之间进行动画过渡。
转到ViewController.swift并为Container和Graph Views添加属性outlets:
@IBOutlet weak var containerView: UIView!
@IBOutlet weak var graphView: GraphView!
这为容器视图和图形视图创建了一个outlet。 现在将它们连接到您在故事板中创建的视图。
返回Main.storyboard并将Graph View和Container 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 Graphics在draw(_ :)中执行的视图绘制。您可以使用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。
将averageWaterDrunk,maxLabel和stackView连接到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的一个简单绘制示例,感兴趣的给个赞或者关注~~~















网友评论