安卓画布学习手册-全-

安卓画布学习手册(全)

原文:zh.annas-archive.org/md5/6E7DDFC03078C433747871B677C39D41

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Android Canvas 学习 提供了 Android Canvas 图形和编程的基本知识和理解。目标读者被假定为对 Canvas 以及 Android 应用程序开发中的图形处理没有任何先验知识。将读者从基本的图形和 Canvas 编程知识带到中级 Android Canvas 编程知识。本书仅关注 2D 图形,不包括 3D 或动画,但为过渡到动画和 3D 图形学习提供了一个非常坚实的基础。它提供了从图形基础到不同图形对象和技术的实践逐步指导,再到更复杂的交互式图形丰富的 Android 应用程序。

这本书涵盖的内容

第一章, Android Canvas 入门, 提供了关于 Android Canvas、2D 图形、显示屏幕及其基本理解的一些背景知识,还介绍了图形丰富应用程序中的重要文件。

第二章,绘图线程, 有助于理解线程的需求、角色和用途,以及与线程相关的问题和解决方案。

第三章, Android Canvas 中的绘图和 Drawable, 向读者介绍了一些 Drawable 以及在 canvas、view 和 surface view 上绘图。

第四章, NinePatch 图片, 解释了切片的基本概念,NinePatch图片,重复和非重复区域,以及使用它们创建背景。

第五章, 触摸事件和在 Canvas 上绘图, 解释了捕捉触摸事件并相应地做出响应。还涵盖了创建自定义View类及其实现,包括触摸事件实现。

第六章, 整合应用, 讲述了如何规划一个应用程序,从零开始创建具有复杂用户界面的应用程序,并将之前学到的所有知识付诸实践。

你需要为这本书准备什么

你需要以下软件来运行本书中的示例:

  • 一台具有合理处理能力和 RAM 的计算机,Intel Core i3 就能胜任这项工作

  • Java 运行时环境

  • Eclipse 经典版

  • Android SDK,最新版本将是最佳选择

这本书适合的读者群体

熟悉 Java 编码和一些基本的 Android 开发知识的开发者。这本书适合那些具备基本的 Android 开发知识但对 Android Canvas 开发一无所知的开发者,也适合对图形丰富的应用程序和游戏开发感兴趣的开发者。

编写约定

在这本书中,你将发现多种文本样式,用于区分不同类型的信息。以下是一些样式示例及其含义的解释。

文本中的代码字如下所示:"我们将把我们的应用程序命名为MyFirstCanvasApp。"

代码块设置如下:

class OurGameView extends SurfaceView implements SurfaceHolder.Callback {
// Our class functionality comes here
}

当我们希望您关注代码块的某个部分时,相关的行或项目会以粗体显示:

android:background="@drawable/myfirstninepatch"
android:text="@string/buttonwith9patchimage"

任何命令行输入或输出都如下编写:

C:\learningandroidcanvasmini\chapter1\firstapp

新术语重要词汇以粗体显示。您在屏幕上看到的内容,例如菜单或对话框中的,会在文本中这样显示:"为此,在 Eclipse 中,我们将导航至文件 | 新建 | Android 应用程序项目。"

注意

警告或重要说明会以这样的框显示。

提示

提示和技巧会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或可能不喜欢的内容。读者的反馈对我们来说非常重要,可以帮助我们开发出您真正能从中受益的图书。

如果要给我们发送一般性反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上有专业知识,并且有兴趣撰写或参与图书编写,请查看我们在www.packtpub.com/authors的作者指南。

客户支持

既然您已经成为了 Packt 图书的骄傲拥有者,我们有许多方式帮助您充分利用您的购买。

下载示例代码

您可以从您的账户下载您购买的所有 Packt 图书的示例代码文件,访问地址为www.packtpub.com。如果您在别处购买了这本书,可以访问www.packtpub.com/support进行注册,我们会将文件通过电子邮件直接发送给您。

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能报告给我们,我们将不胜感激。这样做,您可以避免其他读者的困扰,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误被验证,您的提交将被接受,勘误将在我们网站的相应位置上传,或添加到现有勘误列表中。任何现有的勘误可以通过从www.packtpub.com/support选择您的标题来查看。

盗版

互联网上对版权材料的海盗行为是所有媒体都面临的持续问题。在 Packt,我们非常重视对我们版权和许可的保护。如果您在网上以任何形式遇到我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供该材料的链接。

我们感谢您帮助保护我们的作者,以及我们为您带来有价值内容的能力。

问题

如果您在书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:开始使用 Android Canvas

在本章中,我们将了解有关 Android Canvas 的一些信息,为什么需要它,以及它提供了什么。我们将创建一个简单的 Android 应用程序,在屏幕上显示一张图片。我们将在模拟器上测试该应用程序。然后,我们将讨论应用程序的每个部分以及读者在使用 Android Canvas 时需要了解的事项。在本章结束时,读者将学会使用 Canvas 提供的一种简单技术创建一个基本的 Android 应用程序,以显示图片,并获得一些额外的信息和处理应用程序中图形的良好约定。

Android Canvas

Android Canvas 为开发者提供了创建和修改 2D 图像和形状的能力。此外,Canvas 可用于创建和渲染我们自己的 2D 对象,因为此类提供了各种绘图方法。Canvas 还可以用来创建一些基本动画,比如逐帧动画,或者创建某些Drawable对象,例如带有纹理和形状的按钮,如圆形、椭圆形、正方形、多边形和线条。Android 还提供了硬件加速,以提高 Canvas 绘制的性能。现在我们知道我们要开发什么,为什么需要了解图形,我们的图形需求是什么,以及我们将使用什么。我们也知道 Android Canvas 是什么以及它为我们提供了什么。简而言之,Android Canvas 是我们所有问题的答案,因为它为我们的图形提供了所有正确的元素,以及一些基本动画来完成工作。对于 3D 图形,Android 提供了对 OpenGL 的支持;但在本书的范围内,我们不涉及 3D 图形,因此不会讨论 OpenGL。不过,感兴趣的读者可以查看developer.android.com/guide/topics/graphics/opengl.html的详细信息。

我们的第一款 Android Canvas 应用程序

本部分我们的目标是创建一个非常简单的应用程序,用以在屏幕上显示一张图片。不要害怕,现在开始开发 Canvas 似乎为时尚早——确实如此——但我们的第一个应用程序不涉及任何编码或复杂的内容。我们将逐步完成创建应用程序的不同步骤。我们会了解 SDK 版本和可用的 API,以及我们将使用哪些内容。你可能会对我们执行的应用程序中的某些部分或步骤不理解,但每执行一步,我们都会解释该步骤中发生的情况。至此我们仅仅是讨论,让我们开始真正的工作吧。我们将从打开 Eclipse 并开始一个新的 Android 应用程序项目开始。为此,在 Eclipse 中,我们将导航至文件 | 新建 | Android Application Project

下面的截图展示了 Eclipse 中新建应用程序的窗口:

我们的第一个 Android Canvas 应用程序

这是首先出现的屏幕。我们将应用命名为 MyFirstCanvasApp

应用名称

应用名称将显示在设置中的管理应用列表里;如果我们把应用发布到Play 商店,同样的字符串也会在那里显示。因此,名称应当吸引人且有意义。

项目名称

项目名称仅由 Eclipse 使用,但它应该在 workspace 中是唯一的。它可以与应用名称相同。

包名称

包名称必须是应用的唯一标识符。它不会展示给用户,但应该在应用的整个生命周期中保持不变。包名称是用来识别同一应用的不同版本的依据。我们的包名称将是 com.learningandroidcanvasmini.myfirstcanvasapp。这种特定的语法不是定义包名称的硬性规定,但它是避免包名称冲突的好方法。例如,如果我们有两个应用具有完全相同的名称,如下:

  • com.learningandroidcanvasmini.myfirstcanvasapp

  • com.learningandroidcanvasmini.myfirstcancasapp

假设第一个应用只是一个简单的展示一些图片的应用,而第二个应用是一个带有自由手绘功能的简单绘图应用。如果我们想将它们发布到 Google Play 商店,将会发生包名称冲突,第二个应用将不允许发布,因为已经有一个完全相同名称的应用存在。有两种方法可以避免这种情况。第一,更改包名称,以避免包名称冲突,第二个应用被视为全新的应用。例如:

  • com.learningcandroidcanvasmini.picdisplayapp

  • com.learningandroidcanvasmini.paintapp

第二,我们可以保持包名称不变,但更改 android:version 代码和 android:version 名称。这样发布将会成功,给用户的感觉是第一个应用是核心应用(如 myfirstcanvasapp 1.0 版)而第二个应用是同一应用的更新版本(如 myfirstcanvasapp 2.0 版)。有关在 Google Play 商店发布应用的更多详情和更好理解,请参考链接 developer.android.com/distribute/googleplay/publish/preparing.html

最小可能的 SDK 版本

选择尽可能低的所需 SDK 意味着我们的应用程序能够在尽可能多的设备上运行,但局限性在于,如果我们选择一个非常低版本的 Android,将无法使用硬件加速,因为低版本的 Android 不支持硬件加速。如果我们不需要硬件加速,可以选择更低版本以覆盖更多设备,但如果我们使用的一些动画和图形可能需要硬件加速,就需要使用稍高版本的 Android。

目标 SDK

这是我们的应用程序能够工作的最高 API 级别,但应用程序通常是向前兼容的,即使所使用的 SDK 高于我们的目标 SDK,只要使用的库没有改变,应用程序也能工作。这是一个罕见的情况,但有时新版本中的一些库会发生变化,这会影响对不同类的方法和属性的调用,导致应用程序功能异常。这个属性通知系统我们已经针对目标 API 测试了应用程序,系统不应产生任何兼容性问题。此外,应用程序将向后兼容到最低所需 SDK。我们将选择最高的可用 SDK,因为我们不希望应用程序因缺少新功能(如缺少硬件加速)而显得过时。

主题

主题是最后一个选项。我们将保留此选项的默认选择,因为目前它并不重要。

之后,我们将点击下一步,随后会出现以下屏幕。这是配置项目的界面。

下面的截图展示了新应用程序的配置界面:

主题

在这里,我们将检查是否需要自定义启动器图标。选择创建活动后,系统将自动为我们创建一个默认活动。同时配置创建项目的位置;通常是在我们的工作区。如果我们勾选了创建自定义启动器图标的复选框,点击下一步将带我们进入以下屏幕,即图标配置屏幕:

主题

在这个界面上,我们将配置自定义启动器图标属性,比如它应该从哪里读取源图像的路径。我们将配置图标的前景、形状和背景颜色。点击下一步进入下一个屏幕。我们选择了一个从网上随机下载的 Canvas 图像作为我们的源图像。该图像为 PNG 格式。PNG 图像支持透明度,例如完全透明的图像或带有部分透明背景。选择形状圆形背景颜色为深灰色。其他选项是将形状设置为方形。关于图标更多详情,请参考链接:developer.android.com/design/style/iconography.html

表单右侧显示了不同大小的图像,上方分别写着mdpihdpixhdpixxhdpidpi是每英寸点数,m表示中等,h表示高。这些是我们图像的不同尺寸,用于不同的屏幕尺寸。安卓设备具有不同的屏幕尺寸和分辨率。如果我们希望我们的应用程序支持多种屏幕尺寸,从旧设备到新设备,我们应该收集一些关于它们的 dpis、屏幕尺寸、屏幕分辨率、屏幕密度等信息;由于我们在这里处理的是图形,我们应该了解它们。然而,我们将在本章的末尾讨论这个问题。

下面的截图显示了选择默认活动屏幕的过程:

主题

在这个表单上,向导为我们提供了创建应用程序的选项,可以选择空白活动全屏活动主/细节流程活动表单。我们将选择空白活动并点击下一步继续操作。现在,向导将带我们进入以下表单,即默认活动表单:

主题

在此表单上,我们将把我们的活动命名为MyFirstCanvasAppMainActivity布局名称将会自动为我们填充,而导航类型应选择为,因为目前我们不希望应用程序中出现任何导航上的复杂性。

