首页 iOS.& Swift Tutorials

核心图形教程:渐变和上下文

在此核心图形教程中,了解如何开发具有高级核心图形的现代iOS应用程序,如梯度和转换。

5/5 7评级

版本

  • 迅速 5,iOS 13,Xcode 11
更新注释:Fabrizio Brancati更新了iOS 13,Swift 5和Xcode 11. Caroline Begbie写了原始的和Andrew Kharchyshyn先前更新。

欢迎回到现代核心图形教程系列!

核心图形教程:入门,您了解了具有核心图形的绘制线和弧,并使用Xcode的交互式故事板功能。

在 this second part, you’ll delve further into Core Graphics, learning about drawing gradients and manipulating CGContexts with transformations.

核心图形

你现在要离开舒适的Uikit世界,并进入核心图形的黑社会。

来自Apple的图像概念上描述了相关框架:

图表显示应用程序及其框架包含图层的图表

UIKit is the top layer, and it’s also the most approachable. You’ve used UIBezierPath, which is a UIKit wrapper of the Core Graphics CGPath.

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

One thing to know about lower layer Core Graphics objects and functions is that they always have the prefix CG, so they are easy to recognize.

入门

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

图表显示一周内消耗的杯子的数量

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

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

最终视图层次结构

首先,点击下载项目资料 下载材料 本教程顶部或底部的按钮。当你打开它时,你会看到它在上一个教程中你已经离开的地方。唯一的区别是 main.storyboard., CounterView is inside of another view with a yellow background. Build and run, and this is what you’ll see:

开始视图显示柜台与加号和减号按钮

创建图形

文件▸新▸文件..., 选择 iOS▸源▸可可触摸课 模板并点击 下一页。输入名称 GraphView. 作为类名,选择子类 UIView. 并设置语言 迅速。点击 下一页 然后 创建.

现在 main.storyboard. 单击黄色视图的名称 文件大纲 然后按ENTER重命名。称它为 集装箱视图. Drag a new UIView. 来自 object library inside of 集装箱视图, 以下 柜台视图.

将新视图的类更改为 GraphView. 在里面 身份检查员. The only thing left is to add constraints for the new GraphView., similar to how you added constraints in the previous part of the tutorial:

  • 与之 GraphView. 选, 控制阻力 从中心略微留下,仍在查看中,选择 宽度 从弹出菜单中。
  • 与之 GraphView. 仍然选择, 控制阻力 从中心略微上升,仍然在视图中,选择 高度 从弹出菜单中。
  • 控制阻力 从视图外部留下视图并选择 中心水平在容器中 .
  • 控制阻力 从视图外面的视图中,选择 垂直在容器中.

编辑约束常量 尺寸检查员 到 match these:

尺寸检查员显示所需的约束

你的 文件大纲 应该如下所示:

文档概述显示图形视图应该如何适应

您需要容器视图的原因是在计数器视图和图形视图之间进行动画转换。

ViewController.swift. 并为容器和图形视图添加属性插座:

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

这为容器和图形视图创建了一个插座。现在将它们挂钩到故事板中创建的视图。

回到 main.storyboard. 并挂钩 图形视图集装箱视图 到他们的相应网点:

连接ContainerAview和GraphView插座

设置动画过渡

仍然在 main.storyboard.,拖动A. 点击手势识别器 来自 对象库 到了 集装箱视图 在里面 文件大纲:

添加轻敲手势识别器

接下来,去 ViewController.swift. 并将此属性添加到课堂顶部:

 
var isGraphViewShowing = false

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

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

 
@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:) performs a horizontal flip transition. Other available transitions are cross dissolve, vertical flip and curl up or down. The transition uses .showHideTransitionViews so that you don’t have to remove the view to prevent it from being shown once it is “hidden” in the transition.

Add this code at the end of pushButtonPressed(_:):

 
if isGraphViewShowing {
  counterViewTap(nil)
}

如果用户按下图表显示的同时按下加号,则显示屏将返回以显示计数器。

现在,为了让这个过渡工作,回去 main.storyboard. and hook up your tap gesture to the newly added counterViewTap(gesture:):

连接龙头手势识别器

建立和运行。目前,您将在启动应用程序时看到图形视图。稍后,您将设置隐藏的图形视图,因此将首先显示计数器视图。点击它,您将看到翻转过渡。

摇动过渡正在进行的静态图片

分析图形视图

注释的graphView以说明以下分析

记住画家从第1部分的模型?它解释说,您将图像从核心图形中绘制到前面。所以你在代码之前需要订单。对于flo的图表,这将是:

  1. 梯度背景视图
  2. 在图表下剪裁梯度
  3. 图表行
  4. 图表点的圆圈
  5. 水平图线
  6. 图标签标签

绘制渐变

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

