裁剪画布对象

此 Codelab 是“使用 Kotlin 进行高级 Android 开发”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘课程的价值,但并不强制要求这样做。“使用 Kotlin 进行高级 Android 开发”Codelab 着陆页列出了所有课程 Codelab。

简介

在本 Codelab 中,剪裁是一种定义图片、画布或位图区域的方法,这些区域可以选择性地绘制到屏幕上或不绘制到屏幕上。剪裁的用途之一是减少过度绘制。过度绘制是指在屏幕上绘制某个像素的次数超过一次,以显示最终图像。减少过度绘制可最大限度地减少绘制像素或显示区域的次数,从而最大限度地提高绘制性能。您还可以使用剪裁在界面设计和动画中创建有趣的效果。

例如,当您绘制一叠重叠的卡片(如下所示)时,与其从底部开始完整绘制每张卡片,不如仅绘制可见部分,这样通常更高效。之所以说“通常”,是因为剪裁操作也有开销,而且总体而言,Android 系统会进行大量绘制优化。

如需仅绘制卡片的可见部分,您需要为每张卡片指定一个剪裁区域。例如,在下图中,当对图片应用剪裁矩形时,系统只会显示该矩形内的部分。

剪裁区域通常是矩形,但也可以是任意形状或形状组合,甚至是文字。您还可以指定是包含还是排除剪辑区域内的区域。例如,您可以创建一个圆形剪裁区域,并仅显示圆形区域之外的内容。

在此 Codelab 中,您将尝试各种剪裁方式。

您应当已掌握的内容

您应熟悉以下内容:

  • 如何使用 Android Studio 创建包含 Activity 的应用并运行该应用。
  • 如何创建和绘制 Canvas
  • 如何创建自定义 View,以及如何替换 onDraw()onSizeChanged()

学习内容

  • 如何剪裁对象以在 Canvas 上绘制。
  • 如何保存和恢复画布的绘制状态。
  • 如何对画布和文字应用转换。

您将执行的操作

  • 创建一个应用,该应用可在屏幕上绘制剪裁后的形状,演示不同的剪裁方式以及这些方式对形状可见性的影响。
  • 您还将绘制一些经过平移和倾斜处理的文字。

ClippingExample 应用演示了如何使用和组合形状来指定画布的哪些部分显示在视图中。最终的应用将如以下屏幕截图所示。

您将从头开始构建此应用,因此必须设置项目、定义尺寸和字符串,并声明一些变量。

第 1 步:创建 ClippingExample 项目

  1. 使用 Empty Activity 模板创建一个名为 ClippingExample 的 Kotlin 项目。使用 com.example.android 作为软件包名称前缀。
  2. 打开 MainActivity.kt
  3. onCreate() 方法中,替换默认的内容视图,并将内容视图设置为 ClippedView 的新实例。这将是您接下来要创建的剪辑示例的自定义视图。
setContentView(ClippedView(this))
  1. 在与 MainActivity.kt 相同的级别,创建一个新的 Kotlin 文件和类,用于名为 ClippedView 的自定义视图,该视图扩展自 View。为其提供如下所示的签名。您其余的所有工作都将在此 ClippedView 中完成。@JvmOverloads 注解指示 Kotlin 编译器为此函数生成替换默认形参值的重载。
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

第 2 步:添加维度和字符串资源

  1. res/values/dimens.xml 中的新资源文件中定义将用于剪裁视图的尺寸。这些默认尺寸是硬编码的,大小适合在相当小的屏幕上显示。
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

为了让应用在大屏幕上看起来更美观(并更轻松地查看细节),您可以创建一个具有更大值的 dimens 文件,该文件仅适用于大屏幕。

  1. 在 Android Studio 中,右键点击 values 文件夹,然后依次选择 New > Values resource file
  2. New Resource File 对话框中,将文件命名为 dimens。在可用限定符中,选择最小屏幕宽度,然后点击 >> 按钮将其添加到所选限定符中。在最小屏幕宽度框中输入 480,然后点击确定

  1. 该文件应显示在您的 values 文件夹中,如下所示。

  1. 如果您看不到该文件,请切换到应用的项目文件视图。新文件的完整路径如下所示:ClippingExample/app/src/main/res/values-sw480dp/dimens.xml

  1. values-sw480dp/dimens.xml 文件的默认内容替换为以下尺寸。
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. strings.xml 中,添加以下字符串。这些将用于在画布上显示文字。
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

