CoreGraphic框架解析 (二十) —— Curves and Layers(一)

版本记录

版本号 时间
V1.0 2019.03.09 星期六

前言

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效果的按钮(二)

开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

在本教程中,您将学习如何使用Core Graphics在屏幕上绘图。 您将学习如何绘制二次曲线和贝塞尔曲线以及将变换应用于现有形状。 最后,您将使用Core Graphics图层以詹姆斯邦德超级恶棍的轻松和风格来克隆您的绘图。

在Xcode中打开RageTweet.xcworkspace(而不是.xcodeproj!)。

构建并运行应用程序。 你应该看到以下内容:

在本教程中,您将使用风景秀丽的山地背景替换平坦的蓝色背景颜色。 随着情绪的每次变化,天空将变成不同的颜色来表示该状态。

有一个问题 - 没有源Photoshop文件。 这不是为每种情绪导出不同背景PNG文件的情况。 你会从头开始画!

这是最终输出的外观:

回到应用程序并刷过不同的面孔,准备惊讶。 触摸一张脸发送Tweet

注意:此项目使用Twitter Kit发送推文。 自iOS 11以来,通过内置社交框架对Twitter的支持已被弃用。 要了解有关如何从Social框架迁移并在您的应用程序中采用Twitter工具包的更多信息,请查看他们关于此主题的出色指南excellent guide 。 您需要在设备上测试才能发送推文,因为Twitter Kit不支持从模拟器发送推文。


Core Graphics’ Painter’s Model

在编写一个绘图命令之前,了解如何将内容绘制到屏幕上非常重要。 Core Graphics使用称为画家模型的绘图模型。 在画家的模型中,每个绘图命令都在前一个绘图命令之上。

这类似于在实际画布上绘画。

绘画时,您可能首先在画布上绘制蓝天。 当油漆完成干燥后,你可能会在天空中画一些云。 当你画云时,云层背后的原始蓝色被新鲜的白色油漆遮挡。 接下来,您可能会在云上绘制一些阴影,现在一些白色涂料被深色涂料遮挡,从而为云提供了一些定义。

这是Apple开发人员文档中的一个图像,它说明了这个想法:

总之,绘图模型确定您必须使用的绘图顺序。


An Image Is Worth a Thousand Words

是时候开始画画了。 打开SkyView.swift并在类中添加以下方法:

override func draw(_ rect: CGRect) {
  guard let context = UIGraphicsGetCurrentContext() else {
    return
  }

  let colorSpace = CGColorSpaceCreateDeviceRGB()

  //  drawSky(in: rect, context: context, colorSpace: colorSpace)
  //  drawMountains(in: rect, in: context, with: colorSpace)
  //  drawGrass(in: rect, in: context, with: colorSpace)
  //  drawFlowers(in: rect, in: context, with: colorSpace)
}

在此方法中,您将获得当前图形的上下文并为设备创建标准颜色空间。 注释代表了绘制天空,山脉,草地和鲜花的未来绘图。


Drawing the Sky

你将使用三色渐变来绘制天空。 在draw(_:)之后,添加以下代码来执行此操作:

private func drawSky(in rect: CGRect, context: CGContext, colorSpace: CGColorSpace?) {
  // 1
  context.saveGState()
  defer { context.restoreGState() }

  // 2
  let baseColor = UIColor(red: 148.0 / 255.0, green: 158.0 / 255.0, 
                          blue: 183.0 / 255.0, alpha: 1.0)
  let middleStop = UIColor(red: 127.0 / 255.0, green: 138.0 / 255.0, 
                           blue: 166.0 / 255.0, alpha: 1.0)
  let farStop = UIColor(red: 96.0 / 255.0, green: 111.0 / 255.0, 
                        blue: 144.0 / 255.0, alpha: 1.0)

  let gradientColors = [baseColor.cgColor, middleStop.cgColor, farStop.cgColor]
  let locations: [CGFloat] = [0.0, 0.1, 0.25]

  guard let gradient = CGGradient(
    colorsSpace: colorSpace, 
    colors: gradientColors as CFArray, 
    locations: locations) 
    else {
      return
  }

  // 3
  let startPoint = CGPoint(x: rect.size.height / 2, y: 0)
  let endPoint = CGPoint(x: rect.size.height / 2, y: rect.size.width)
  context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
}