点击完成将关闭向导,我们将回到 Eclipse,屏幕显示如下截图所示,它显示我们的应用程序处于设计模式。以下截图显示了向导成功完成后,我们项目的第一次 Eclipse 视图:

主题

在这里,我们将在 AVD 管理器中创建一个安卓虚拟设备AVD)以及我们的模拟器,配置目标 SDK 版本为 4.2.2 以测试我们的应用程序。为此,我们将在默认出现在左侧的包资源管理器面板中,对我们的项目右键点击。在出现的菜单中,我们将导航至运行方式 | 运行配置。在此窗口中,我们将在安卓标签下选择我们的项目。然后,我们将进入目标标签,选择我们之前创建的 AVD 来测试我们的应用程序,并点击运行。这将触发模拟器运行,我们的应用程序将显示在模拟器中,如下面的截图所示。

如果我们在模拟器上点击主键,然后点击菜单查看模拟器上安装的所有应用程序,我们会看到我们的自定义启动图标也出现在菜单中,如下面的截图所示。为了给我们的应用程序留下好印象,我们必须设计一个有吸引力且相关的图标。为此,可以使用 Photoshop 或其他图形设计工具。如果开发人员拥有实际设备并将其配置为在 Eclipse 中进行测试,则可以跳过此步骤。

下面的截图展示了第一个默认活动,显示Hello world!

主题

下面的截图展示了我们应用程序图标在从顶部数第四行:

主题

既然我们已经让第一个应用程序运行起来,我们将尝试了解 Eclipse 中项目的最重要部分。了解重要部分之后,我们将达到我们的目标;即在屏幕上显示一张图片。

开采我们的第一个应用程序

首先,在每一个 Android 应用程序中,有三个文件需要我们特别注意;如果不理解这三个文件,我们将无法开发 Android 应用。以下部分将讨论这些文件。

配置文件

每个 Android 应用程序中的主配置文件是AndroidManifest.xml。这是一个 XML 文件,可以在Package Explorer中的项目根目录看到。这是我们应用程序的主要配置文件,也是项目中最重要的文件之一。这个文件包含了应用程序包的信息,应用程序使用的最小和最大 SDK,应用程序中使用的活动以及应用程序运行或执行特定任务所需的权限。每当应用程序即将在 Android 设备上安装时,这个文件会向系统提供应用程序将需要的权限和资源以及其中使用的活动的所有详细信息。系统读取这个文件后,会知道应用程序的包名称,兼容的 SDK 是什么,应用程序包含哪些活动,以及应用程序运行或执行某些任务所需的权限。

布局文件

我们应用程序中的布局文件是activity_my_first_canvas_app_main.xml,位于res文件夹内的layout文件夹中。所以在Package Explorer中的完整路径是res/layout/activity_my_first_canvas_app_main.xml。这是一个 XML 文件,负责我们活动的布局以及应用程序中活动上出现的视图。其他活动可以使用相同的 XML 格式和相同的 XML 布局文件进行布局。

代码文件

我们应用程序中的主活动代码文件是MyFirstCanvasAppMainActivity.java。这是我们活动的编码文件,在这里编写所有的功能。这个文件位于项目中的包文件夹内,即在src文件夹中,所以在Package Explorer中的项目路径变为src/com.learningandroidcanvasmini.myfirstcanvasapp/MyFirstCanvasAppMainActivity.java

下面的截图清晰地展示了这一点:

代码文件

除了上述文件,我们还将讨论res文件夹。res文件夹包含以下Drawable文件夹:

  • drawable-hdpi

  • drawable-ldpi

  • drawable-mdpi

  • drawable-xhdpi

  • drawable-xxhdpi

下面的截图展示了我们res文件夹内的drawable-xxhdpi文件夹。这就是我们放置图标的地方。

代码文件

如果我们检查所有这些文件夹,会发现每个文件夹中都有一个名为ic_launcher.png的图像,实际上这是我们在创建应用时使用的 Canvas 图像。每个文件夹中的图像都是相同的,但大小不同。现在,假设我们想要在屏幕上显示原始的 Canvas 图像。我们将原始的 Canvas 图像复制到这些文件夹中的一个;假设我们将图像复制到drawable-xhdpi文件夹。在 Package Explorer 中刷新文件夹,然后转到显示Hello world字符串的活动Design视图。选择字符串并删除它。在 Palette 中,点击Images & Media展开它。在Design视图中将 ImageView 拖放到活动上。系统会弹出一个对话框,提示我们为拖放的活动选择 ImageView 的源图像。

下面的截图显示了提示我们在Design模式下拖放到活动上的 ImageView 选择源图像的对话框:

代码文件

我们将选择 Canvas 图像并点击确定。我们的 Canvas 图像将显示在屏幕上。

以下屏幕显示了在Design模式下屏幕上显示的图像:

代码文件

我们将运行应用程序。以下是在模拟器上得到的结果——我们的应用程序在用 Canvas 绘制的 ImageView 上显示图像:

代码文件

这是我们需要非常小心的事情:当我们在res文件夹中保存图像文件时,需要仔细重命名图像文件。尽管图像文件名在这个项目之外可能没有影响,但在 Eclipse 中,如果出现以下错误,图像文件名会给你带来错误:

  • 包含空格的文件名;例如,our canvas.png

    这将返回一个错误,并且不会在我们项目文件中的 Package Explorer 窗格中显示。包含除_.以外的特殊字符的文件名也会返回错误,例如our-canvas(1).png

    下面的截图显示了将会显示的错误:

    代码文件

  • 不以字母字符开头的文件名;例如,886_1_Canvas.png。这将返回一个错误。

命名图像文件的最佳约定是先用字母字符开头;之后可以包含数字。在特殊字符中只使用_.;例如,our_canvas_1.png。这个文件名会被接受,我们也能在应用中使用这个图像。

我们已经完成了本章的目标,但如果我们不想让应用程序在不同屏幕尺寸和分辨率上的图形出现问题,我们需要了解一些额外的事情。如果我们希望应用程序支持多屏幕,我们需要了解以下内容:

  • 屏幕尺寸:这是物理屏幕尺寸,通常以对角线英寸为单位测量。Android 对所有显示设备分为四组:小、正常、大和超大。

  • 屏幕密度:这是每英寸的点数(dpi)。这是物理区域上的像素数量。这意味着一个 3 英寸的高密度屏幕将比一个 6 英寸的低密度屏幕拥有更多的像素。较低的 dpi 表示低密度屏幕,较高的 dpi 表示高密度屏幕。Android 有四个密度组:低、中、高和超高。这里就涉及到了ldpihdpimdpixhdpi。有关屏幕尺寸和密度的更多详细信息,请点击此链接:developer.android.com/guide/practices/screens_support.html

  • 屏幕方向:这是屏幕的方向。它可以是纵向或横向。我们需要注意这一点,因为不同的设备在不同的模式下运行,用户可以在运行时改变方向。因此,如果我们只设计其中一种,我们就必须锁定屏幕的方向。这样,即使用户旋转屏幕,我们 UI 的图形也会保持不变。最好为两种方向设计布局和图形。

总结

在本章中,我们学习了以下内容:

  • 了解 Android Canvas 的必要性

  • 什么是 Android Canvas 以及它为我们提供了什么

  • 创建一个简单的应用程序,在屏幕上显示图像

  • 了解我们第一个应用程序的重要文件和部分。

  • 关于屏幕尺寸、密度和方向的更多信息

到本章结束时,读者将了解在 Android 中处理图形的基础知识。读者将能够创建一个简单的应用程序,在屏幕上显示图像,并对项目不同部分有一个基本的了解。他/她还将了解哪些文件用于什么目的以及在哪里找到它们。读者还将获得一些关于屏幕和密度的基本知识;因此,在设计应用程序中的图形时,读者将使用这些信息来更好地决定设计用户界面。

在下一章中,我们将讨论线程,它们的重要性,如何在 Canvas 中使用它们进行绘图,以及使用线程时应用程序的性能权衡。

第二章:绘制线程

线程是可以由操作系统独立管理的最小指令序列。通常,在单个进程的单个线程中完成单个任务,但如果我们想改变正常行为,希望多个任务同时运行,我们将使用多线程。线程共享相同的进程资源,但独立执行。如果系统只有一个处理器,任务可能会看起来是同时处理的,但实际上并非如此。实际上,在一个处理器的情况下,处理线程的分配会从线程切换到线程,但切换非常快,以至于看起来像是同时处理的。如果系统有多个处理器,两个线程可以同时执行——彼此并行。多线程是一种执行模型,在单个进程中可以执行一个以上的线程。线程可以用多种方式使用,每种方式都有自己的重要性;例如,要么同时执行多个任务,要么如果需要在后台处理某些内容而让前端保持响应和活跃,就可以将负载从主线程移开。这是 Android 应用程序的理想情况,因为我们必须将尽可能多的负载从主线程移开,并保持前端响应和活跃。如果我们不这样做,保留消耗大量处理器能力和内存的重操作,应用程序可能会变得无响应,甚至要求我们强制关闭。

本章节的目标是清晰理解 Android 中的线程。我们不会深入探讨线程及其编码的细节,但会对 Android 中的线程有一个基本的了解。这有什么已知问题?在绘制和 Canvas 方面,Android 线程的重要性何在?我们将看到一个简单的代码结构,通过它我们希望任务在另一个线程上运行。

在 Android 中,所有应用都在单个线程上运行。所有指令按顺序执行,这意味着第二条指令不会在第一条完成之前开始。这个主线程也被称为UI用户界面)线程,因为它负责在屏幕上绘制所有对象或视图,并处理所有事件,例如屏幕触摸和按钮点击。现在的问题是,如果我们有两个操作计划在同一个默认线程或 UI 线程中运行,而第一个操作需要很长时间才能完成,系统会要求用户强制关闭应用程序或等待进程完成。这种情况被称为ANR应用程序无响应)。

绘制线程的需求

我们知道我们将要处理图像、绘图和其他图形处理,我们也知道它们对系统资源非常重。因此,我们希望在设计应用程序时非常谨慎,考虑到性能。如果我们忽略这一点,将所有的图像、位图、图形和其他图形处理项都放在默认的 UI 线程上会怎样呢?这是新 Android 开发者的工作方式——将所有内容都放在默认活动的代码中,意味着将整个负载放在 UI 线程上。默认活动是我们希望应用程序运行时首先加载的活动。UI 线程是我们应用程序的主要执行线程。这是大多数应用程序代码运行的地方。如ActivitiesServicesContentPorvidersBroadcastReceivers等应用程序组件都在这个线程中创建。这种情况下会发生什么?即使我们的应用程序是地球上最有用、最吸引人的应用程序,它也撑不过一天。每次用户运行我们的应用程序,它最终都会变得没有响应。Play 商店上用户的几条愤怒评论,我们的应用程序就完了。我们将失去这个想法,因为到那时它已经是公开的,我们也会失去我们的观众。为了解决这个问题,我们将从主 UI 线程中移走所有繁重的工作和负载,放到另一个线程上。理想情况下,当它运行时,看起来所有的线程都在并行运行,但这仅在有多 CPU 的情况下。如果只有一个 CPU 但支持多线程,系统将决定启动哪个线程,停止哪个线程,但没有任何线程会被永久停止。因此,控制将在运行中的线程之间切换,看起来就像是所有线程都在并行运行。

Android 中多线程的问题

我们将把耗时的资源密集型操作放在一个单独的线程上,但在 Android 中这会产生一个问题,那就是为什么不允许其他线程更新负责所有 UI 元素和处理过程的主 UI 线程。为了解决这个问题,我们需要将其与 UI 线程的当前状态同步。Android 提供了一个专门处理这个问题的类,它就是AsyncTask类。我们将在本章后面讨论这个问题。

Thread 类

ThreadRunnable类是使我们能够使用多线程的基本类,它们的功能非常有限,但仍然为AsyncTaskHandlerThreadIntentService.ThreadThreadPoolExecuter提供了基础。这些类能够自动管理线程,并可以并行运行多个线程。

