CoreGraphic框架解析 (二十二) —— Gradien

2020-07-29  本文已影响0人  刀客传奇

版本记录

版本号 时间
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),类似于在教程的上一部分中添加约束的方式:

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的图形,应为:


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: []
    )
  }
}

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

渐变将填充传递给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

在上面的代码中,您:

现在,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()

通过这样做,您:

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

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

这看起来有些笨拙,但是您需要它来设置日历并检索星期几。 为此,您:

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

setupGraphDisplay()

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


Mastering the Matrix

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

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

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

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

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

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

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

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

在上面的代码中,您:

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

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

后记

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

上一篇下一篇

猜你喜欢

热点阅读