这是代码的作用:

  • 1) 首先,保存图形状态。 当你要做一些绘画时,你应该总是这样做。 还要确保在完成绘图后恢复状态。 当方法通过使用defer退出时,您可以执行此操作。
  • 2) 设置颜色,位置,最后设置实际渐变本身。
  • 3) 绘制渐变。

接下来,在draw(_ :)中,取消对drawSky(in:context:colorSpace:)的调用的注释。

构建并运行应用程序。 现在您应该看到以下内容:

您会注意到实际渐变发生在矩形顶部附近,而不是均匀地应用于整个矩形。

这是绘图的实际天空部分。 随后的绘图调用会使下半部分变得模糊。

随着天空的完整,现在是时候画山了。


Getting Comfortable with Curves

看一下源图并观察山脉。 虽然通过绘制一系列弧可以产生相同的效果,但更简单的方法是使用曲线。

Core Graphics中有两种曲线。 一个被称为二次曲线(Quadratic Curve),它的大哥被称为贝塞尔曲线(Bezier Curve)。 这些曲线基于数学原理,无论分辨率如何,都可以无限扩展。

如果查看该图表会使您的胃扭曲成结,请深呼吸,然后再来一杯威士忌。

现在再看一遍。感觉好多了?没有?行。再看一遍并意识到这一点......你不需要知道任何数学概念来画这些野兽。

实际上,这些曲线实际上很容易绘制。像任何一条线一样,你首先需要知道一个起点和一个终点。然后,添加一个控制点。

控制点基本上决定了线的曲线。将控制点放置在线越近,曲线越不显着。通过将控制点放置在远离线的位置,曲线越明显。将控制点视为将磁力线拉向它的小磁铁。

在实际上,二次曲线和贝塞尔曲线之间的主要区别在于控制点的数量。二次曲线有一个控制点。贝塞尔曲线有两个。就是这样。

注意:另一个直观理解控制点如何影响Bezier曲线的好方法是花一些时间在http://cubic-bezier.com上。

SkyView.swift中,就在 drawSky(in:context:colorSpace:)下面,添加这个新方法:

private func drawMountains(in rect: CGRect, in context: CGContext, 
                   with colorSpace: CGColorSpace?) {
  let darkColor = UIColor(red: 1.0 / 255.0, green: 93.0 / 255.0, 
                          blue: 67.0 / 255.0, alpha: 1)
  let lightColor = UIColor(red: 63.0 / 255.0, green: 109.0 / 255.0, 
                           blue: 79.0 / 255.0, alpha: 1)
  let rectWidth = rect.size.width

  let mountainColors = [darkColor.cgColor, lightColor.cgColor]
  let mountainLocations: [CGFloat] = [0.1, 0.2]
  guard let mountainGrad = CGGradient.init(colorsSpace: colorSpace, 
        colors: mountainColors as CFArray, locations: mountainLocations) else {
    return
  }

  let mountainStart = CGPoint(x: rect.size.height / 2, y: 100)
  let mountainEnd = CGPoint(x: rect.size.height / 2, y: rect.size.width)

  context.saveGState()
  defer { context.restoreGState() }

  // More coming 1...
}

此代码设置了该方法的基础,很快就会有更多代码。 它会创建一些您稍后将使用的颜色和点。

从源图中可以看出,山脉开始呈深绿色,巧妙地变为浅棕色。 现在,是时候绘制实际曲线了。 你将从二次曲线开始。

在同一方法中,使用以下内容替换// More coming 1...

let backgroundMountains = CGMutablePath()
backgroundMountains.move(to: CGPoint(x: -5, y: 157), transform: .identity)
backgroundMountains.addQuadCurve(to: CGPoint(x: 77, y: 157), 
                                 control: CGPoint(x: 30, y: 129), 
                                 transform: .identity)

// More coming 2...

你在这里做的第一件事就是创建一个路径对象。 您将一条二次曲线添加到路径中。 move(to:transform :)调用设置了该行的起点。 下一步是所有魔法发生的地方。