下面是Runnable类的一个示例代码片段:

public class ImageReSize implements Runnable {
  public void run(){
    //the main functionality of the thread comes here
  }
}

如果我们希望线程在后台运行,我们将在前面提到的run()方法中添加以下这行代码:

Android.os.Process.setThreadPriority(Android.os.Process.THREAD_PRIORITY_BACKGROUND);

假设我们有我们的Runnable类。我们仍然无法在用户界面上显示任何内容,因为只有 UI 线程执行 UI 对象,如视图。在 UI 线程上运行的对象可以访问其他对象。现在,在我们的线程上运行的任务不在 UI 线程上,因此它们无法访问 UI 对象。为了使我们的任务能够访问 UI 线程上的 UI 对象,我们必须使用可以将在后台线程中的数据移动到 UI 线程的东西。如果一个线程在后台运行,并且需要更改 UI 上的内容,它本身无法做到这一点,但可以使用runOnUiThread提供的功能,这将使我们能够在主 UI 线程上运行后台线程中的代码。另外,我们可以选择使用Handler对象。

提示

下载示例代码

您可以从您的账户下载您购买的所有 Packt 图书的示例代码文件,访问地址为www.packtpub.com。如果您在别处购买了这本书,可以访问www.packtpub.com/support注册,我们会将文件直接通过电子邮件发送给您。

在 UI 线程上运行的 Handler 对象

要编写在 UI 线程上运行的Handler对象,首先应在 UI 线程上定义一个Handler对象,然后将数据存储在Task对象中。应该将对象的状态发送到对象层次结构的上层。完成这些操作后,应将数据移动到 UI 线程。我们这样做是为了实现在后台的另一个线程上运行任务,并在需要时与主 UI 线程通信以获得我们期望的输出。然而,这需要付出很多努力。

在 UI 线程上运行的 Handler 对象

AsyncTask

为了显著减少工作量和复杂性,Android 提供了AsyncTask类。这个类将我们的任务分配到后台的另一个线程上运行,并在需要时自动与 UI 线程通信,为我们节省了使用Handler对象的时间和精力。为了完成任务,我们将创建一个扩展AsyncTask的类,将我们的功能放入其中,并执行我们的应用程序。AsyncTask会为我们做很多工作。

概述

在本章中,我们了解了以下内容:

  • 简单 Android 应用程序的线程结构

  • UI 线程的角色及其重要性

  • 需要将耗用资源的操作从主 UI 线程中分离出来

  • 非 UI 线程的局限性

  • 如何使用Handler对象处理问题并与 UI 线程通信

  • 实现Runnable接口的类的代码结构,使我们能够使用线程

  • Android 以AsyncTask类的形式提供给我们的功能;我们也了解到了它的重要性。

在下一章中,我们将学习Drawable类以及在 Canvas 上使用资源中的图像和 XML 进行绘图。同时还将介绍在ViewSurfaceView上的绘图以及基本形状如圆形的绘制。

第三章:安卓画布中的绘制和可绘制资源

在本章中,我们的目标是了解以下内容:

  • 在 Canvas 上绘制

  • 在视图上绘制

  • 在 SurfaceView 上绘制

  • 可绘制资源

  • 来自资源图片的可绘制资源

  • 来自资源 XML 的可绘制资源

  • 形状可绘制资源

安卓为我们提供了 2D 绘图 API,使我们能够在 Canvas 上绘制自定义图形。在处理 2D 绘图时,我们将在视图上绘制,或者直接在表面或 Canvas 上绘制。使用视图来处理图形时,绘制由系统的正常视图层次结构绘制过程处理。我们只需定义要在视图中插入的图形;其余的由系统自动完成。当使用直接在 Canvas 上绘制的方法时,我们必须手动调用合适的 Canvas 绘制方法,如onDraw()createBitmap()。这种方法需要更多的努力和编码,并且稍微复杂一些,但我们能控制一切,比如动画以及通过代码控制绘制的尺寸、位置、颜色以及将绘制内容从当前位置移动到另一个位置的能力。onDraw()方法的实现可以在视图上的绘制部分看到,而createBitmap()的代码在在 Canvas 上绘制一节中展示。

如果我们处理的是静态图形——在应用程序执行期间不会动态变化的图形——或者我们处理的图形不是资源饥渴型的,因为我们不希望将应用程序性能置于风险之中,我们将使用在视图上绘制的方法。在视图上绘制可以用于设计具有静态图形和简单功能的引人注目的简单应用程序——简单的吸引人背景和按钮。使用主 UI 线程在视图上绘制是完全可以的,因为这些图形不会对应用程序的整体性能构成威胁。

当处理像游戏中那样动态变化的重型图形时,应该使用在 Canvas 上绘制的方法。在这种情况下,Canvas 会不断重绘自己以保持图形更新。我们可以在主 UI 线程上在 Canvas 上绘制,但是正如我们在第二章中尽可能详细讨论的那样,绘制线程,在处理重型、资源饥渴、动态变化的图形时,应用程序会持续重绘自己。最好使用单独的线程来绘制这些图形。将这样的图形保留在主 UI 线程上不会使它们进入无响应模式,而且经过如此努力工作后,我们肯定不会喜欢这种情况。因此,这个选择应该非常谨慎地做出。

在 Canvas 上绘制

Canvas是一个接口,一种媒介,它使我们能够实际访问表面,我们将用它来绘制图形。Canvas包含了绘制图形所需的所有必要方法。在Canvas上绘制的实际内部机制是,每当需要在Canvas上绘制任何内容时,它实际上是绘制在一个底层的空白位图图像上。默认情况下,这个位图是自动为我们提供的。但如果我们想使用一个新的Canvas,那么我们需要创建一个新的位图图像,然后再创建一个新的Canvas对象,同时将已创建的位图提供给Canvas类的构造函数。以下是一个示例代码的说明。最初,位图被绘制但不在屏幕上;它实际上是在内部Canvas的后台绘制的。但要将它带到前台,我们需要创建一个新的Canvas对象,并提供已创建的位图,以便在屏幕上绘制。

Bitmap ourNewBitmap = Bitmap.CreateBitmap(100,100,Bitmap.Config.ARGB_8888);
Canvas ourNewCanvas = new Canvas(ourNewBitmap);

View上绘制

如果我们的应用程序不需要大量的系统资源或高帧率,我们应该使用View.onDraw()。在这种情况下,好处是系统会自动为Canvas提供其底层的位图。我们需要做的就是进行绘图调用,完成我们的绘图工作。

我们将通过扩展View类来创建我们的类,并在其中定义onDraw()方法。onDraw()方法是我们将在其中定义要在Canvas上绘制的内容的地方。Android 框架将调用onDraw()方法,要求我们的View绘制自己。

onDraw()方法将在需要时由 Android 框架调用;例如,每当我们的应用程序想要绘制自己时,都会调用这个方法。每当想要我们的view重绘自己时,我们必须调用invalidate()方法。这意味着,每当我们想要应用程序的视图重新绘制时,我们都会调用invalidate()方法,然后 Android 框架会为我们调用onDraw()方法。假设我们想要画一条线,那么代码可能如下所示:

class DrawView extends View {
  Paint paint = new Paint();
  public DrawView(Context context) {
    super(context);
    paint.setColor(Color.BLUE);
  }
  @Override
  public void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawLine(10, 10, 90, 10, paint);
  }
}

onDraw()方法内部,我们将使用Canvas类提供的各种设施,比如Canvas类提供的不同的绘图方法。我们还可以使用其他类的绘图方法。当我们的onDraw()方法完成了所有期望的功能后,Android 框架会在Canvas上为我们绘制一个位图。如果我们使用的是主 UI 线程,我们会调用invalidate()方法,但如果我们使用的是另一个线程,那么我们会调用postInvalidate()方法。

SurfaceView上绘制

View类提供了一个子类SurfaceView,在View的层次结构中提供了一个专用的绘图表面。目标是使用一个辅助线程进行绘制,这样应用程序就不会等待资源空闲并准备好重绘。辅助线程可以访问SurfaceView对象,该对象具有使用自己的Canvas以自己的重绘频率进行绘制的功能。

我们将从创建一个扩展 SurfaceView 类的类开始。我们应该实现一个接口 SurfaceHolder.Callback。这个接口之所以重要,是因为它能在表面被创建、修改或销毁时提供给我们信息。当我们及时了解表面的创建、更改或销毁情况时,我们可以更好地决定何时开始绘图以及何时停止。在 SurfaceView 类中也可以定义将在 Canvas 上执行所有绘图工作的次要线程类。

为了获取信息,应该通过 SurfaceHolder 处理 Surface 对象,而不是直接处理。为此,我们将在初始化 SurfaceView 时调用 getHolder() 方法来获取 Holder。然后,我们会告诉 SurfaceHolder 对象我们希望接收所有回调;为此,我们将调用 addCallBacks()。此后,我们将重写 SurfaceView 类内的所有方法,以根据我们的功能完成我们的工作。

下一步是在第二个线程内部绘制表面的 Canvas;为此,我们将传递我们的 SurfaceHandler 对象到线程对象,并使用 lockCanvas() 方法获取 Canvas。这将为我们获取 Canvas,并将其仅对当前线程的绘图锁定。我们需要这样做,因为我们不希望有一个可以被另一个线程绘制的开放的 Canvas;如果出现这种情况,它将干扰我们在 Canvas 上的所有图形和绘图。当我们完成在 Canvas 上绘制图形后,我们将通过调用 unlockCanvasAndPost() 方法并传递我们的 Canvas 对象来解锁 Canvas。为了成功绘图,我们将需要重复重绘;因此,我们将根据需要重复锁定和解锁,表面将绘制 Canvas。

为了实现统一和平滑的图形动画,我们需要拥有 Canvas 的上一个状态;因此,我们将每次从 SurfaceHolder 对象中获取 Canvas,并且每次整个表面都应该重新绘制。如果我们不这样做,例如,没有绘制整个表面,上一个 Canvas 的绘图将会持续存在,这将破坏我们图形密集型应用程序的整体外观。

一个示例代码如下:

class OurGameView extends SurfaceView implements SurfaceHolder.Callback {
  Thread thread = null;
  SurfaceHolder surfaceHolder;
  volatile boolean running = false;
  public void OurGameView (Context context) {
    super(context);
    surfaceHolder = getHolder();
  }

  public void onResumeOurGameView (){
    running = true;
    thread = new Thread(this);
    thread.start();
  }
    public void onPauseOurGameView(){
  boolean retry = true;
  running = false;
  while(retry){
    thread.join();
    retry = false;
  }

  public void run() {
  while(running){
    if(surfaceHolder.getSurface().isValid()){
      Canvas canvas = surfaceHolder.lockCanvas();
    //... actual drawing on canvas
      surfaceHolder.unlockCanvasAndPost(canvas);
      }
    }
  }
}

Drawable 图形

Android 提供的二维图形和绘图库称为 Drawable。确切的包名是 android.graphics.drawable。这个包提供了绘制我们 2D 图形所需的所有类。

通常,Drawable 是可以绘制的抽象概念。Android 提供了许多扩展了 Drawable 类的类,以定义特殊的 Drawable 图形类型。完整的列表可以在 developer.android.com/reference/android/graphics/drawable/package-summary.html 找到。

Drawable 可以通过三种方式定义和实例化:

  • 从保存在我们项目的资源文件夹中的图片

  • 从 XML 文件中