第 3 步:创建并初始化 Paint 和 Path 对象

  1. 切换回项目的 Android 视图。
  2. ClippedView 中,定义一个用于绘制的 Paint 变量。启用抗锯齿,并使用尺寸中定义的描边宽度和文字大小,如下所示。
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. ClippedView 中,创建并初始化一个 Path,以在本地存储已绘制内容的路径。导入 android.graphics.Path
private val path = Path()

第 4 步:设置形状

在此应用中,您将显示多行和两列以各种方式剪裁的形状。

它们都具有以下共同点:

  • 充当容器的大矩形(正方形)
  • 大矩形上的对角线
  • 圆圈
  • 一小段文本

在此步骤中,您将为资源中的这些形状设置尺寸,以便稍后使用时只需获取一次尺寸。

  1. ClippedView 中,在 path 下方,为围绕整个形状集的剪切矩形的维度添加变量。
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. 添加了用于表示矩形边衬区和小型矩形偏移量的变量。
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. 添加一个用于表示圆半径的变量。这是在矩形内绘制的圆的半径。
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. 为绘制在矩形内的文字添加偏移量和文字大小。
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

第 4 步:设置行和列位置

此应用的形状显示在两列四行中,由上面设置的维度值决定。相关数学知识不在此 Codelab 的讨论范围内,但您可以在复制此步骤中给出的代码时查看一下。

  1. 为两列设置坐标。
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. 添加每行的坐标,包括转换后的文本的最后一行。
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. 运行应用。应用应打开,并在应用名称下方显示空白白色屏幕。

onDraw() 中,您将调用方法来绘制七个不同的剪切矩形,如下面的应用屏幕截图所示。所有矩形的绘制方式都相同;唯一的区别在于它们定义的剪裁区域和在屏幕上的位置。

用于绘制矩形的算法的工作方式如下图和说明所示。总而言之,您可以通过移动 Canvas 的原点来绘制一系列矩形。从概念上讲,这包括以下步骤:

(1) 首先,将 Canvas 平移到您希望绘制矩形的位置。也就是说,您无需计算下一个矩形和所有其他形状的绘制位置,只需移动 Canvas 原点(即其坐标系)即可。

(2) 然后,在画布的新原点处绘制矩形。也就是说,您在平移后的坐标系中绘制的形状位于同一位置。这要简单得多,效率也略高一些。

(3) 最后,将 Canvas 恢复到原始的 Origin

以下是您将实现的算法:

  1. onDraw() 中,调用一个函数以使用灰色背景颜色填充 Canvas 并绘制原始形状。
  2. 为每个剪裁后的矩形和要绘制的文本调用一个函数。

对于每个矩形或文本:

  1. 保存 Canvas 的当前状态,以便您可以重置为该初始状态。
  2. 将画布的 Origin 转换到您想要绘制的位置。
  3. 应用剪切形状和路径。
  4. 绘制矩形或文字。
  5. 恢复 Canvas 的状态。

步骤:替换 onDraw()

  1. 替换 onDraw(),如下面的代码所示。您将针对绘制的每个形状调用一个函数,这些函数将在稍后实现。
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. 为每个绘制函数创建桩,以便代码继续编译。您可以复制以下代码。
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

应用绘制了 7 次相同的矩形和形状,第一次没有剪裁,其余 6 次应用了不同的剪裁路径。drawClippedRectangle() 方法提取了用于绘制一个矩形的代码,如下所示。

第 1 步:创建 drawClippedRectangle() 方法

  1. 创建一个 drawClippedRectangle() 方法,该方法接受类型为 Canvas 的实参 canvas
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. drawClippedRectangle() 方法内,为整个形状设置剪裁矩形的边界。应用一个裁剪矩形,将绘制限制为仅绘制正方形。
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

Canvas.clipRect(...) 方法会缩小未来绘制操作可写入的屏幕区域。它会将剪裁边界设置为当前剪裁矩形与传递到 clipRect() 中的矩形的空间交集。clipRect() 方法有很多变体,这些变体接受不同形式的区域,并允许对剪裁矩形执行不同的操作。

  1. 用白色填充 canvas。是的!整个画布,因为您不是在绘制矩形,而是在剪裁!由于存在剪裁矩形,因此只有剪裁矩形定义的区域会被填充,从而创建一个白色矩形。其余表面保持灰色。