backgroundMountains.addQuadCurve(to: CGPoint(x: 77, y: 157), 
  control: CGPoint(x: 30, y: 129), transform: .identity)
  • 第一个参数是CGPoint,其值x和y,分别为77和157。 这表示该行结束的位置。
  • 下一个参数是一个CGPoint,其值x和y,值为30和129。 这表示控制点的位置。
  • 最后一个参数是仿射变换(affine transform)。 例如,如果要应用旋转或缩放曲线,则可以在此处提供变换。 您稍后将使用此类转换。

简而言之,您现在拥有二次曲线。

要查看此操作,请使用以下内容替换// More coming 2 ...

// Background Mountain Stroking
context.addPath(backgroundMountains)
context.setStrokeColor(UIColor.black.cgColor)
context.strokePath()

// More coming 3...

这将在图形上下文中绘制黑色路径。

接下来,在draw(_ :)中,取消drawMountains(in:in:with:)调用的注释。

现在,构建并运行应用程序。 你应该看到以下内容:

你在这里创造了一条漂亮的小曲线。 这不是蒙娜丽莎,但它是一个开始。

现在是时候解决Bezier曲线了。 回到drawMountains(in:in:with :),在backgroundMountains.addQuadCurve ...下面,添加以下内容:

backgroundMountains.addCurve(to: CGPoint(x: 303, y: 125), 
                             control1: CGPoint(x: 190, y: 210), 
                             control2: CGPoint(x: 200, y: 70), 
                             transform: .identity)

此调用与上一次调用之间的最大区别是为下一个控制点添加了另一组x和y点。 这个是贝塞尔曲线而不是二次曲线。

  • 第一个参数是CGPoint,其值x和y,值为303和125。 这表示该行的结束。
  • 第二个参数是CGPoint,其值x和y,值为190和210。 这表示第一个控制点的位置。
  • 第三个参数是CGPoint,其值x和y,值为200和70。 这表示第二个控制点的位置。
  • 和之前一样,backgroundMountains是一个CGPath,你正在应用identity transform

构建并运行应用程序,您现在应该看到以下内容:

关于曲线要记住的关键是,使用它们越多,就越容易确定所需曲线的控制点的位置。

现在是时候完成第一组山。 在刚刚添加的行下添加以下代码:

backgroundMountains.addQuadCurve(to: CGPoint(x: 350, y: 150), 
                                 control: CGPoint(x: 340, y: 150), 
                                 transform: .identity)
backgroundMountains.addQuadCurve(to: CGPoint(x: 410, y: 145), 
                                 control: CGPoint(x: 380, y: 155), 
                                 transform: .identity)
backgroundMountains.addCurve(to: CGPoint(x: rectWidth, y: 165), 
                             control1: CGPoint(x: rectWidth - 90, y: 100), 
                             control2: CGPoint(x: rectWidth - 50, y: 190), 
                             transform: .identity)
backgroundMountains.addLine(to: CGPoint(x: rectWidth - 10, y: rect.size.width),
                            transform: .identity)
backgroundMountains.addLine(to: CGPoint(x: -5, y: rect.size.width), 
                            transform: .identity)
backgroundMountains.closeSubpath()

// Background Mountain Drawing
context.addPath(backgroundMountains)
context.clip()
context.drawLinearGradient(mountainGrad, start: mountainStart, 
                           end: mountainEnd, options: [])
context.setLineWidth(4)

这样可以在山上完成一些延伸超出设备长度的曲线。 它还增加了山脉的渐变图。

构建并运行应用程序。 它应该如下所示:

现在,添加一些前景山。 使用以下代码替换// More coming 3...

// Foreground Mountains
let foregroundMountains = CGMutablePath()
foregroundMountains.move(to: CGPoint(x: -5, y: 190), 
                         transform: .identity)
foregroundMountains.addCurve(to: CGPoint(x: 303, y: 190), 
                             control1: CGPoint(x: 160, y: 250), 
                             control2: CGPoint(x: 200, y: 140), 
                             transform: .identity)
foregroundMountains.addCurve(to: CGPoint(x: rectWidth, y: 210), 
                             control1: CGPoint(x: rectWidth - 150, y: 250), 
                             control2: CGPoint(x: rectWidth - 50, y: 170), 
                             transform: .identity)
foregroundMountains.addLine(to: CGPoint(x: rectWidth, y: 230), 
                            transform: .identity)