  • 从普通的类构造函数中

在本书的背景下,我们只解释前两种方法。

从资源图片中的可绘制资源

这是向我们的应用程序添加图形的最快和最简单的方法。我们已经解释了项目中的不同重要文件夹,并在第一章《开始使用 Android 画布》中详细讨论了哪个文件夹包含什么类型的文件。在本章结束时,我们将知道如何将图片复制到资源文件夹中,以及在哪里找到资源文件夹。

我们将使用在第一章《开始使用 Android 画布》中已经复制的图片,该图片位于我们应用程序项目的res/drawable文件夹中。图片名为lacm_5396_01_14.png,确切位置在res/drawable-xhdpi。这里有一个重要点是,支持的格式有 PNG、JPEG 和 GIF。最理想的格式是 PNG,最不理想的是 GIF。每当我们把图片放在res/drawable文件夹中时,在构建过程中,图片将会使用无损压缩来节省系统内存;这个过程是自动的。压缩后的图片通常能保持相同的质量,但大小会小得多。如果我们不希望系统压缩我们的图片,我们应该将图片复制到res/raw文件夹中。

我们将使用第一章《开始使用 Android 画布》中的相同应用程序源代码来说明这一章节的内容。我们将打开我们的项目MyFirstCanvasApp。这是我们在进行任何更改之前的代码:

package com.learningandroidcanvasmini.myfirstcanvasapp;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
public class MyFirstCanvasAppMainActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my_first_canvas_app_main);
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it //is present.
    getMenuInflater().inflate(R.menu.my_first_canvas_app_main,menu);
    return true;
  }
}

我们将在设计视图中打开我们的布局文件activity_my_first_canvas_app_main.xml。我们将删除在第一章《开始使用 Android 画布》中添加到活动中的ImageView对象。现在,我们将再次打开我们的代码文件,并逐步添加以下代码行到前面的代码中。在我们的主活动类中,我们将定义一个LinearLayout对象:

LinearLayout myLinearLayout;

这将是我们自定义的布局,我们想使用这段代码来显示图片。然后,在我们的主活动类中,我们将实例化LinearLayout对象:

myLinearLayout = new LinearLayout(this);

接下来,我们将向我们的文件中添加以下代码行:

ImageView MySecondImageView = new ImageView(this);
MySecondImageView.setImageResource(R.drawable.lacm_5396_01_14);
MySecondImageView.setAdjustViewBounds(true);
MySecondImageView.setLayoutParams(new ImageView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
myLinearLayout.addView(MySecondImageView);
setContentView(myLinearLayout);

在前面的代码块中,首先我们定义了一个ImageView对象。然后我们设置我们希望ImageView对象显示的图像源。在下一行,我们调整了视图边界,使ImageView的边界与源图像的宽度和高度相匹配。setLayoutParams方法可以帮助我们将视图边界围绕图像内容,即使尺寸有差异。在此之后,我们将使用以下代码行将我们的ImageView控件提供给我们的自定义布局:

myLinearLayout.addView(MySecondImageView);

在最后一行,我们将活动的布局设置为我们的自定义布局。为此,我们将内容视图设置为我们的自定义布局:

setContentView(myLinearLayout);

现在,我们将在模拟器中测试我们的应用程序,然后我们将在模拟器屏幕上看到以下内容:

来自资源图像的可绘制资源

如果我们将此输出图像与在第一章,Android Canvas 入门中的活动屏幕上的图像进行比较,我们看到的区别并不大。我们在第一章,Android Canvas 入门中很容易地实现了相同的输出。那么,为什么我们还要经历所有这些复杂的编码来在本章中实现相同的输出呢?

我们之所以要经历所有这些艰难的工作和复杂的代码,是因为在第一章,Android Canvas 入门中,我们将ImageView对象硬编码为只显示我们在设计视图的属性标签中定义的一张图片。现在,当我们从设计视图中删除屏幕上的ImageView对象并开始编码时,那时在设计视图中屏幕上什么都没有。在前一个示例中我们所做的是创建我们自己的自定义布局,该布局将承载我们的图形和绘图。我们创建了一个ImageView对象,为其提供了源图像并设置了其他属性。稍后,我们将ImageView对象添加到我们的自定义布局中,最后,我们要求活动在没有自定义创建的布局和自动布局的情况下出现在屏幕上。这段代码为我们提供了保持图形应用程序动态的灵活性。我们可以通过我们的代码逻辑控制,为应用程序提供运行时图像。

完整的代码现在看起来像这样:

package com.learningandroidcanvasmini.myfirstcanvasapp;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.ImageView;
import android.widget.LinearLayout;

public class MyFirstCanvasAppMainActivity extends Activity {
  LinearLayout myLinearLayout;  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my_first_canvas_app_main);
  myLinearLayout = new LinearLayout(this);
  ImageView MySecondImageView = new ImageView(this);
  MySecondImageView.setImageResource(R.drawable.lacm_5396_01_14);
  MySecondImageView.setAdjustViewBounds(true);
  MySecondImageView.setLayoutParams(new ImageView.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
  myLinearLayout.addView(MySecondImageView);
  setContentView(myLinearLayout);  
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it //is present.
    getMenuInflater().inflate(R.menu.my_first_canvas_app_main, menu);
    return true;
  }

}

如果我们希望我们的资源图像被视为一个Drawable,我们将从我们的资源图像创建一个Drawable对象:

Resources myRes = mContext.getResources();
Drawable myImage = myRes.getDrawable(R.drawable.5396_01_14);

在这里,我们需要了解的是,我们的Resources对象中的每个资源一次只能保持一个状态。如果我们正在两个不同实例中使用相同的资源图像,并且我们更新了一个实例的属性,第二个实例中的图像也会反映出这个变化。因此,每当我们处理Drawable对象的多个实例时,我们不是改变 Drawable 本身,而是可以创建补间动画。

来自资源 XML 的可绘制资源

对于那些对 Android 开发有一定背景的开发者来说,我们知道应用程序中的每个活动都有一个 XML 布局文件。在这个文件中,我们在活动中拖放的所有视图或控件都有一个定义的 XML 标记。因此,我们假设阅读这本书的开发者知道在为 Android 开发时用户界面是如何工作的。对象可以在 XML 中定义和初始化。如果我们处理的是那些属性不依赖于我们计划在代码中执行的操作的图形,或者图形可能是静态的,那么在 XML 中定义图形对象是一个好方法。一旦图形被实例化,其属性可以根据需要随时调整。

我们将在res/drawable中保存文件,在 XML 中定义 Drawable,并通过调用Resouces.getDrawable()获取 Drawable。这个方法将从我们的 XML 文件中获取资源 ID 作为参数。

为了举例说明,并了解哪些 Drawable 可以使用这种方法以及我们如何查看应用程序中自动创建的菜单,请注意前面代码中的onCreateOptionMenu()方法。当我们点击屏幕上的菜单按钮或从硬件键上点击时,我们在屏幕底部看到一个名为设置的小菜单。此时菜单没有功能。现在如果我们检查onCreateOptionMenu()的代码,我们看到一个对inflate()方法的调用。我们可以在 XML 中定义任何支持inflate()方法的 Drawable。前面提到的菜单就是这方面的一个简单例子。

设置菜单可以在以下屏幕截图中看到:

来自资源 XML 的 Drawables

假设我们想要一个展开-折叠过渡的 Drawable,下面这段 XML 代码可以为我们完成这项工作。这段 XML 代码将被保存在res/drawable expand_collapse.xml文件中。

<transition 
>
  <item android:drawable="@drawable/image_expand">
    <item android:drawable="@drawable/image_collapse">
      </transition>

expandcollapse文件是我们保存在项目drawable文件夹中的两个不同的图像。现在为了使这个过渡工作,我们需要以下代码:

Resources myRes = mContext.getResources();
TransitionDrawable myTransition = (TransitionDrawable)
  res.getDrawable(R.drawable.expand_collapse);
ImageView myImage = (ImageView) findViewById(R.id.toggle_image);
  myImage.setImageDrawable(myTransition);

首先,我们从资源中创建了一个resources对象,并要求该对象从这些资源中获取所有内容(这些资源是我们保存在项目res文件夹子文件夹中的所有图片和 XML 文件)。然后,我们创建了一个TransitionDrawable对象,并要求该对象从res/drawable文件夹中获取expand_collapse文件。此后,我们将创建一个ImageView对象,它将获取另一个名为toggle_image的视图。在前面代码的最后一句中,我们将 Drawable 类型设置为已创建的过渡。

现在包含以下代码行将使过渡以每秒一次的速度运行:

myTransition.startTransition(1000);

关于这些过渡和动画,我们不会过多详述,因为动画本身就是一个非常庞大的主题。但是我可以解释一些在 Android 中处理图形时可以进行的关键类型的动画,让读者了解这个领域以及动画所涵盖的内容。Android 中的动画类型如下:

  • 属性动画(Property animation)

  • 视图动画(View animation)

  • 可绘制动画(Drawable animation)

形状可绘制对象(Shape Drawables)

每当我们想在画布上动态或以编程方式绘制某些形状时,形状可绘制对象就显得非常方便。使用形状可绘制对象,我们可以绘制圆形以及所有圆形形式,如椭圆形、正方形、矩形和许多其他形状。为了解释形状可绘制对象,我们将以第一章,开始使用 Android 画布的方式启动一个新项目。我们将我们的项目命名为MyShapeDrawablesApp,并按照第一章使用空白起始活动进行相同的步骤。我们这项练习的目标是在屏幕上绘制一个带有某种颜色填充的椭圆形。

  1. 为此,我们将在主活动类的结束括号之前添加另一个类。我们将我们的类命名为MyCustomDrawableView,它将扩展View类。

    public class MyCustomDrawableView extends View {.....
    
  2. 在这个类的构造函数中,我们将定义我们的绘制。我们将定义一个ShapeDrawable对象,并向其构造函数提供OvalShape()方法作为参数以定义形状的类型:

    myDrawable = new ShapeDrawable(new OvalShape());
    
  3. 接下来,我们将获取画笔对象并为我们的ShapeDrawable对象设置颜色:

    myDrawable.getPaint().setColor(0xff74fA23);
    
  4. 之后,我们将定义要绘制的对象的尺寸。比方说我们想要绘制一个椭圆形。第一个x, y是它将开始的位置,接下来的则是椭圆的宽度和高度,如下所示:

    myDrawable.setBounds(x, y, x + width, y + height);
    
  5. 在这一点上,我们将关闭构造函数,并为我们的对象定义onDraw()方法。在这个方法内部,我们将调用我们对象的draw()方法。

    protected void onDraw(Canvas canvas) {
      myDrawable.draw(canvas);
    }
    
  6. 下一步将在主活动类中创建我们自定义类的对象,并将内容视图设置为我们的新自定义类:

    MyCustomDrawableView myCustomDrawableView;
    .
    .
    .
    myCustomDrawableView = new MyCustomDrawableView(this);
    
      setContentView(myCustomDrawableView);
    
  7. 我们将在模拟器中运行应用程序。

  8. 下面的截图显示了在画布上绘制的绿色椭圆形:形状可绘制对象

MyShapeDrawablesMainActivity.java文件的完整代码如下:

package com.learningandroidcanvasmini.myshapedrawablesapp;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.view.Menu;
import android.view.View;

public class MyShapeDrawablesMainActivity extends Activity {
  MyCustomDrawableView myCustomDrawableView;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_my_shape_drawables_main);

    myCustomDrawableView = new MyCustomDrawableView(this);
      setContentView(myCustomDrawableView);
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is 
    //present.
    getMenuInflater().inflate(R.menu.my_shape_drawables_main, menu);
    return true;
  }

  public class MyCustomDrawableView extends View {
    private ShapeDrawable myDrawable;

    public MyCustomDrawableView(Context context) {
      super(context);

      int x = 10;
      int y = 10;
      int width = 300;
      int height = 50;

      myDrawable = new ShapeDrawable(new OvalShape());
      myDrawable.getPaint().setColor(0xff74fA23);
      myDrawable.setBounds(x, y, x + width, y + height);
    }

    protected void onDraw(Canvas canvas) {
      myDrawable.draw(canvas);
    }
  }

}

总结