canvas.drawColor(Color.WHITE)
  1. 将颜色更改为红色,然后在剪切矩形内绘制一条对角线。
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. 将颜色设置为绿色,并在剪切矩形内绘制一个圆。
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. 将颜色设置为蓝色,并绘制与剪切矩形右边缘对齐的文字。使用 canvas.drawText() 绘制文字。
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

第 2 步:实现 drawBackAndUnclippedRectangle() 方法

  1. 如需查看 drawClippedRectangle() 方法的实际效果,请通过实现 drawBackAndUnclippedRectangle() 方法来绘制第一个未剪裁的矩形,如下所示。保存 canvas,将其平移到第一个行和列位置,通过调用 drawClippedRectangle() 进行绘制,然后将 canvas 恢复到之前的状态。
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. 运行应用。您应该会在灰色背景上看到第一个白色矩形及其圆圈、红线和文字。

在以下剪裁示例方法中,您将应用各种剪裁区域组合来实现图形效果,并了解如何组合剪裁区域来创建所需的任何形状。

这些方法都遵循相同的模式。

  1. 保存画布的当前状态:canvas.save()

activity 上下文会维护一个绘制状态堆栈。绘制状态包括当前转换矩阵和当前剪切区域。您可以保存当前状态,执行会更改绘制状态的操作(例如平移或旋转画布),然后恢复已保存的绘制状态。(注意:这类似于 Git 中的“stash”命令!)。

如果您的绘制内容包含转换,那么通过反转转换来链接和撤消转换很容易出错。例如,如果您先平移,然后拉伸,再旋转,很快就会变得复杂。而是保存画布的状态,应用您的转换,进行绘制,然后恢复之前的状态。

例如,您可以定义剪裁区域,然后保存该状态。然后,平移画布、添加剪裁区域并旋转。绘制一些内容后,您可以恢复原始剪裁状态,然后继续进行不同的平移和倾斜转换,如图所示。

  1. 将画布的原点转换为行/列坐标:canvas.translate()

与移动所有元素进行绘制相比,移动画布的原点并在新的坐标系中绘制相同的内容要简单得多。(提示:您可以使用相同的方法来旋转元素。)

  1. path 应用转换(如有)。
  2. 应用剪辑:canvas.clipPath(path)
  3. 绘制形状:drawClippedRectangle() or drawText()
  4. 恢复之前的画布状态:canvas.restore()

第 1 步:实现 drawDifferenceClippingExample(canvas)

添加代码以绘制第二个矩形,该矩形使用两个剪切矩形之间的差异来创建相框效果。

使用以下代码,该代码可执行以下操作:

  1. 保存画布。
  2. 将画布的原点平移到开放空间中的第一个矩形右侧的第一行第二列。
  3. 应用两个剪裁矩形。DIFFERENCE 运算符从第一个矩形中减去第二个矩形。
  1. 调用 drawClippedRectangle() 方法来绘制修改后的画布。
  2. 恢复画布状态。
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. 运行应用,界面应如下所示。

第 2 步:实现 drawCircularClippingExample(canvas)

接下来,添加代码以绘制一个矩形,该矩形使用通过圆形路径创建的圆形剪切区域,实际上是移除(不绘制)圆形,从而显示灰色背景。

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

第 3 步:实现 drawIntersectionClippingExample(canvas)

接下来,添加代码以在第二行和第二列中绘制两个剪切矩形的交集。

请注意,此区域的外观会因屏幕分辨率而异。尝试使用 smallRectOffset 维度来更改可见区域的大小。较小的 smallRectOffset 会使屏幕上的区域变大。

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

第 4 步:实现 drawCombinedClippingExample(canvas)

接下来,组合圆形和矩形这两个形状,并绘制任意路径来定义剪裁区域。

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

第 5 步:实现 drawRoundedRectangleClippingExample(canvas)

接下来,添加一个圆角矩形,这是一种常用的剪裁形状。

  1. 在顶层,创建并初始化一个矩形变量。RectF 是一个用于以浮点格式保存矩形坐标的类。
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. 实现函数 drawRoundedRectangleClippingExample()addRoundRect() 函数接受一个矩形、圆角半径的 x 和 y 值,以及绘制圆角矩形轮廓的方向。Path.Direction 用于指定将闭合形状(例如矩形、椭圆形)添加到路径时,这些形状的朝向。CCW 表示逆时针。
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

第 6 步:实现 drawOutsideClippingExample(canvas)

通过将剪切矩形的边衬加倍,剪切矩形周围的外部区域。

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

第 7 步:实现 drawTranslatedTextExample(canvas)