foregroundMountains.addCurve(to: CGPoint(x: -5, y: 225), 
                             control1: CGPoint(x: 300, y: 260), 
                             control2: CGPoint(x: 140, y: 215), 
                             transform: .identity)
foregroundMountains.closeSubpath()

// Foreground Mountain drawing
context.addPath(foregroundMountains)
context.clip()
context.setFillColor(darkColor.cgColor)
context.fill(CGRect(x: 0, y: 170, width: rectWidth, height: 90))

// Foreground Mountain stroking
context.addPath(foregroundMountains)
context.setStrokeColor(UIColor.black.cgColor)
context.strokePath()

这会在您刚刚添加的山峰之前增加一些山脉。

现在构建并运行应用程序。 你应该看到以下内容:

只需几条曲线和渐变线,您就可以构建一个漂亮的背景!


Drawing the Grass

添加草使用您刚学到的所有东西的组合。

SkyView.swift中,在drawMountains(in:in:with:)下面添加以下方法:

private func drawGrass(in rect: CGRect, in context: CGContext, 
                       with colorSpace: CGColorSpace?) {
  // 1
  context.saveGState()
  defer { context.restoreGState() }

  // 2
  let grassStart = CGPoint(x: rect.size.height / 2, y: 100)
  let grassEnd = CGPoint(x: rect.size.height / 2, y: rect.size.width)
  let rectWidth = rect.size.width

  let grass = CGMutablePath()
  grass.move(to: CGPoint(x: rectWidth, y: 230), transform: .identity)
  grass.addCurve(to: CGPoint(x: 0, y: 225), control1: CGPoint(x: 300, y: 260), 
                 control2: CGPoint(x: 140, y: 215), 
                 transform: .identity)
  grass.addLine(to: CGPoint(x: 0, y: rect.size.width), 
                transform: .identity)
  grass.addLine(to: CGPoint(x: rectWidth, y: rect.size.width), 
                transform: .identity)

  context.addPath(grass)
  context.clip()

  // 3
  let lightGreen = UIColor(red: 39.0 / 255.0, green: 171.0 / 255.0, 
                           blue: 95.0 / 255.0, alpha: 1)

  let darkGreen = UIColor(red: 0.0 / 255.0, green: 134.0 / 255.0, 
                          blue: 61.0 / 255.0, alpha: 1)

  let grassColors = [lightGreen.cgColor, darkGreen.cgColor]
  let grassLocations: [CGFloat] = [0.3, 0.4]
  if 
    let grassGrad = CGGradient.init(colorsSpace: colorSpace, 
    colors: grassColors as CFArray, locations: grassLocations) {
      context.drawLinearGradient(grassGrad, start: grassStart, 
                                 end: grassEnd, options: [])
  }
}

这是代码的作用:

  • 1) 像往常一样,保存图形状态并确保在函数结束时恢复它。
  • 2) 这将设置剪切后续渐变的路径。 这是为了保持草的梯度限制在屏幕的底部。
  • 3) 这绘制了从可爱的浅绿色到深绿色的渐变。

要查看它的实际效果,请在draw(_ :)中取消注释drawGrass(in:in:with :)

现在构建并运行,它应该如下所示:


Affable Affine Transforms

这个过程的下一步是在草地上添加一些花。

仔细查看源图像。不要看三朵花,只需挑选一朵,仔细看看它是如何绘制的。你会看到每朵花都是由不同的圆圈组成 - 一个用于中心,五个用于花瓣。小曲线代表茎。

画圆圈没问题。有一个名为addEllipse(in :)的方法。您需要做的就是定义一个CGRect,这个方法将在它的中心绘制一个椭圆。

当然,有一个问题。 CGRects只能是垂直或水平的。如果你想要以40度的角度绘制椭圆怎么办?

引入affine transforms。仿射变换修改坐标系,同时仍保持点,线和形状。这些数学函数允许您旋转,缩放,移动甚至组合对象。

由于您想要旋转对象,因此您需要使用CGAffineTransform(rotationAngle :)。这是你怎么称呼它:

CGAffineTransform(rotationAngle: radians) 

弧度只是角度的度量。 由于大多数人都考虑度数而不是弧度,因此简单的辅助方法可以使这个函数调用更容易使用。