在本章中,我们学习了Canvas类、它的方法、属性,以及如何使用它们进行绘图。我们还了解了ViewSurfaceView类,并学会了如何使用这两个类进行绘图,以及在哪种情况下使用哪一个。我们也学习了Drawables以及使用它们绘图的不同方法,例如从资源中的图片或资源中的 XML 代码进行绘图,以及使用代码绘制形状。在处理 Shape Drawables 和在 Canvas 上绘图时,我们还看到了一个功能性的示例。两个示例应用程序的源代码都可以从Packt Publishing网站下载。在下一章中,我们将详细讨论 9-patch(也称为 NinePatch)图片。我们将开发一个使用 9-patch 图片的工作应用程序,并讨论它在设计我们移动应用程序布局中的重要性。

第四章:NinePatch 图像

在本章中,我们将了解 NinePatch 图像:它们是什么,它们的重要性,如何创建它们,如何使用它们,以及它们可以在我们的 Android 应用程序中产生什么影响。

NinePatch 图像非常重要,因为我们希望以这样的方式开发我们的布局,即如果方向改变或者我们的应用程序在不同的分辨率设备上运行,它都可以调整。我们应用程序的布局需要足够灵活,以根据不同设备的分辨率、屏幕尺寸和方向进行调整。NinePatch 图像是具有可伸展区域的图像。这些可伸展区域可以水平和垂直伸展以包含内部内容。最好的部分是,如果内容的宽度和高度未定义,NinePatch 图像可以水平和垂直伸展以适应任何类型的宽度和高度的任何内容。通常,NinePatch 图像用于不同类型容器或视图的背景中,这些容器或视图将承载某种类型的内容。它可以作为我们应用程序中活动的背景,或者仅用于带文本的按钮的背景。Ninepatch 图像是带有额外 1 像素宽边框的普通 PNG 图像。必须确保一件事,NinePatch 图像必须保存为 .9.png 文件扩展名。

NinePatch 图像之所以这样称呼,是因为 Drawable 对象允许在九个区域中绘制图像。为了使这个概念清晰,请查看以下图表:

NinePatch 图像

在上一个图表中,有可伸展和不可伸展的区域。在 NinePatch 图像中,四个部分将是可伸展的;在前面示例中它们是部分2468。四个部分将不可伸展,在前面示例中它们是部分1379。最后一部分是内容区域,将在两个方向上伸展;在之前的示例中它是部分5

在可伸展的区域中,第28部分仅水平伸展。第46部分仅垂直伸展。第5部分将水平和垂直伸展,这是图形的主要部分,用于容纳内容。

这里重要的一点是,并不一定需要按照示例中显示的确切样式来设置补丁。NinePatch 图像可以创建为仅水平或垂直延伸的补丁;此外,它还可以拥有比提到的示例更多的补丁。

创建 NinePatch 图像

为了创建 NinePatch 图像,Android 提供了一个非常简单的工具;然而,在使用该工具之前,我们需要了解使用该工具的要求。首先,我们需要有一个基础的 PNG 图形文件,我们将把它转换成我们的第一个 NinePatch 图像。我们可以在任何图形编辑工具中创建我们的基础 PNG 图像,比如 Photoshop。因此,我们将通过打开 Photoshop 并创建一个新的 PNG 图像来开始创建我们的基础 PNG 图像。

下面的截图展示了 Photoshop 中新建文件对话框:

创建 NinePatch 图像

我们计划创建一个绿色的盒子,稍后会将盒子内的内容转换为 NinePatch 图像。首先,我们将文件命名为 MyFirst9PatchBox 并将 背景内容 设置为 透明。点击 确定 后,我们会得到一个空白画布。接下来,我们将在画布内绘制一个带有透明背景的绿色盒子。

下面的截图展示了我们在 Photoshop 中绘制的绿色盒子:

创建 NinePatch 图像

默认情况下,Photoshop 以 PSD 格式保存文件,但我们会以 PNG 格式保存文件,因为我们需要一个 PNG 文件来将其转换为 NinePatch 图像。或者,我们可以将文件保存为网页格式;这样创建的 PNG 文件会更小。这将提高应用程序的整体性能。在我们的应用程序中使用许多重量级的图像可能会降低应用程序的性能。

要从我们的绿色盒子 PNG 文件创建 NinePatch 图像,我们将打开 Android SDK 中 Tools 文件夹提供的 Draw 9-patch 工具。为此,我们将浏览到 Android SDK 中的 Tools 文件夹,并找到 draw9patch.bat 文件。

文件的路径为 F:\Android\adt-bundle-windows-x86_64-20130219\adt-bundle-windows-x86_64-20130219\sdk\tools,其中 F 是我的驱动器,Android 是 F 驱动器根目录下的一个文件夹,其余部分位于 Android 文件夹内。下面的截图展示了 draw9patch 工具的位置。

创建 NinePatch 图像

双击可以打开 draw9patch 工具。下面的截图展示了带有空屏幕的 draw9patch 工具:

创建 NinePatch 图像

在下一步中,我们将把我们的 PNG 基础图像拖拽到 draw9patch 工具中,或者直接在 draw9patch 工具中打开我们的 PNG 文件。

下面的截图展示了我们的基础 PNG 文件和 draw9patch 工具并排显示:

创建 NinePatch 图像

当我们将基础 PNG 文件拖拽或打开在 draw9patch 工具中时,我们会看到以下截图:

创建 NinePatch 图像

右侧区域显示了绘图区域,我们将在其中定义我们的补丁区域——需要拉伸的区域和不需要拉伸的区域。左侧窗格显示了预览区域。

在下一步中,我们将定义我们的补丁。当我们将光标移到图像上时,我们会看到非常浅的水平和垂直线。我们将拖动这些水平和垂直线来定义我们的补丁。

下面的截图显示了我们定义的补丁:

创建 NinePatch 图像

浅绿色垂直和水平区域显示了我们定义的补丁区域,这些是可拉伸的部分。如果我们错误地定义了补丁,那么当我们在其中放入内容时,它们将无法正确伸展以包含所有内容。这个工具会告诉我们是否有错误的补丁。在左侧窗格的右上角有一个名为显示错误补丁的按钮。点击它,如果我们的 9-patch 图像中有错误补丁,它会显示错误补丁。

下面的截图显示了错误补丁:

创建 NinePatch 图像

这些用红色边框标记的补丁是错误的补丁,它们不能正确伸展以包含其中的全部内容。我这样做只是为了清楚地说明错误补丁的外观。我们将调整水平和垂直线以获得正确的补丁,检查是否有错误补丁,然后我们将文件保存为MyFirst9Patch.9.png;这样我们就完成了第一个 NinePatch 图像的创建。

使用 NinePatch 图像

我们将从打开 Eclipse 并开始一个新的 Android 项目开始。我们将项目命名为MyFirst9PatchApp

下面的截图显示了新应用程序的配置设置:

使用 NinePatch 图像

在向导的下一步中,我们将提供基础的 PNG 文件作为我们应用程序的图标。

下面的截图显示了图标配置屏幕:

使用 NinePatch 图像

在下一步中,我们将主活动命名为MyFirst9PatchAppMainActivity。下面的截图显示了主活动配置屏幕:

使用 NinePatch 图像

我们将点击完成,这会完成向导的执行,并进入我们应用程序的设计视图。下面的截图显示了应用程序的设计视图:

使用 NinePatch 图像

接下来我们要做的是将我们的 NinePatch 图像复制到项目的res/drawable文件夹中,这样我们就可以在代码中使用这个 NinePatch 图像了。

接下来,我们将打开主活动的activity_my_first9_patch_app_main.xml文件,并在代码中创建一个按钮。整个Button代码如下所示:

<Button
id="@+id/btnninepatch"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_alignParentTop="true"
  android:layout_centerHorizontal="true"
  android:layout_marginTop="26dp"
  android:background="@drawable/myfirstninepatch"
  android:text="@string/buttonwith9patchimage"
  android:textSize="38sp" />

之前代码中以下两行是最重要的:

android:background="@drawable/myfirstninepatch"
android:text="@string/buttonwith9patchimage"

第一行显示了按钮将使用哪个 Drawable 工具作为其背景,第二行显示了按钮上要显示的文本内容。现在在上面的例子中,字符串名称是buttonwith9patchimage,它的值是背景带有 9 Patch 图像的按钮,并且我们需要添加更多文本以使按钮背景扩展超过 3 行。需要向按钮添加这么多文本是为了使其成为多行;这将使我们能够看到 NinePatch 图像的拉伸效果。

下面的屏幕截图显示了背景带有拉伸 NinePatch 图像的按钮:

使用 NinePatch 图像

到目前为止,很明显,我们主要将使用 NinePatch 图像来处理背景图形;尤其是当我们不知道想要包含内容的宽度和高度时。接下来我们要做的是更改应用程序主活动的整个背景。比如说,我们不喜欢活动的默认白色背景,我们需要一个自定义背景。在前面提到的应用程序中,我们将从 XML 文件中删除按钮代码,并在Layout标签中添加以下代码。

android:background="@drawable/myfirstninepatch"

上面的代码将使我们能够为整个活动使用可拉伸的背景。此标签的完整代码如下所示:

<RelativeLayout 

    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:background="@drawable/myfirstninepatch"
    tools:context=".MyFirst9PatchAppMainActivity" >  

</RelativeLayout>

当我们在模拟器中测试应用程序时,我们会看到以下内容:

使用 NinePatch 图像

在前面的图片中,我们可以看到我们的 NinePatch 盒图像已经垂直和水平拉伸以填满整个活动屏幕。这将为我们所有图形丰富的应用程序提供一个自定义的可拉伸背景。

总结

在本章中,我们了解到了一种可以拉伸以填满其所在容器的图像,这就是所谓的 NinePatch 图像。我们学习了这些可拉伸图像的重要性、它们的架构以及这些图像的基本理解。我们还学会了如何将简单图像转换为这些可拉伸图像,以及需要避免的常见错误。我们也学习了如何在 Android 应用程序中使用这些图像作为视图、控件背景,或者整个活动的背景。在下一章中,我们将学习关于用户交互和图形,如触摸事件。

第五章:触摸事件和画布上的绘图

在本章中,我们将学习如何与应用程序进行交互,捕捉触摸事件并在画布上对这些触摸事件做出图形渲染。为了实现这个目标,我们将开发一个非常简单的 Android 应用程序,在屏幕上显示一个图像。应用程序的核心功能是,当我们点击或触摸图像并在屏幕上拖动它时,图像会随着我们的手指从当前位置被拖动。当我们释放触摸并抬起手指时,图像停止移动。换句话说,我们将触摸图像并保持按下状态,将图像从屏幕上的点 A 拖放到点 B。

一些背景知识

我们首先需要明白屏幕充满了被称为像素的点。水平点是x,垂直点是y。每当我们把一个对象放在屏幕上,它就在一个特定的x, y位置。比如说,如果对象在屏幕的左上角,那么它的位置就是x = 0y = 0

在编写代码时,我们将设置应用程序首次运行时图像的默认位置。默认位置将在屏幕的左上角。因此,我们将为图像位置设置x = 0y = 0。这里最重要的时刻将是我们开始拖动图像时;每次触摸的x, y发生变化,我们都会将图像位置更新到触摸的当前位置。这样,它看起来就像我们从位置 A 拖动图像到位置 B。我们将首先监听触摸事件,然后捕获这些触摸事件来实现这一点。

之后,我们将捕获触摸的动作。动作是按下还是抬起?如果动作是按下,是否有移动?因为如果有移动且动作是按下,我们就需要进行拖动。

随着我们开发简单的应用程序并进行编码,将会有更多细节出现。

应用程序开发

我们将在 Eclipse 中创建一个名为Touch App的新项目。下面的截图显示了新 Android 应用程序向导的第一步:

应用程序开发

下面的截图显示我们已经选择了一个自定义创建的 PNG 绘图文件作为我们应用程序的图标:

应用程序开发

下面的截图显示我们需要从一个空白屏幕开始我们的项目,因为这是我们想要的游戏场所:

应用程序开发

下面的截图是向导的最后一步,显示了我们已经创建了一个名为TouchAppMainActivity的默认主活动,并且其布局文件名已自动填充:

应用程序开发