打开 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. You need to set the start and end colors for the gradient as @IBInspectable properties so that you’ll be able to change them in the storyboard.
  2. CG drawing functions need to know the context in which they will draw, so you use the UIKit method UIGraphicsGetCurrentContext() 到 obtain the current context. That’s the one that draw(_:) draws into.
  3. 所有上下文都有颜色空间。这可能是CMYK或灰度,但在这里您正在使用RGB颜色空间。
  4. 颜色停止描述梯度变化的颜色在哪里。在这个例子中,你只有两种颜色,红色进入绿色,但你可以有一系列三个站点,并有红色的蓝色去绿色。止挡介于0和1之间,其中0.33是通过梯度的三分之一。
  5. 然后,您需要创建实际渐变,定义颜色空间,颜色和颜色停止。
  6. Finally, you need to draw the gradient. drawLinearGradient(_:start:end:options:) takes the following parameters:
    • The CGGradient with color space, colors and stops
    • 开始点
    • 结束点
    • 选项标志以扩展渐变

The gradient will fill the entire rect passed to draw(_:).

打开 main.storyboard. 并且您将看到渐变显示在图形视图上。

初始红色到绿色梯度在故事板上显示

在故事板中,选择 图形视图。然后在这一边 属性检查员, 改变 开始颜色RGB(250,233,222), 和 末端颜色RGB(252,79,8)。为此,请单击颜色,然后单击颜色 风俗:

渐变修改为使用自定义颜色

现在有一些清理工作。在 main.storyboard.,依次选择每个视图,除了主视图,并设置 背景颜色清晰的颜色。您不再需要黄色,并且按钮视图应该具有透明背景。

构建和运行,您将注意到图表看起来很多,或者至少是其背景。 :]

应用与干净的图表背景视图

剪裁区域

当您刚才使用梯度时,您填充了整个视图的上下文区域。但是,如果您不想填充整个区域,则可以创建剪辑绘图区域的路径。

在行动中看到这一点,去 graphview.swift..

First, add these constants at the top of GraphView., which you’ll use for drawing later:

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
}

Now add this code to the top of draw(_:):

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

这将创建一个限制梯度的剪切区域。稍后将使用相同的技巧在图表行下绘制第二个渐变。

构建和运行,看看您的图形视图具有漂亮,圆角:

与圆角的图表背景

笔记: Drawing static views with Core Graphics is generally quick enough, but if your views move around or need frequent redrawing, you should use Core Animation layers. Core Animation is optimized so that the GPU, not the CPU, handles most of the processing. In contrast, the CPU processes view drawing performed by Core Graphics in draw(_:).

If you use Core Animation, you’ll use CALayer’s cornerRadius property instead of clipping. For a good tutorial on this concept, check out iOS和swift的自定义控制教程:可重用的旋钮,在那里您将使用核心动画来创建自定义控件。

计算图表点

现在你将从绘图中短暂休息以制作图表。你会绘制7分; X轴将是“一周中的一天”,而Y轴将是“醉酒的玻璃数”。

首先,设置本周的示例数据。

还在 graphview.swift.,在课堂顶部,添加此属性:

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

这保持了七天的示例数据。

Add this code to the top of the draw(_:):

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

And add this code to the end of 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轴上应位于应位的值。

Add the code to calculate the y-axis points to the end of 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和最大数量的眼镜之间喝醉。

Because the origin in Core Graphics is in the top-left corner and you draw a graph from an origin point in the bottom-left corner, columnYPoint adjusts its return value so that the graph is oriented as you would expect.

Continue by adding line drawing code to the end of 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()

在 this block, you create the path for the graph. The UIBezierPath is built from the x and y points for each element in graphPoints.

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

显示白色线的图表在梯度背景

Now that you verified the line draws correctly, remove this from the end of draw(_:):

graphPath.stroke()

这只是让您可以查看故事板中的线路并验证计算是否正确。

为图创建渐变

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

First set up the clipping path at the end of 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. Commented out context.saveGState() for now. You'll come back to this in a moment once you understand what it does.
  2. 将绘制的路径复制到定义要填充渐变区域的新路径。
  3. 使用角点完成区域并关闭路径。这增加了图形的右下角和左下角。
  4. 将剪切路径添加到上下文。填充上下文时,仅填充剪切路径。
  5. Fill the context. Remember that rect is the area of the context that was passed to draw(_:).

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

与丑陋的绿色线下的区域的图表

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

使用此代码替换注释#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()

在此块中,您发现最多的眼镜醉酒并使用它作为梯度的起点。

You can't fill the whole rect the same way you did with the green color. The gradient would fill from the top of the context instead of from the top of the graph, and the desired gradient wouldn't show up.

Take note of the commented out context.restoreGState(); you'll remove the comments after you draw the circles for the plot points.

At the end of draw(_:), add this:

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

此代码绘制原始路径。

你的图表现在真的在塑造:

在该线下的区域中具有更漂亮渐变的图表

绘制数据点

At the end of draw(_:), add the following:

// 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点处为阵列中的每个元素填充圆形路径来绘制绘图点。

与平圆圈的图表

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

考虑上下文状态