draw(_:)之前添加以下内容:

private func degreesToRadians(_ degrees: CGFloat) -> CGFloat {
  return CGFloat.pi * degrees/180.0
}

此方法只是将值从度数转换为弧度。

现在,旋转CGRect只是提供角度的问题。 例如,如果要旋转45度,请使用以下转换:

let transform = CGAffineTransform(rotationAngle: degreesToRadians(45))

很简单,嗯? 不幸的是,还有另一个问题。 旋转路径可能有点令人沮丧。

通常,您需要围绕特定点旋转路径。 由于路径只是一个点的集合,所以没有中心位置 - 只是原点。 因此,当您旋转椭圆时,它将显示在与您开始的位置不同的x和y位置。

要使其工作,您必须重置原点,旋转路径,然后恢复上一个点。 而不是在一个方法中完成所有这些,创建一个绘制每个花瓣的新方法。 在drawGrass(in:in:with :)之后,添加这个新方法:

private func drawPetal(in rect: CGRect, inDegrees degrees: Int, 
                       inContext context: CGContext) {
  // 1
  context.saveGState()
  defer { context.restoreGState() }

  // 2
  let midX = rect.midX
  let midY = rect.midY
  let transform = CGAffineTransform(translationX: -midX, y: -midY)
    .concatenating(CGAffineTransform(rotationAngle: degreesToRadians(CGFloat(degrees))))
    .concatenating(CGAffineTransform(translationX: midX, y: midY))

  // 3
  let flowerPetal = CGMutablePath()
  flowerPetal.addEllipse(in: rect, transform: transform)
  context.addPath(flowerPetal)
  context.setStrokeColor(UIColor.black.cgColor)
  context.strokePath()
  context.setFillColor(UIColor.white.cgColor)
  context.addPath(flowerPetal)
  context.fillPath()
}

这是代码的作用:

  • 1) 这是非常标准的。 保存图形状态,然后再恢复。
  • 2) 创建一个首先偏移宽度的一半和高度的一半的变换。 然后旋转。 然后偏移原始量。 这相当于围绕中心的旋转。
  • 3) 绘制填充CGRect的椭圆,并通过上面创建的旋转进行变换。

创造一朵花应该相当容易。 在drawPetal(in:inDegrees:inContext:)之后添加此方法:

private func drawFlowers(in rect: CGRect, in context: CGContext, 
                         with colorSpace: CGColorSpace?) {
  // 1
  context.saveGState()
  defer { context.restoreGState() }

  // 2
  drawPetal(in: CGRect(x: 125, y: 230, width: 9, height: 14), 
            inDegrees: 0, inContext: context)
  drawPetal(in: CGRect(x: 115, y: 236, width: 10, height: 12), 
            inDegrees: 300, inContext: context)
  drawPetal(in: CGRect(x: 120, y: 246, width: 9, height: 14), 
            inDegrees: 5, inContext: context)
  drawPetal(in: CGRect(x: 128, y: 246, width: 9, height: 14), 
            inDegrees: 350, inContext: context)
  drawPetal(in: CGRect(x: 133, y: 236, width: 11, height: 14), 
            inDegrees: 80, inContext: context)

  // 3
  let center = CGMutablePath()
  let ellipse = CGRect(x: 126, y: 242, width: 6, height: 6)
  center.addEllipse(in: ellipse, transform: .identity)

  let orangeColor = UIColor(red: 255 / 255.0, green: 174 / 255.0, 
                            blue: 49.0 / 255.0, alpha: 1.0)

  context.addPath(center)
  context.setStrokeColor(UIColor.black.cgColor)
  context.strokePath()
  context.setFillColor(orangeColor.cgColor)
  context.addPath(center)
  context.fillPath()

  // 4
  context.move(to: CGPoint(x: 135, y: 249))
  context.setStrokeColor(UIColor.black.cgColor)
  context.addQuadCurve(to: CGPoint(x: 133, y: 270), control: CGPoint(x: 145, y: 250))
  context.strokePath()
}

此代码执行以下操作:

  • 1) 保存图形状态。
  • 2) 使用刚刚创建的方法绘制5个花瓣。
  • 3) 在花的中间画一个橙色圆圈。
  • 4) 使用单个二次曲线绘制茎。