下面的截图显示我们的向导已经完成,现在我们有一个可工作的应用程序框架:

应用程序开发

从这一点开始,我们首先会从屏幕上删除Hello world!文本,因为我们要让屏幕完全空白,只显示我们将要拖动的图像。

接下来,我们将在硬盘上浏览到项目的res文件夹,并创建一个名为drawable的新文件夹,如果它尚未创建或res文件夹中的任何可用文件夹都可以使用,例如drawable-hdpi

然后,我们将drawing.png文件复制到那个文件夹中,并再次回到 Eclipse。我们将在包资源管理器中刷新项目文件。以下屏幕截图显示了res文件夹的最终状态:

应用程序开发

我们的自定义视图类

我们将打开扩展了Activity类的TouchAppMainActivity Java 文件。在这个类中,我们将创建另一个类,其中包含我们所有的核心功能,并扩展View类:

public class TouchAppView extends View {

定义类属性和对象

在这个类中,我们将定义一些全局对象和变量,以便它们对所有类中的方法可见。首先,我们将定义一个Paint对象:

  private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

接着是屏幕上x, y点的变量:

  private float x, y;

下面是一个布尔变量,用于检查是否正在发生触摸事件:

  boolean touching = false;

此后,我们将定义一个Bitmap对象,并提供我们自定义的drawing.png文件作为其源,该文件我们已复制到项目资源的drawable文件夹中:

  Bitmap drawingPic = BitmapFactory.decodeResource(getResources(),R.drawable.drawing);

接下来是我们将用来设置屏幕上图像默认位置的变量。初始化值为x, y位置的0, 0,图像将显示在屏幕的左上角。稍后,当我们改变图像的位置时,这些变量的值将相应更新。

  int drawingPic_x = 0;
  int drawingPic_y = 0;

接下来,我们捕获显示图像的宽度和高度:

  int drawingPic_w = drawingPic.getWidth();
  int drawingPic_h = drawingPic.getHeight();

然后,我们将定义偏移变量。偏移变量定义了一个对象相对于另一个对象或位置的相关位置。

  int drawingPic_offsetx;
  int drawingPic_offsety;

下面只是一个布尔变量,用于检查触摸动作。默认设置为false

  boolean dm_touched = false;

第一个布尔变量检查屏幕是否被触摸,第二个布尔变量检查在屏幕被触摸时,实际上触摸的是图像还是图像以外的其他点。

我们自定义视图类中的方法

现在,我们将以下四种方法添加到我们的类中:

  • 构造函数

  • 绘图方法

  • 测量方法

  • 触摸事件

构造函数

我们将定义构造函数,但暂时不会在其中放置任何功能。现在,我们只调用父级的上下文。

  public TouchAppView(Context context) {

    super(context);

  }

绘图方法

这是我们每次改变图像位置时为我们绘制位图图像的方法。我们希望旧的图像被移除,并在新的位置绘制相同的图像。

  protected void onDraw(Canvas canvas) {

    canvas.drawBitmap(drawingPic, drawingPic_x, drawingPic_y, paint);
  }

实际上,这个方法通过使用drawingPic作为源图像在画布上绘制位图。drawingPic是包含我们drawing.png图像的对象。它从初始化的变量中获取xy点,其中x是要绘制的位图的左侧位置,Y是要绘制的位图的顶部位置。最后,paint对象将绘制位图,但如果我们使用源位图,此对象的值可以为 null。

测量方法

这是一个方法,它将告诉系统视图及其内容在垂直和水平方向上需要多少空间。我们可以不使用这个方法来完成这个应用程序;然而,提到它是必要的,因为当我们在更复杂的图形应用程序中工作时,这个方法可以提供有价值的信息。代码如下:

  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
  }

触摸事件

我们将定义一个onTouchEvent()方法,该方法将接收一个动作作为参数。现在,动作可以是三种类型:移动、按下动作或按下按钮类型的动作,以及抬起动作或释放按钮类型的动作。我们将为这三种情况定义案例和功能,并将我们想要执行的操作逻辑整合到每种类型的动作中。

我们定义了onTouchEvent()方法:

  public boolean onTouchEvent(MotionEvent event) {

我们定义一个变量来存储动作值:

    int action = event.getAction();

注意

有关所有可用的属性和方法,可以访问以下链接:

developer.Android.com/reference/android/view/MotionEvent.html(链接内容不需要翻译,保留英文)

一个开关案例开始,它将获取动作并针对不同的场景进行检查,并根据每个动作执行操作,如下所示:

    switch(action){

如果动作是按下,将x, y值设置为当前事件的x, y位置,并将触摸变量设为true

      case MotionEvent.ACTION_DOWN:
      x = event.getX();
      y = event.getY();
      touching = true;

现在检查动作是否为按下,并且触摸的是图像,因为我们不希望图像因为触摸屏幕其他地方而被拖动。在以下代码中,我们将通过初始值检查我们图像的垂直和水平位置以及图像的宽度和高度。如果完全相同且一切返回true,我们将dm_touched变量设为true。这意味着图像被触摸了。

      if((x > drawingPic_x) && (x < drawingPic_x+drawingPic_w) && (y > drawingPic_y) && (y < drawingPic_y+drawingPic_h)){

既然我们知道图像被触摸了,我们将根据新的x, y位置更新图像的x, y位置,即从当前地点发生的事件中获得的位置。

        drawingPic_offsetx = (int)x - drawingPic_x;
        drawingPic_offsety = (int)y - drawingPic_y;
        dm_touched = true;
      }

      break;

执行前面的代码一次后,执行第二个案例中提到的案例代码。if语句将执行,因为现在我们已经确认执行了一个触摸事件,并且触摸的区域实际上是我们的图像。现在检查dm_touched变量是否为true,然后更新图像的x, y位置。起初,第一个案例中的代码将不会执行,因为dm_touched变量为false

      if(dm_touched){
        drawingPic_x = (int)x - drawingPic_offsetx;
        drawingPic_y = (int)y - drawingPic_offsety;
      }

      break;

如果动作是MOVE,则将之前定义的x, y变量值设置为当前事件的x, y值,并将触摸布尔变量设为true

      case MotionEvent.ACTION_MOVE:
      x = event.getX();
      y = event.getY();
      touching = true;

如果情况是ACTION_UP,这意味着我们正在抬起手指。释放触摸将简单地将touchingdm_touched布尔变量设为false

      case MotionEvent.ACTION_UP:
      default:
      dm_touched = false;
      touching = false;
    }

最后,我们将调用invalidate()方法,以便移除之前的绘图并在新参数下绘制全新的位图。

    invalidate();
    return true;
  }
}

我们的主活动类和最终输出

我们将进入主类TouchAppMainActivityonCreate()方法。在OnCreate方法中,我们将添加以下代码:

setContentView(new TouchAppView(this));

这个方法为我们提供了使用当前活动界面的用户界面的功能。如果我们定义了一个自定义视图,但没有将其设置为我们的自定义视图类,那么我们的自定义视图将不会出现。所以一切看起来都正常且不会生成错误,但应用程序将无法按计划运行。setContentView()的重要性在于,它是负责显示基于 XML 的布局甚至动态布局的方法。通过调用这个方法并将我们的自定义视图类,即TouchAppView作为参数传入,我们使应用程序能够执行在TouchAppView类中编写的任何代码。

下面的屏幕截图显示了应用程序在模拟器中测试时的最终输出:

我们的主活动类和最终输出

现在,我们将点击屏幕并保持按下状态,尝试用鼠标拖动图像。这将模拟触摸屏幕的操作;保持手指按下并在屏幕上从点 A 拖动图像到点 B。

下面的屏幕截图显示我们已经将图像从默认位置拖动到另一个位置:

我们的主活动类和最终输出

总结

在本章中我们学到了以下内容:

  • 为我们的图形应用程序创建一个自定义视图类

  • 捕获触摸事件

  • 识别在触摸屏幕时执行的操作以及可以执行的操作类型

  • 我们对触摸事件执行的操作的响应功能

  • 最后,我们将讨论如何在主活动类中实现这个自定义视图类

在下一章中,我们将汇总之前章节中学到的所有经验,并创建一个完整的程序,我们将在其中选择不同的颜色,并在画布上绘制我们自己的图形。这将是一个非常简单的类似画图的应用程序。

第六章:将它们整合在一起

最后,我们已经学完了使用 Android Canvas 的基础知识和技能的过程。在本章中,我们将从零开始逐步开发一个完整的应用程序,实现所有功能,使用我们在前几章中学到的所有知识和技巧。在本书的每一章中,我们都学习和练习了使用 Canvas 绘图的一个核心基本组件。现在,在本章中,我们将把这些组件与一些额外的功能结合起来,创建一个功能齐全的应用程序。

要创建任何应用程序或游戏,我们需要有一个计划、情节或故事板。故事板将告诉我们应用程序中事物的工作和展示方式,应用程序的核心功能或输出是什么,以及我们需要使应用程序有用和完整的一些额外或支持功能。计划可以是纸上的一些粗略的子弹笔记,也可以是一些手绘草图,但它将包含我们从它那里需要的所有需求和输出。附在绘图上的一些注释将完成我们的计划和对应用程序的整体构想。规划的好处在于它冻结了需求和我们的应用程序。在正常的软件工程中,在开始应用程序开发之前,会准备一份软件需求规范文档,其中写明了所有需求,并由双方签字。它定义了项目的边界,这非常重要。如果没有定义需求的边界或限制,应用程序的开发将永无止境,因为在开发过程中,随着时间的推移,需求会增加。所以我们必须有一套明确的需求和功能。以后,我们可以在应用程序的新版本中添加更多功能,但至少基础应用程序中的核心需求和功能将是完整的。

故事板

我们将从应用程序的线框开始,上面有一些注释、指针和部分内容;然后我们会解释这个草图。

下图展示了我们将要开发的应用程序的故事板:

故事板

前面的图表非常直观,为我们提供了完整的设计草图,展示了我们计划要做的事情。最顶部区域显示应用程序标题,如果我们创建应用程序时使用了小图标,它也会显示出来。标题栏下方是主要的空白绘图区域,我们将在那里进行所有的自由手绘和绘画。在屏幕底部,我们将有一个控制面板,从中可以选择功能以及绘图和绘画风格。控制面板将帮助我们选择画笔的颜色。我们可以改变画笔的大小。我们可以执行某些功能,如创建新绘图、保存现有绘图、选择画笔大小、点击橡皮擦以选择它,以及擦除当前绘制的画作。我们将使用手指和触摸事件在 Canvas 上绘图。稍后,我们可以选择保存擦除,或者更改颜色和画笔大小,然后继续我们的绘画。所以我们的目标是开发一个类似 Paint 的简单 Android 应用程序。

项目和应用开发

我们将通过将项目划分为四个阶段来实现我们的目标。在开发过程中,我们会逐步完成每个阶段,最终开发出我们自己的绘图应用程序。以下是各个阶段:

  • 用户界面

  • 启用触摸和绘画

  • 启用颜色选择

  • 为我们的应用程序增加更多功能

用户界面

我们将通过在 Eclipse 中通过向导创建一个新的 Android 应用程序来开始我们的项目,正如我们在本书前面练习的那样。仅附上了新 Android 应用程序向导的第一屏。

下面的截图展示了新 Android 应用程序向导的第一屏:

用户界面

我们将我们的应用程序命名为OurFirstPaintApp项目名称将会自动填充,我们将包名称更改为com.learningandroidcanvasmini。在开发这个应用程序的过程中,我们将在项目的res文件夹中的 XML 文件和src文件夹中的 Java 文件中工作。

屏幕方向

在开始之前,我们需要决定我们的项目将支持哪种方向。假设我们希望我们的项目应用程序即使在用户横屏持设备时也始终保持纵向形式。为此,我们将打开AndroidManifest.xml文件,并将Activity标签中的android:screenOrientation="landscape"更改为android:screenOrientation="portrait"

画笔