圆圈的奇怪外观的原因与州有关。图形上下文可以保存状态。因此,当您设置许多上下文属性时,例如填充颜色,转换矩阵,颜色空间或剪贴区域,您实际上将为当前图形状态设置。

You can save a state by using context.saveGState(), which pushes a copy of the current graphics state onto the state stack. You can also make changes to context properties, but when you call context.restoreGState(), the original state is taken off the stack and the context properties revert. That's why you're seeing the weird issue with your points.

虽然你还在 graphview.swift., in draw(_:), uncomment the context.saveGState() before you create the clipping path. Also, uncomment context.restoreGState() before you use the clipping path.

通过这样做,你:

  1. Push the original graphics state onto the stack with context.saveGState().
  2. 将剪切路径添加到新图形状态。
  3. 在剪切路径内绘制梯度。
  4. Restore the original graphics state with context.restoreGState(). This was the state before you added the clipping path.

您的图表行和圈子现在应该更加清晰:

具有完整圆圈的图表和更清晰的线

At the end of draw(_:), add the code below to draw the three horizontal lines:

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

容易,对吗?你只是搬到一个点并绘制水平线。

轴线图

添加图形标签

现在,您将添加标签以使图表友好。

ViewController.swift. 并添加这些插座属性:

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

这增加了动态改变平均水醉标签的文本,最大水醉标签以及堆栈视图的日常名称标签的出口。

现在去 main.storyboard. 并将以下视图添加为子视图 图形视图:

图显示添加到图形视图的标签

The first five subviews are UILabels. The fourth subview is right-aligned next to the top of the graph and the fifth is right-aligned to the bottom of the graph. The sixth subview is a horizontal StackView with labels for each day of the week. You'll change these in code.

转移点击 所有标签然后将字体更改为自定义 Avenir下一个浓缩,中等样式.

如果您有任何问题设置这些标签,请通过使用签出最终项目中的代码 下载材料 本教程顶部或底部的按钮。

Connect averageWaterDrunk, maxLabel and stackView 到了 corresponding views in main.storyboard.. 控制阻力 从视图控制器到正确的标签,然后从弹出窗口中选择插座:

连接出口

既然您已完成设置图形视图,请进入 main.storyboard. 选择 图形视图 和检查 因此,在应用程序首次运行时,图形不会出现。

隐藏图形视图

打开 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. Use Swift's reduce 到 calculate the average glasses drunk for the week; it's a very useful method for summing all the elements in an array.
  4. This section sets up DateFormatter 到 return the first letter of each day.
  5. This loop goes through all labels inside stackView. From this, you set text for each label from date formatter.

还在 ViewController.swift., call this new method from counterViewTap(_:). In the else part of the conditional, where the comment says 展示图形,添加此代码:

setupGraphDisplay()

构建并运行并单击计数器。欢呼!在所有荣耀中,图表摇摆到视图中!

完成翻转动画

掌握矩阵

你的应用程序看起来非常敏锐!尽管如此,您还可以通过添加标记来改进计数器视图,以指示每间玻璃饮用:

柜台与刻度标记

既然您已经使用CG函数进行了一点练习,您将使用它们旋转并翻译绘图上下文。

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

每个刻度线的旋转角度计数器

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

起初,这似乎很令人困惑,但在你通过这些练习后,它会更有意义。转换的顺序很重要,所以这里有一些图表来解释你会做什么。

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

仅旋转上下文的结果

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

  1. 上下文在左上方旋转(0,0)
  2. 矩形仍然显示在上下文中 您旋转上下文。

当您绘制计数器视图的标记时,在旋转它之前,您将首先翻译上下文。

翻译的结果然后旋转上下文

在此图中,矩形标记位于上下文的左上角。蓝线概述翻译上下文。红色虚线表示旋转。在此之后,您将再次翻译上下文。

当您将红色矩形绘制到上下文中时,您将在视图中显示出一定角度。

旋转并转换上下文绘制红色标记后,您需要重置该中心,以便可以再次旋转和翻译上下文以绘制绿色标记。

正如您在图形视图中使用剪切路径保存上下文状态,每次绘制标记时都会使用转换矩阵保存并还原状态。

绘制标记

致命威达 and add this code to the end of draw(_:) 到 add the markers to the counter:

// 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. 使用先前计算的各个角度确定每个标记的角度。然后您旋转并翻译上下文。
  6. 在旋转和翻译的上下文的左上角绘制标记矩形。
  7. 恢复中心的上下文状态。
  8. 在任何旋转或翻译之前恢复上下文的原始状态。

哇!很好的工作挂在那里。现在建立并跑步并欣赏Flo的美丽和信息丰富的UI:

最终柜台UI.

从这里去哪里?

您可以使用使用的项目的已完成版本 下载材料 本教程顶部或底部的按钮。

此时,您已经了解了如何绘制路径,渐变以及如何更改上下文的转换矩阵。

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

如果您有任何疑问或意见,请加入下面的讨论!

平均评级

5/5

为此内容添加评级

7 ratings

更像这样的

贡献者

评论