现在,在draw(_ :)中取消注释drawFlowers(in:in:with:)

建立并运行。 你现在应该在山下看到一朵漂亮的花。


Attack of the Clones

绘制下两朵花应该是相对容易的事情,但Core Graphics提供了一种使其更容易的方法。您可以简单地克隆现有的花并制作它们的一个区域,而不是计算两个新花的测量值。

注意:为了使这个更好,你可以做几个花的排列,并在创建你的领域时随机选择花。这将使该领域具有多样化和有机的感觉。

Core Graphics允许您通过CGLayer对象制作图纸副本。您可以绘制到图层上下文,而不是绘制到主图形上下文。完成绘图到CGLayer后,它就像一个工厂,抽出每张图纸的副本。图纸被缓存,比使用常规绘图调用更快。

使用CGLayer的一个很好的例子是美国国旗。国旗包含蓝色背景下的五十颗星。虽然您可以一次循环绘制一个星的绘图说明,但更快的方法是将星形绘制到CGLayer,然后复制该星。

用以下内容替换drawFlowers(in:in:with :)

private func drawFlowers(in rect: CGRect, in context: CGContext, 
                         with colorSpace: CGColorSpace?) {
  context.saveGState()
  defer { context.restoreGState() }

  // 1
  let flowerSize = CGSize(width: 300, height: 300)

  // 2
  guard let flowerLayer = CGLayer(context, size: flowerSize, 
                                  auxiliaryInfo: nil) else {
    return
  }

  // 3
  guard let flowerContext = flowerLayer.context else {
    return
  }

  // Draw petals of the flower
  drawPetal(in: CGRect(x: 125, y: 230, width: 9, height: 14), inDegrees: 0, 
            inContext: flowerContext)
  drawPetal(in: CGRect(x: 115, y: 236, width: 10, height: 12), inDegrees: 300, 
            inContext: flowerContext)
  drawPetal(in: CGRect(x: 120, y: 246, width: 9, height: 14), inDegrees: 5, 
            inContext: flowerContext)
  drawPetal(in: CGRect(x: 128, y: 246, width: 9, height: 14), inDegrees: 350, 
            inContext: flowerContext)
  drawPetal(in: CGRect(x: 133, y: 236, width: 11, height: 14), inDegrees: 80, 
            inContext: flowerContext)

  let center = CGMutablePath()
  let ellipse = CGRect(x: 126, y: 242, width: 6, height: 6)
  center.addEllipse(in: ellipse, transform: .identity)

  let orangeColor = UIColor(red: 255 / 255.0, green: 174 / 255.0, 
                            blue: 49.0 / 255.0, alpha: 1.0)

  flowerContext.addPath(center)
  flowerContext.setStrokeColor(UIColor.black.cgColor)
  flowerContext.strokePath()
  flowerContext.setFillColor(orangeColor.cgColor)
  flowerContext.addPath(center)
  flowerContext.fillPath()

  flowerContext.move(to: CGPoint(x: 135, y: 249))
  context.setStrokeColor(UIColor.black.cgColor)
  flowerContext.addQuadCurve(to: CGPoint(x: 133, y: 270), 
                             control: CGPoint(x: 145, y: 250))
  flowerContext.strokePath()
}

这是如何工作的:

  • 1) 设置要绘制的对象的大小。
  • 2) 通过传递当前图形上下文来创建新图层。
  • 3) 提取图层的图形上下文。 从这一点开始,您
    将绘制到图层的上下文而不是主图形上下文。

花完成后,唯一剩下的就是打印副本。

现在在函数末尾添加以下内容:

// Draw clones
context.draw(flowerLayer, at: CGPoint(x: 0, y: 0))
context.translateBy(x: 20, y: 10)
context.draw(flowerLayer, at: CGPoint(x: 0, y: 0))
context.translateBy(x: -30, y: 5)
context.draw(flowerLayer, at: CGPoint(x: 0, y: 0))
context.translateBy(x: -20, y: -10)
context.draw(flowerLayer, at: CGPoint(x: 0, y: 0))

这在不同的点上绘制了4个花的克隆。

构建并运行,您应该看到以下内容:


Finishing the App