从我们的故事板中,我们知道需要使用不同大小的画笔;为此,我们将定义一些数字,这些数字将代表特定的画笔大小。我们将转到 res/values 文件夹,并打开 dimens.xml 文件。如果文件不在那里,我们将创建一个名为 dimens.xml 的新 XML 文件,并将我们的值放入其中。在这个文件中,我们将查找 <resources></resources> 标签。在这个标签内部,我们将按如下方式放置我们的值:

<!-- Available brushes --> 
<dimen name="small_brush">10dp</dimen>
<integer name="small_size">10</integer>
<dimen name="medium_brush">20dp</dimen>
<integer name="medium_size">20</integer>
<dimen name="large_brush">75dp</dimen>
<integer name="large_size">75</integer>

我们将保持尺寸和整数值相同,以便用户界面在我们绘图时可以显示确切的画笔大小。

设计控制面板

在我们故事板草图中的控制面板中,我们有一排颜色和另一排执行某些功能的按钮。现在我们将开始设计控制面板。首先,我们将添加所有在控制面板中使用的字符串。为此,我们将打开 res/values 文件夹中的 string.xml 文件,并将以下代码添加到文件中:

<string name="start_new_painting">New Painting</string>
<string name="brush_size">Brush Size</string>
<string name="erase">Erase</string>
<string name="save_painting">Save Painting</string>
<string name="paint">Paint</string>

设置布局

首先,我们将从网上创建或下载以下图片,并将它们复制到 res/drawable 文件夹中。以下屏幕截图显示了 res/drawable 文件夹中的图片:

设置布局

现在,我们将为我们的画布和控制面板设置布局,绘图空间以及将容纳我们按钮的空间。我们将打开 activity_main.xml 文件。在主 Layout 标签内部,我们将输入以下代码片段中提到的三个子布局标签。第一个将包含新建、画笔、擦除和保存按钮的图像,接下来的两个将分别包含一排颜色。

<LinearLayout
  android:layout_width="wrap_content"
  android:layout_height="100dp"
  android:layout_gravity="center"
  android:orientation="horizontal">

  <!-- content code starts here -->
  <ImageButton
    android:id="@+id/new_btn"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:contentDescription="@string/start_new"
    android:src="img/new_pic" />

  <ImageButton
    android:id="@+id/draw_btn"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:contentDescription="@string/brush"
    android:src="img/brush" />

  <ImageButton
    android:id="@+id/erase_btn"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:contentDescription="@string/erase"
    android:src="img/eraser" />

  <ImageButton
    android:id="@+id/save_btn"
    android:layout_width="wrap_content"
    android:layout_height="fill_parent"
    android:contentDescription="@string/save"
    android:src="img/save" />
  <!-- content code ends here -->

</LinearLayout>

接下来,我们将添加两个更多布局,我们将在其中有两排彩色按钮,如下所示:

<LinearLayout
  android:id="@+id/paint_colors"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:orientation="horizontal" >

  <ImageButton android:layout_width="@dimen/large_brush"
    android:layout_height="@dimen/large_brush"
    android:layout_margin="2dp"
    android:background="#FF660000"
    android:contentDescription="@string/paint"
    android:onClick="paintClicked"
    android:src="img/paint"
    android:tag="#FF660000" />
.
.
.

前面代码中的点代表我们想要添加的其余颜色按钮的代码。在前面代码块中唯一需要更改的是 android:tag="#FF660000" 代码,这是颜色的值。现在我们已经为控制面板准备好了布局。我们需要在布局中为我们的绘图预留一些空间。这里,我们将在 src 文件夹中创建一个名为 CustomDrawingView 的新 Java 类,该类将扩展 View。目前,我们只创建类的骨架代码,以便可以在布局 XML 文件中引用它。稍后,我们将定制 CustomDrawingView 类的每个部分,并加入我们希望应用程序拥有的所有功能。骨架代码如下所示:

import android.content.Context;
import android.util.AttributeSet;

public class CustomDrawingView extends View
{
  public CustomDrawingView(Context context, AttributeSet attrs){
    super(context, attrs);
    setupDrawing();
}

private void setupDrawing(){
  //get drawing area setup for interaction
}

}

这样,我们已经添加了一个 CustomDrawingView 类以及它的构造函数和 setupDrawing() 方法。这些是目前最核心的组件。没有构造函数,程序将生成错误。现在,我们将回到 activity_main.xml 文件,并在父级 layout 标签的打开标签下方添加以下代码:

<com.learningandroidcanvasmini.ourfirstpaintapp.CustomDrawingView
  android:id="@+id/drawing"
  android:layout_width="fill_parent"
  android:layout_height="0dp"
  android:layout_marginBottom="3dp"
  android:layout_marginLeft="5dp"
  android:layout_marginRight="5dp"
  android:layout_marginTop="3dp"
  android:layout_weight="1"
  android:background="#FFFFFFFF" />

我们 XML 文件中的代码行<com.learningandroidcanvasmini.ourfirstpaintapp.CustomDrawingView显示了我们的自定义视图的包目录。其次,这也告诉我们如何在我们的 XML 布局文件中添加基于自定义代码的布局。

尽管我们的布局几乎已经准备好了,但仍然不完整。我们需要在控制面板中添加适当的矩形颜色选择按钮。到目前为止,我们已经定义了图像按钮,但还需要一些配置,比如这些按钮的形状。我们希望这些按钮是略带圆角的正方形。为此,我们将在res/drawables文件夹中创建一个新的paint.xml文件。在这个文件中,我们将使用资源 XML 中的 Drawables 技术。我们将创建一个双层 Drawable 形状:一层用于矩形对象,另一层用于圆角。

<layer-list  >
<item>
  <shape android:shape="rectangle" >
    <stroke
      android:width="5dp"
      android:color="#FF999999" />
    <solid android:color="#ff000000" />
    <padding
      android:bottom="0dp"
      android:left="0dp"
      android:right="0dp"
      android:top="0dp" />
  </shape>
</item>
<item>
  <shape  >
    <stroke  android:width="4dp"  android:color="#FF999999" />
    <solid android:color="#ff000000" />
    <corners android:radius="15dp" />
  </shape>
</item>
</layer-list>

在前面的代码中,我们使用了 layer-list 标签,在其中我们使用了两个项目。第一个项目用于矩形形状,第二个用于圆角。我们将保存所有文件,并在模拟器中运行我们的代码。

下面的截图展示了我们没有任何功能的应用程序:

设置布局

到目前为止,我们已经完成了用户界面并完成了图形工作。现在,我们需要使我们的应用程序能够响应触摸。

启用触摸并使用触摸绘画

目标是启用触摸:换句话说,我们的应用程序将在我们拖动手指在屏幕上时进行绘画。我们将从打开我们的CustomDrawingView类开始,并添加以下对象和变量:

private Path drawPath;
private Paint drawPaint, canvasPaint;
private int paintColor = 0xFF660000;
private Canvas drawCanvas;
private Bitmap canvasBitmap;

在此之后,在setupDrawing()方法中,我们将实例化PathPaint对象,并将其不同的属性设置为某些我们希望它获得的默认值:

drawPath = new Path();
drawPaint = new Paint();
drawPaint.setColor(paintColor);
drawPaint.setAntiAlias(true);
drawPaint.setStrokeWidth(20);
drawPaint.setStyle(Paint.Style.STROKE);
drawPaint.setStrokeJoin(Paint.Join.ROUND);
drawPaint.setStrokeCap(Paint.Cap.ROUND);

最后,在方法中,我们将实例化 canvasPaint 对象:

canvasPaint = new Paint(Paint.DITHER_FLAG);

现在,我们将跳转到onDraw()方法,并添加以下代码行:

canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
canvas.drawPath(drawPath, drawPaint);

onDraw()方法接收一个类型为Canvas的对象作为参数,用于绘制位图和我们想要的绘图或绘画。每次我们触摸屏幕并拖动手指,Canvas上之前的绘画将调用invalidate()方法,onDraw()方法将自动被调用,显示我们当前的绘画。

我们几乎已经准备好了一切,但我们还没有使我们的应用程序能够响应触摸。为此,我们将向我们的CustomDrawingView类添加一个onTouchEvent()方法,如下所示:

public boolean onTouchEvent(MotionEvent event) {
}

在这个方法中,我们需要完成两项工作:检测用户的触摸动作并检查移动的方向。触摸可以是向上的,意味着手指没有接触屏幕;也可以是向下的,意味着手指按在屏幕上。动作可以是按下的手指向任何方向移动。首先,我们将使用以下代码获取触摸的 x、y 位置:

float touchX = event.getX();
float touchY = event.getY();

然后,我们将根据以下开关代码中包含的可能情况之一响应触摸事件:

switch (event.getAction()) {

当按下动作事件时,手指按下。将绘制点移动到触摸的位置:

case MotionEvent.ACTION_DOWN:
  drawPath.moveTo(touchX, touchY);
  break;

通过动作事件,手指按住并向某个方向拖动。首先,绘制点将是触摸的点,然后在拖动动作期间沿着手指的移动绘制一条线:

case MotionEvent.ACTION_MOVE:
  drawPath.lineTo(touchX, touchY);
  break;

最后,当手指抬起时,在 Canvas 上将会绘制一条路径,并且Path对象将被刷新,以便它准备好从下一个触摸位置开始绘制新线:

case MotionEvent.ACTION_UP:
  drawPath.lineTo(touchX, touchY);
  drawCanvas.drawPath(drawPath, drawPaint);
  drawPath.reset();
  break;
  default:
  return false;
}

最后,我们将调用invalidate()方法,以便我们可以激活onDraw()方法:

invalidate();
return true;

整个类的代码如下:

public class CustomDrawingView extends View {
  private Path drawPath;
  private Paint drawPaint, canvasPaint;
  private int paintColor = 0xFF660000;
  private Canvas drawCanvas;
  private Bitmap canvasBitmap;
  public CustomDrawingView(Context context, AttributeSet attrs){
    super(context, attrs);
    setupDrawing();
  }
  private void setupDrawing(){
    drawPath = new Path();
    drawPaint = new Paint();
    drawPaint.setColor(paintColor);
    drawPaint.setAntiAlias(true);
    drawPaint.setStrokeWidth(20);
    drawPaint.setStyle(Paint.Style.STROKE);
    drawPaint.setStrokeJoin(Paint.Join.ROUND);
    drawPaint.setStrokeCap(Paint.Cap.ROUND);
    canvasPaint = new Paint(Paint.DITHER_FLAG);
  }
  @Override
  protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
    drawCanvas = new Canvas(canvasBitmap);
  }
  @Override
  protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(canvasBitmap, 0, 0, canvasPaint);
    canvas.drawPath(drawPath, drawPaint);
  }
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    float touchX = event.getX();
    float touchY = event.getY();
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      drawPath.moveTo(touchX, touchY);
      break;
    case MotionEvent.ACTION_MOVE:
      drawPath.lineTo(touchX, touchY);
      break;
    case MotionEvent.ACTION_UP:
      drawPath.lineTo(touchX, touchY);
      drawCanvas.drawPath(drawPath, drawPaint);
      drawPath.reset();
      break;
    default:
      return false;
    }
    invalidate();
    return true;
  }
}

启用颜色选择

我们将启用之前创建的用户界面进行颜色选择。我们知道我们有两行不同的颜色。现在我们需要应用程序能够将画笔颜色设置为从可用调色板中选择的颜色。为此,我们将打开主活动类,并在主活动类的导入部分添加ViewImageButtonLinearLayoutimport语句:

import android.view.View;
import android.widget.ImageButton;
import android.widget.LinearLayout;

然后,在我们的主活动类内部,我们将创建我们CustomDrawingView类的一个对象:

private CustomDrawingView drawView;

在主活动类的onCreate()方法中,我们将通过获取其在activity_main.xml文件中定义的布局引用来实例化drawView对象:

drawView = (CustomDrawingView)findViewById(R.id.drawing);

我们将添加另一个对象,以便我们知道哪个画笔按钮被点击了:

private ImageButton imgBtnSelectedPaint;