绘制文本与绘制任何其他形状并无太大区别,您可以对文本应用转换。例如,您可以通过翻译画布并绘制文字来翻译文字。

  1. 实现以下函数。
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. 运行应用,查看翻译后的文本。

第 8 步:实现 drawSkewedTextExample(canvas)

您还可以倾斜文字。也就是说,以各种方式扭曲它。

  1. ClippedView 中创建以下函数。
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. 运行应用,查看在翻译后的文字之前绘制的倾斜文字。

借助 quickReject() Canvas 方法,您可以检查在应用所有转换后,指定的矩形或路径是否完全位于当前可见区域之外。

当您需要尽快构建更复杂的绘制内容时,quickReject() 方法非常有用。借助 quickReject(),您可以高效地决定哪些对象根本不需要绘制,并且无需编写自己的相交逻辑。

  • 如果矩形或路径在屏幕上完全不可见,quickReject() 方法会返回 true。对于部分重叠,您仍需自行检查。
  • EdgeTypeAA(抗锯齿:通过圆角处理边缘,因为边缘可能经过抗锯齿处理)或 BW(黑白:通过仅舍入到最接近的像素边界来处理边缘),仅舍入到最接近的像素。

quickReject() 有多个版本,您也可以在文档中找到它们。

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(路径 path, Canvas.EdgeType type)

在本练习中,您将像之前一样,在文字下方和 clipRect 内绘制新行。

  • 您首先使用与 clipRect 重叠的矩形 inClipRectangle 调用 quickReject()。因此,quickReject() 返回 false,clipRect 填充了 BLACK,并绘制了 inClipRectangle 矩形。

  • 然后更改代码并调用 quickReject(),并使用 notInClipRectanglequickReject() 现在返回 true,clipRect 填充了 WHITE,并且不绘制 notInClipRectangle

当您有复杂的绘制内容时,此属性可以快速告知您哪些形状完全位于剪切区域之外,哪些形状可能需要进行额外的计算和绘制,因为它们部分或完全位于剪切区域内。

步骤:使用 quickReject() 进行实验

  1. 在顶层,为额外一行的 y 坐标创建一个变量。
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. 将以下 drawQuickRejectExample() 函数添加到 ClippedView。请阅读代码,其中包含使用 quickReject() 所需的一切信息。
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. onDraw() 中,取消对 drawQuickRejectExample() 的调用的注释。
  2. 运行应用,您会看到一个黑色矩形(即填充的剪切区域)和部分 inClipRectangle,因为这两个矩形重叠,所以 quickReject() 返回 false 并绘制 inClipRectangle

  1. drawQuickRejectExample() 中,更改代码以针对 notInClipRectangle. 运行 quickReject()。现在,quickReject() 会返回 true,并且剪裁区域会填充为白色。

下载已完成的 Codelab 的代码。

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


或者,您也可以下载 ZIP 文件形式的代码库,将其解压缩并在 Android Studio 中打开。

下载 Zip 文件

  • activity 的 Context 会维护一种状态,用于保留 Canvas 的转换和剪裁区域。
  • 使用 canvas.save()canvas.restore() 绘制内容并返回到画布的原始状态。
  • 如需在画布上绘制多个形状,您可以计算它们的位置,也可以移动(平移)绘图表面的原点。后者可让您更轻松地为重复的绘制序列创建实用方法。
  • 剪裁区域可以是任意形状、形状组合或路径。
  • 您可以添加、减去和相交剪裁区域,以获得所需的精确区域。
  • 您可以通过转换画布来对文字应用转换。
  • 借助 quickReject() Canvas 方法,您可以检查指定的矩形或路径是否完全位于当前可见区域之外。

Udacity 课程:

Android 开发者文档:

另请参阅图形架构系列文章,深入了解 Android 框架如何绘制到屏幕。

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

您调用什么方法来高效地排除要绘制的形状?

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

问题 2

Canvas.save()Canvas.restore() 会保存和恢复哪些信息?

▢ 颜色、线条粗细等。

▢ 仅限当前转换

▢ 当前的转换和剪裁区域

▢ 仅限当前剪辑区域

问题 3

Paint.Align 指定:

▢ 如何对齐以下绘图形状

▢ 文本是从原点的哪一侧绘制的

▢ 在剪辑区域中的对齐位置

▢ 要将文本的哪一侧与原点对齐

如需本课程中其他 Codelab 的链接,请参阅“使用 Kotlin 进行高级 Android 开发”Codelab 着陆页。