恭喜! 你做到了这么远! 现在是时候为这个未来的畅销品添加最后润色。

对于情绪的每一次变化,天空都应该反映出这种状态。 滚动视图上的每次滑动都会重置SkyView中的愤怒级别。 进行以下更改以反映这种内部动荡。

用以下内容替换drawSky(in:context:colorSpace :)

private func drawSky(in rect: CGRect, rageLevel: RageLevel, context: CGContext, 
                     colorSpace: CGColorSpace) {
  let baseColor: UIColor
  let middleStop: UIColor
  let farStop: UIColor

  switch rageLevel {
  case .happy:
    baseColor = UIColor(red: 0 / 255.0, green: 158.0 / 255.0, 
                        blue: 183.0 / 255.0, alpha: 1.0)
    middleStop = UIColor(red: 0.0 / 255.0, green: 255.0 / 255.0, 
                         blue: 252.0 / 255.0, alpha: 1.0)
    farStop = UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, 
                      blue: 255.0 / 255.0, alpha: 1.0)
  case .somewhatHappy:
    baseColor = UIColor(red: 0 / 255.0, green: 158.0 / 255.0, 
                        blue: 183.0 / 255.0, alpha: 1.0)
    middleStop = UIColor(red: 144.0 / 255.0, green: 152.0 / 255.0, 
                         blue: 253.0 / 255.0, alpha: 1.0)
    farStop = UIColor(red: 96.0 / 255.0, green: 111.0 / 255.0, 
                      blue: 144.0 / 255.0, alpha: 1.0)
  case .neutral:
    baseColor = UIColor(red: 148.0 / 255.0, green: 158.0 / 255.0, 
                        blue: 183.0 / 255.0, alpha: 1.0)
    middleStop = UIColor(red: 127.0 / 255.0, green: 138.0 / 255.0, 
                         blue: 166.0 / 255.0, alpha: 1.0)
    farStop = UIColor(red: 96.0 / 255.0, green: 111.0 / 255.0, 
                      blue: 144.0 / 255.0, alpha: 1.0)
  case .somewhatAngry:
    baseColor = UIColor(red: 255.0 / 255.0, green: 147.0 / 255.0, 
                        blue: 167.0 / 255.0, alpha: 1.0)
    middleStop = UIColor(red: 127.0 / 255.0, green: 138.0 / 255.0, 
                         blue: 166.0 / 255.0, alpha: 1.0)
    farStop = UIColor(red: 107.0 / 255.0, green: 107.0 / 255.0, 
                      blue: 107.0 / 255.0, alpha: 1.0)
  case .angry:
    baseColor = UIColor(red: 255.0 / 255.0, green: 0 / 255.0, 
                        blue: 0 / 255.0, alpha: 1.0)
    middleStop = UIColor(red: 140.0 / 255.0, green: 33.0 / 255.0, 
                         blue: 33.0 / 255.0, alpha: 1.0)
    farStop = UIColor(red: 0 / 255.0, green: 0 / 255.0, 
                      blue: 0 / 255.0, alpha: 1.0)
  }

  context.saveGState()
  defer { context.restoreGState() }

  let gradientColors = [baseColor.cgColor, middleStop.cgColor, farStop.cgColor]
  let locations: [CGFloat] = [0.0, 0.1, 0.25]

  let startPoint = CGPoint(x: rect.size.height/2, y: 0)
  let endPoint = CGPoint(x: rect.size.height/2, y: rect.size.width)

  if let gradient = CGGradient.init(colorsSpace: colorSpace, 
                                    colors: gradientColors as CFArray, 
                                    locations: locations) {
    context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
  }
}

这只是在函数中添加rageLevel:参数,然后根据该参数值更改渐变的颜色。

接下来,使用以下内容替换draw(_ :)drawSky(in:context:colorSpace :)的调用:

drawSky(in: rect, rageLevel: rageLevel, context: context, colorSpace: colorSpace)

您现在将rageLevel传递给drawSky(in:rageLevel:context:colorSpace:)

构建并运行,然后滑过不同的面。

如果您有兴趣了解有关UIKit绘图系统的更多信息,请浏览Apple’s excellent UIKIT Drawing System resource

后记

本篇主要讲述了Curves and Layers,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容