接下来,在onCreate()方法中,我们将添加以下代码。首先,我们会获取承载我们画笔按钮的布局。paint_colors属性在activity_main.xml文件中定义:

LinearLayout paintLayout =(LinearLayout)findViewById(R.id.paint_colors);

然后,我们将选择顶部颜色行中的第一个颜色作为默认选中颜色:

imgBtnSelectedPaint = (ImageButton)paintLayout.getChildAt(0);

为了区分选中颜色按钮与其他按钮,我们将为选中的按钮使用不同的 Drawable。为了给选中的按钮一个单独的Drawable对象,我们将在drawable文件夹中先定义一个单独的 XML 文件。XML 文件中选中按钮的代码如下所示:

<layer-list  >
    <item>
      <shape android:shape="rectangle" >
        <stroke
          android:width="4dp"
          android:color="#FF333333" />
          <solid android:color="#00000000" />
          <padding
            android:bottom="0dp"
            android:left="0dp"
            android:right="0dp"
            android:top="0dp" />
        </shape>
    </item>
    <item>
      <shape  >
        <stroke
          android:width="4dp"
          android:color="#FF333333" />
          <solid android:color="#00000000" />
          <corners android:radius="10dp" />
        </shape>
    </item>

</layer-list>

代码与我们在drawable文件夹中为普通画笔按钮创建的前一个 XML 文件完全相同。这里唯一的区别是android:colorsolid android:color的不同值。在此之后,我们将打开主活动类,并在最近添加的图片按钮代码下面的onCreate()方法中,添加以下行以启用选择按钮的不同样式:

imgBtnSelectedPaint.setImageDrawable(getResources().getDrawable(R.drawable.paint_pressed));

接下来,在CustomDrawingView类中,我们将创建一个方法来更新我们在绘制绘画时使用的画笔颜色:

public void setColor(String newColor){
  invalidate();
  paintColor = Color.parseColor(newColor);
  drawPaint.setColor(paintColor);
}

我们很快将在主活动类中使用前面的方法。请注意,在 XML 文件中的图像按钮代码中,我们提到了一个onClick()方法,方法名为paintClicked。现在是时候在我们的主活动类中创建paintClicked方法了。我们将从以下方式定义该方法开始:

public void paintClicked(View view){

现在,我们将检查点击的颜色按钮是否已经被选择:

if(view!=imgBtnSelectedPaint){ 

如果没有选择,我们将从点击的按钮获取标签,并将当前绘画颜色设置为选中标签的颜色:

ImageButton imgView = (ImageButton)view;
String color = view.getTag().toString();

以下代码行将调用在CustomDrawingView类中先前创建的setColor()方法,以改变画笔颜色:

drawView.setColor(color);

在此之后,我们将简单地更新控制面板,通过更改选中按钮的 Drawable 来显示选中的按钮已被修改;我们将把之前选中的按钮恢复到正常状态:

imgView.setImageDrawable(getResources().getDrawable(R.drawable.pa int_pressed));
imgBtnSelectedPaint.setImageDrawable(getResources().getDrawable(R.drawable.paint));
imgBtnSelectedPaint=(ImageButton)view;
}}

我们的进度状态显示,我们已经完成了项目的四分之三:

  • 我们的应用程序已经拥有相对吸引人的用户界面。

  • 我们的应用程序支持触摸操作。

  • 我们能够通过手指触摸和拖动进行绘画;此外,我们还可以更改绘画时所使用的颜色或画笔。

下面的截图展示了我们当前的状态,带有一个粗糙的绘画:

启用颜色选择

我试图在如图所示的画笔按钮上绘制一个画笔。我不是一个好的艺术家,这甚至不是一个好的绘画,但它足以解释我们目前的位置。

为我们的应用程序增加更多功能,使其更加丰富。

我们的应用程序核心结构已经完成,但我们仍应使其更有用、更吸引人;我们需要通过增加更多功能来让它更加有趣,而不仅仅是提供颜色选择和绘画。我们将从将绘画保存到设备的功能开始。

保存绘画。

我们将在主活动类中创建savePaintingButton的实例:

private ImageButton savePaintingButton;

接下来,我们将在主活动类的onCreate()方法中创建它的实例,并监听其点击事件:

savePaintingButton = (ImageButton)findViewById(R.id.save_btn);
savePaintingButton.setOnClickListener(this);

当我们为点击监听器编写前面的代码行时,下面会出现一条红线,警告我们有问题。右键点击这条线将给我们所有可能解决问题的选项。选择最能解决问题的选项。假设在示例中,我们选择了主活动实现继承的OnClickListner的选项;会发生两件事。首先,我们的主活动类将继承OnClickListner,如代码行所示:

public class MainActivity extends Activity implements OnClickListener {

其次,以下方法将在我们的主活动类内部创建,其内容为空。

public void onClick(View v) {
}

现在,我们将在前面的方法内编写我们的保存逻辑。为了保存我们的画作,我们将从包含我们画作的布局开始,并为它启用绘图缓存。然后,我们将创建一个Bitmap对象,并提供一个带有我们视图对象的绘图缓存。

View ourView = findViewById(R.id.drawing);
ourView.setDrawingCacheEnabled(true);
Bitmap ourBitmapImage = ourView.getDrawingCache();

接下来,我们将定义一个字符串,其中包含我们保存文件的路径,并创建一个File对象,该对象将提供该路径:

String extr = Environment.getExternalStorageDirectory().toString()
  + "/SaveCapture";
File myPath = new File(extr);

如果路径不存在,我们将在该位置创建一个新目录,如果路径存在,我们将在该位置创建我们的文件,如下所示:

if (!myPath.exists()) {
  boolean result = myPath.mkdir();
}
  myPath = new File(extr, getString(R.string.app_name) + ".jpg");

之后,我们将创建一个FileOutputStream对象。如果文件存在,输出流将字节写入文件;如果不存在,将创建一个新文件,并将流写入其中。我们之前创建的带有我们绘图缓存的Bitmap对象,我们将使用它来调用compress()方法,提供压缩类型信息和FileOutputStream对象,以创建具有JPEG扩展名的图像文件。为了追踪它,即查看画作信息是如何进入这个 JPEG 文件的,Bitmap对象提供了包含myPath对象的FileOutputStream对象。myPath对象包含文件必须保存的完整路径以及保存文件的名称。此外,Bitmap对象已经从我们创建的View对象中获取了画作的必要信息,以及特定时刻绘图缓存中可用的内容。代码块解释了输出流对象的创建,并将其提供给我们已经创建的Bitmap对象。try块中的最后一行使用了MediaStoreMediaStore包含外部和内部存储设备上所有可用媒体的元数据。MediaStoreinsertImage将插入图像,并在我们的图库中为其创建缩略图:

FileOutputStream fos = null;
  try {
    fos = new FileOutputStream(myPath);
    ourBitmapImage.compress(Bitmap.CompressFormat.JPEG, 100, fos);
    fos.flush();
    fos.close();
    MediaStore.Images.Media.insertImage(getContentResolver(), ourBitmapImage,
      "Screen", "screen");
  }

  catch (FileNotFoundException e) {
    Log.e("Error", e + "");
  }

  catch (Exception e) {
    Log.e("Error", e + "");
  }

最后,我们将在AndroidManifest.xml文件中添加以下权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

如果没有这些权限,我们的应用程序将不允许从存储驱动器中读取或写入。有关权限及其理解的更多详细信息,请访问链接developer.android.com/guide/topics/security/permissions.html

在这段代码的末尾,我们的保存功能完成了。现在我们可以在图库中绘制并保存我们的画作。以下屏幕截图显示了已保存的画作:

保存画作

如果我们查看图库,会发现如果我们在查看新创建图像的详情时,它会显示图像的缩略图、名称和创建日期。我们的图像以我们的应用程序名称保存,因为这是我们代码中提供的文件名。我们还必须确保我们的模拟器创建时包含 SD 卡选项。如果我们的模拟器在未选择 SD 卡选项的情况下创建,这段代码将无法工作。

创建新绘图

要创建新绘画或启用新按钮的功能,我们需要清除画布上当前绘制的任何内容,并清除之前的绘图缓存。为此,我们将创建一个新按钮的实例、一个监听器以及一个onCreate()方法,就像我们对savePaintingButton按钮所做的那样。我们将在新按钮的监听器中添加以下代码:

drawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
invalidate();

上述代码将清除屏幕和缓存。我不会详细介绍PorterDuff,但只想在这里写一下它不是 Android 特有的。PorterDuff是实现 Porter 和 Duff 定义的 12 条规则的AlphaComposite类。关于 Android 的更多详细信息可以在以下链接中找到:

PorterDuff 参考文档

在我们的应用程序中启用橡皮擦

在本章中,我们将为我们的应用程序增加一个功能:使用橡皮擦。我知道我不是毕加索,而且在我的绘画应用程序中我肯定需要橡皮擦。因此,我们将编写更多代码,使我们的应用程序支持使用橡皮擦。首先,我们将创建一个橡皮擦按钮的实例,并为它设置一个监听器,就像我们对savePaintingButton按钮所做的那样。实现橡皮擦功能的最简单方法是将画笔颜色设置为背景色。这样看起来我们像是在擦除绘图,但实际上,我们是用设置为背景色的画笔颜色来绘制。

这是我们选择画笔的代码:

if(view!=imgBtnSelectedPaint){
  ImageButton imgView = (ImageButton)view;
  String color = view.getTag().toString();
  drawView.setColor(color);
  imgView.setImageDrawable(getResources().getDrawable(R.drawable.paint_pressed));
  imgBtnSelectedPaint.setImageDrawable(getResources().getDrawable(R.drawable.paint));
  imgBtnSelectedPaint=(ImageButton)view;

我们知道我们的背景色是白色,从我们的 XML 布局文件中,我们知道我们为白色定义的标签是#FFFFFFFF,我们白色按钮的背景色是#FFFFFFFF。因此,要在我们的应用程序中启用擦除功能,我们只需将String color = view.getTag().toString();的值更改为String color = "#FFFFFFFF";

保存文件并运行应用程序。现在我们的应用程序将具有擦除功能。以下屏幕截图显示了我们应用程序中使用的橡皮擦,针对我们之前的画笔绘图:

在我们的应用程序中启用橡皮擦

随着橡皮擦功能的实现,我们基本的绘图应用程序已经完成,但说到增加趣味,额外的功能不仅限于新画、擦除和保存。只要我们的想象力足够,我们可以加入许多其他功能。但有一点很清楚:无论我们计划加入什么功能,我们都会用到本书中讲解的内容。虽然实现的方法和顺序可能有所不同,但就基本的 2D 图形而言,本书中的课程和示例将会涵盖。

总结

在本章中,我们学到的内容比一开始承诺的要多。我们学到的东西如下:

  • 规划我们的应用程序和故事板的使用

  • 使用嵌套布局创建丰富、复杂的用户界面

  • 仅使用 XML 创建图形对象

  • 创建我们自己的独立View类并在 XML 文件和主活动文件代码中引用该类

  • 使用PathPaintBitmapCanvas对象

  • 定义监听器并捕获触摸事件

  • 对触摸事件做出响应

  • 切换画笔的颜色

  • 与文件系统和输出流合作

  • 将文件保存到存储设备

  • AndroidManifest.xml文件中添加权限

  • 清除画布、绘图缓存,并创建新的绘图

  • 使用非常简单的逻辑擦除已经绘制的画作

我们已经读到了这本书的结尾。本书仅关注最基础的内容,从 0 级到中级关于使用 Android 的 Canvas 进行工作的知识。尽管如此,本书中包含的知识适用于所有类型的基本的 2D Android 图形应用程序。对于更复杂的带有动画的应用程序,我们可能需要更高级的 Android Canvas 知识和一些在 OpenGL ES 中处理 3D 的核心知识。OpenGL ES 是针对嵌入式设备或手持设备的 OpenGL 版本。

posted @ 2024-05-23 11:07  绝不原创的飞龙  阅读(3)  评论(0编辑  收藏  举报