OpenGL ES 3.x游戏开发(上卷)

978-7-115-43349-7
作者: 吴亚峰
译者:
编辑: 张涛

图书目录:

详情

本书共分为16章,内容按照必知必会的基础知识、基于OpenGL ES 3.x实现基本特效以及真实大型应用、游戏案例的顺序进行详细地讲解。为了便于读者学习,本套书附赠的光盘中包含了书中所有案例的完整源代码,最大限度地帮助读者快速掌握各方面的开发技术。

图书摘要

版权信息

书名:OpenGL ES 3.x游戏开发(上卷)

ISBN:978-7-115-43349-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    吴亚峰

  责任编辑 张 涛

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


本书共分17章,内容按照必知必会的OpenGL ES 3.x基础知识、基于实现基本特效以及真实大型应用、游戏案例开发的顺序进行详细地讲解。主要内容为:OpenGL ES 3.x的渲染管线介绍,OpenGL ES 3.x可编程渲染管线着色器的着色语言介绍,3D开发中投影、各种变换的原理与实现,介绍了OpenGL ES 3.x中光照的基本原理与实现、点法向量与面法向量的区别以及光照的每顶点计算与每片元计算的差别,介绍了纹理映射的基本原理与使用,介绍了使用OpenGL ES 3.0开发各种3D基本形状的实现,包括圆柱、圆锥、圆环、螺旋管、几何球以及贝塞尔曲线旋转面等内容,讲解了3D模型加载,介绍了混合和雾的基本原理与使用以及常用3D开发技巧,如标志板、天空盒、镜像绘制等,介绍了OpenGL ES 3.0中经常使用的几种剪裁与测试,包括剪裁测试、Alpha测试、模板测试以及任意剪裁平面等,讲解了传感器应用开发和Android 及iOS平台下的OpenGL ES开发。最后,通过两大案例,如休闲类游戏3D可爱抓娃娃和3D楼盘展示系统把所学的知识应用起来,让读者尽快进入实战角色。

本书适合OpenGL ES 3D应用开发的初学者、程序员、游戏开发者,也适合作为大专院校相关专业师生的学习用书以及培训学校的教材。


随着智能手机硬件性能的不断提升,如水果忍者、极品飞车、狂野飙车等一批优秀的3D游戏娱乐应用在众多智能机用户间流行开来。与此同时,也带动了手机游戏产业逐渐从2D走向3D。但目前国内专门介绍这方面开发的书籍与资料都非常少,同时3D应用开发的门槛又比较高,使得很多初学者无从下手。根据这种情况,笔者结合多年从事3D游戏应用开发的经验编写了本书。

了解一些3D开发领域的技术人员都知道,移动嵌入式平台上的3D应用开发主要是基于OpenGL ES的3个版本,1.x、2.x和3.x。OpenGL ES 1.x是较老的版本,渲染能力很有限,留给开发人员发挥的空间也很有限。OpenGL ES 3.x新特性的添加使渲染的3D场景光影效果更加真实,所以,它的应用越来越多。

本书中在给出实际案例时涉及了Android SDK、Android NDK、iOS、WebGL等主流平台,充分考虑到了各个不同主流目标平台读者的需求。同时,本书中关于OpenGL ES 3.x可编程渲染管线着色器的知识与技术还可以通用于其他的很多移动嵌入式平台,因此,学习各种移动嵌入式平台下高级3D应用开发的读者都可以参考此书。

1.内容丰富,由浅入深

本书组织上本着“起点低,终点高”的原则,内容覆盖了从学习OpenGL ES 3.x必知必会的基础知识到基于OpenGL ES 3.x实现各种高级特效,书的最后还给出了完整的大型3D游戏或应用案例。这样的内容组织可以帮助移动嵌入式3D应用开发的初学者一步一步成长为3D开发的达人,满足绝大部分想学习3D应用开发的学生与程序开发人员以及相关技术人员的需求。

2.结构清晰,讲解到位

本书中配合每个需要讲解的知识点都给出了丰富的插图与完整的案例,使得初学者易于上手,有一定基础的读者便于深入。书中所有的案例均是根据笔者多年的开发心得进行设计的,结构清晰明朗,便于读者进行学习。同时书中还给出了很多笔者多年来积累的编程技巧与心得,具有很高的参考价值。

3.完整的源程序

为了便于读者学习,本书附赠了书中所有案例的完整源代码,最大限度地帮助读者快速掌握各方面的开发技术。源程序下载地址为www.toppr.net。

本书分为17章,内容按照必知必会的基础知识、基于OpenGL ES 3.x实现基本特效以及真实大型应用、游戏案例的顺序进行详细讲解。

章 名

主 要 内 容

Android概述

本章简要介绍了Android的特点、开发环境的搭建以及运行机制

游戏开发相关的Android基础知识

本章主要介绍了基于Android平台开发游戏所必需了解的一些基础知识,包括音效、数据库、I/O以及网络通信等

初识OpenGL ES 3.x

本章介绍了OpenGL ES的一些基本知识,内容主要包括OpenGL ES 3.x与2.0的渲染管线以及一个简单的案例

着色语言Shading Language

本章对用于实现OpenGL ES 3.x可编程渲染管线着色器的着色语言进行了系统地介绍,为后面各方面的深入学习打好基础

必知必会的3D开发知识——投影及各种变换

本章介绍了3D开发中投影、各种变换的原理与实现,同时还介绍了几种不同的绘制方式

光照

本章介绍了OpenGL ES 3.x中光照的基本原理与实现、点法向量与面法向量的区别以及光照的每顶点计算与每片元计算的差别

纹理映射

本章介绍了纹理映射的基本原理与使用,同时还介绍了不同的纹理拉伸与采样方式、多重过程纹理技术以及压缩纹理

3D基本形状的构建

本章介绍了使用OpenGL ES 3.0开发各种3D基本形状的原理与实现,包括圆柱、圆锥、圆环、螺旋管、几何球以及贝塞尔曲线旋转面等内容

更逼真的游戏场景——3D模型加载

本章介绍了如何使用自定义的加载工具类直接加载使用3DMax创建的3D立体物体模型

独特的场景渲染技术——混合与雾

本章主要介绍了混合以及雾的基本原理与使用

常用的3D开发技巧——标志板、天空盒、镜像绘制等

本章主要介绍了一些常用的3D开发技巧,包括标志板、灰度图地形、粒子沉积地形、天空盒与天空穹、镜像技术、动态文本输出以及非真实感绘制等

渲染出更加酷炫的3D场景——几种剪裁与测试

本章主要介绍了OpenGL ES 3.0中经常使用的几种剪裁与测试,包括剪裁测试、Alpha测试、模板测试以及任意剪裁平面等

引人入胜的游戏特性——传感器应用开发

本章介绍了Android手机中各种传感器的使用,包括加速度传感器、磁场传感器、光传感器、温度传感器、接近传感器以及姿态传感器等,同时还给出了使用重力传感器进行3D应用体感操控的案例

Android NDK及iOS平台下的OpenGL ES开发

本章介绍了Android NDK以及iOS平台下基于OpenGL ES 3.0的3D应用开发,同时还介绍了如何在不同的平台间进行3D应用的移植

WebGL 3D应用开发

本章介绍了WebGL平台下基于OpenGL ES的3D应用开发,同时还介绍了如何将3D应用移植到WebGL平台

休闲类游戏——3D可爱抓娃娃

本章给出了一款完整的3D休闲类游戏案例——3D可爱抓娃娃,该游戏模仿游乐场中的夹娃娃机,全触控操作。内容涉及3D场景的渲染、JBullet物理引擎的使用、音效设置等功能的开发

基于WebGL的3D楼盘展示系统

本章给出了一个基于WebGL开发的3D楼盘展示系统,内容涉及楼房模型和户体模型的制作、查看楼房和户型的展示、整个小区的漫游和自由浏览等各种功能的开发

本书内容丰富,从基本知识到高级特效;从简单的应用程序到完整的3D游戏案例,适合不同需求、不同水平层次的读者。

本书内容包括在各个主流平台下进行3D应用开发的知识,内容由浅入深,配合详细的案例,非常适合3D游戏、应用的初学者循序渐进地学习,以及有一定3D开发基础希望进一步深入学习OpenGL ES 3.x高级开发技术的读者学习参考。

本书不仅包括了OpenGL ES 3.x开发的基础知识,同时也包括了基于OpenGL ES 3.x实现高级特效以及完整的游戏案例,有利于有一定基础的开发人员进一步提高开发水平与能力。

吴亚峰,毕业于北京邮电大学,后留学澳大利亚卧龙岗大学取得硕士学位。1998年开始从事Java应用的开发,有10多年的Java开发与培训经验。主要的研究方向为OpenGL ES、手机游戏、Java EE以及搜索引擎,成为手机游戏、Java EE独立软件开发工程师,并兼任百纳科技Java培训中心首席培训师。近10年来为多家著名企业培养了上千名高级软件开发人员,曾编写过《OpenGL ES 2.0游戏开发》《Unity 4 3D开发实战详解》《Android 3D游戏案例开发大全》《Android应用案例开发大全》(第一版、第二版及第三版)和《Android游戏开发大全》(第一版、第二版及第三版)等多本畅销技术书籍。2008年初开始关注Android平台下的3D应用开发,并开发出一系列优秀的Android应用程序与3D游戏。

本书在编写过程中得到了唐山百纳科技有限公司Java培训中心的大力支持,同时李玲玲、贺蕾红、张月月、张双彐、刘佳、陆晓鸽、王冬、程祎以及作者的家人为本书的编写提供了很多帮助,在此表示衷心地感谢!

由于笔者的水平和学识有限,且书中涉及的知识较多,书中难免存有错误与疏漏,敬请广大读者批评指正,并多提宝贵意见。本书责任编辑联系邮箱为:zhangtao@ptpress.com.cn。

为方便读者阅读学习,本书中所有的彩色插图可以在异步社区下载: http://www.epubit.com.cn/book/details/4665

编者


随着移动互联网时代的到来,智能手机逐渐走进了人们的生活, Google公司于2007年11月5日发布了基于Linux平台的开源手机操作系统——Android。由于Android系统的开源性以及其他各个方面的因素,其受到了广大手机厂商的青睐,因此需要大量的Android开发人员来满足日益增长的海量软件开发需求。

 提示

有些读者可能会有一点奇怪,不是介绍OpenGL ES 3.0的知识吗,怎么一开始就介绍Android呢?这是因为本书是立足于通过案例来讲解OpenGL ES 3.0 3D游戏应用开发的。而OpenGL ES 3.0的应用是必须在某种平台上才能运行的,若不借助于一个具体的平台,就只能进行空泛的理论讨论和纸上谈兵了,这非作者本意。

而Android是目前市面上占有率最高的移动嵌入式平台,故作者选用其作为本书OpenGL ES 3.0 3D应用开发的辅助平台。所以,需要在本书开始章节简要介绍一下Android的相关基础知识。不准备在Android平台下开发3D应用的读者也不用担心,可以先略过此章,本书后面的章节还会介绍在iOS、WebGL等其他平台下的3D应用开发。

Android平台基于Linux,其不仅继承了Linux的各种优良特性,同时由于“Android之父”Andy Rubin的先进理念,使得Android平台又增加了许多非常具有吸引力的新特性。这不仅给终端用户带来了非凡的体验,还减轻了Android应用开发者的工作量与难度。

Android是具有开源性质的手机终端解决方案,开发人员可以自由地获取以及修改相应的源代码,具体基本特性如下。

Android平台同时包括底层的操作系统、上层的用户界面和应用程序,这些全部是对开发者及厂商开放的。

Android系统中任何的应用程序均是平等的,不存在传统智能手机操作系统中系统软件与应用软件的区别与障碍。即用户可以随意地替换Android系统中原有的一些应用程序,如拨号软件、短信软件以及照相机等。

Android平台为开发人员提供了丰富的库和多样的工具,这使得开发人员可以快速、方便地开发各种不同功能的应用程序。

随着移动互联网和后PC时代的到来,移动手持设备的性能大幅提升,手机的功能也越来越多,越来越强大,这使得2D手机软件不再满足以追求酷炫为目标的移动手持设备的用户。

如今的手机游戏也远远不再是传统印象中的“俄罗斯方块”、“贪吃蛇”之类的简单游戏了,已经发展到了具有很强的娱乐性和交互性的复杂形态了。

人们在空余时间玩会儿3D手机游戏已经成为了一种潮流,如图1-1和图1-2所示,都是最近很流行的3D Android手机游戏。

▲图1-1 暗黑战神

▲图1-2 狂野飙车

同时,据美国的一家咨询公司的数据显示,从2010年开始,3D应用程序的下载量就稳步增长,增长速度远远超过2D的游戏娱乐应用,这个趋势使得手机3D游戏和应用在智能手机软件中所占的比例日益增加。

对Android开发稍有了解的读者都知道,Android应用程序的开发一般是使用Eclipse进行。Eclipse是一款开放源代码、基于Java的可扩展开发平台。其包括一个框架和一组服务,主要通过插件来构建开发环境。本节主要介绍如何搭建基于Eclipse的Android开发环境,同时也将介绍模拟器的创建和运行等相关知识。

JDK是整个Java开发的核心工具包,其包括了Java运行环境、Java开发工具和Java基础类库等。开发Java应用程序之前首先需要安装JDK,本小节主要介绍JDK的下载、安装以及相关环境变量的配置,详细步骤如下。

(1)首先进入Oracle公司提供的用于下载JDK安装包的网页“http://www.oracle.com/ technetwork/java/javase/downloads/index.html”,如图1-3所示。然后在页面中单击JDK的下载按钮进入如图1-4所示的页面。

▲图1-3 JDK下载页面1

(2)在如图1-4所示的页面中选择对应Windows版本的开发包进行下载。请读者注意选择下载合适的版本,32位操作系统应下载X86版本;64位的应下载X64版本。

▲图1-4 JDK下载页面2

(3)接着双击下载的JDK安装包,开始JDK的安装。安装过程中,系统会弹出如图1-5所示的安装设置界面,若没有特殊需要,单击“下一步”按钮安装到默认路径即可。当然,也可以单击“更改”按钮设置JDK的安装路径。

▲图1-5 JDK安装页面

(4)安装完成后将转到如图1-6所示界面,单击“完成”按钮结束安装。

▲图1-6 安装完成

(5)接着需要在操作系统的Path环境变量中加入JDK的bin路径,用鼠标右键单击“我的电脑”图标,在弹出的菜单中单击属性→高级→环境变量,如图1-7所示。在Path环境变量中添加JDK的bin路径,如“C:\Program Files\Java\jdk1.6.0_43\bin”,并且与前面原有的环境变量用“;”分开。

▲图1-7 设置JDK环境变量

(6)最后在环境变量中新增JAVA_HOME项。具体方法为,在环境变量下的系统变量中添加JAVA_HOME项,将变量值设置为JDK的安装路径,如“C:\Program Files\Java\jdk1.6.0_43”。相关操作过程如图1-8所示。

▲图1-8 创建JAVA_HOME项

Android SDK是开发Android应用程序的基础开发环境,其本身是免费的。本小节将向读者介绍Android SDK的下载、解压及配置,具体步骤如下。

(1)首先在浏览器中输入“http://developer.android.com/sdk/index.html”,打开Android SDK的官方下载网页,如图1-9所示。然后将页面下拉至图1-10所示处,单击 “installser_r24.4.1-windows.exe”进行下载,此时浏览器会弹出下载对话框,提示下载并保存。

▲图1-9 SDK官方下载首页

▲图1-10 SDK官网下载处

(2)将Android SDK下载成功以后,会得到一个名称为“installer_r24.4.1-windows.exe”的可执行文件(随选择下载版本的不同,此名称可能不同)。双击其打开如图1-11所示的界面,单击界面中的 “Next”按钮到下一步,如图1-12所示。根据图1-12所示内容进行勾选,再单击“Next”按钮到下一步。

▲图1-11 Android SDK安装界面1

▲图1-12 Android SDK安装界面2

(3)接着到达选择安装路径界面(如图1-13所示),笔者选择的路径是“D:\Android\android-sdk”,这里建议读者采用与笔者一致的安装路径。路径选择完成之后,单击“Next”按钮进入下一个界面,按照图1-14所示进行勾选,再单击“Install”按钮进入下一个界面。

▲图1-13 Android SDK安装界面3

▲图1-14 Android SDK安装界面4

(4)接着就到达了如图1-15所示的界面,在其中单击“Next”按钮即可进入安装结束界面,如图1-16所示。在如图1-16所示的界面中首先选中“Start SDK Manager”选项,然后单击“Finish”按钮。

▲图1-15 Android SDK安装界面5

▲图1-16 Android SDK安装界面6

(5)稍微等待一会,系统会弹出“Android SDK Manager”的界面,如图1-17所示。在其中选中“Android SDK Build-tools”、“Android SDK Platform-tools”以及“Android 4.4.2”等选项,然后单击界面右下侧的“Install XX packages…”(XX代表一个整数,随选中选项的数量不同而不同)按钮即可开始下载SDK所需要的文件。

▲图1-17 Android SDK安装界面7

(6)下载过程中的界面如图1-18所示。

▲图1-18 Android SDK安装界面8

 提示

这里笔者之所以勾选“Android 4.4.2”是由于其目前市场占有率较高。读者可以根据需要勾选其他Android平台版本选项。另外,随网络情况的不同下载的时间也是不同的,有时需要较长时间。

上一小节介绍了JDK以及Android SDK的下载及安装等相关操作,接着将要介绍的是Eclipse的下载与配置,具体步骤如下。

(1)首先在浏览器中输入“http://www.eclipse.org/juno/”,打开Eclipse的官方网站,如图1-19所示。然后单击“Downloads”跳转到下一个页面,如图1-20所示。

(2)然后在如图1-20所示的页面中选择版本为“Eclipse IDE for Java Developers”的 Eclipse,并选择符合读者PC操作系统数据位的版本(笔者选用的是32bit),单击相关超链接。

▲图1-19 Eclipse下载页面1

▲图1-20 Eclipse下载页面2

(3)选择适合自己的版本之后将进入如图1-21所示的页面,单击 “Downloads”按钮进行下载。

(4)接着浏览器会弹出如图1-22所示的窗口,读者可根据自身需求更改下载路径,然后单击下载按钮,下载Eclipse。

 提示

上述第4步随浏览器的不同弹出的窗口可能会有所不同。

▲图1-21 Eclipse下载页面3

▲图1-22 Eclipse下载窗口

(5)将所需版本的Eclipse下载完成之后,会得到一个名称为“eclipse-java-juno-SR2-win32.zip”(名称随下载版本的不同会有所不同)的压缩包,将此压缩包解压得到名为“eclipse”的文件夹。

(6)打开解压后的“eclipse”文件夹,如图1-23所示。接着双击其中的“eclipse.exe”文件,启动Eclipse集成开发环境,就会出现如图1-24所示的界面。

▲图1-23 eclipse文件夹

▲图1-24 设置Eclipse工作区

(7) 图1-24所示界面提醒用户设置自己的项目工作区路径,笔者采用的是“D:\Android\workspace”。(这里强烈建议读者的工作区路径设置和笔者的保持一致,以便本书中的案例能够直接导入运行。)

(8)指定完工作区路径后,就进入了Eclipse的主界面,如图1-25所示。

(9)在主界面中选择“Help”菜单下的“Install New Software”子菜单项,系统会弹出此子菜单项的相关界面,如图1-26所示。

(10) 在如图1-26所示界面中的“Work with”文本框中输入URL地址“http://dl-ssl-google.com/ android/eclipse/”,并单击“Add”按钮。

▲图1-25 Eclipse的Help选项

▲图1-26 Install New Software界面

(11)此时需要联网获取信息,可能需要稍微等待一会。联网获取信息完毕后,界面中将出现“Developer Tools”列表,将其展开,勾选其中的所有项目,然后单击“Next”按钮进行插件下载,如图1-27所示。

(12)下载过程的界面如图1-27所示,等待Eclipse将插件下载完毕,将进入如图1-28所示的界面。

▲图1-27 Install New Software安装界面

▲图1-28 插件下载完毕界面1

(13)在如图1-28所示的界面中单击“Next”按钮,则进入安装条款界面,勾选同意选项,如图1-29所示。然后单击“Finish”按钮,则出现如图1-30所示的安装提示框。

▲图1-29 插件下载完毕界面2

▲图1-30 安装提示框1

(14)在安装进度条走完之后,将出现如图1-31所示的提示框,询问用户是否确定安装。单击“OK”按钮,则会出现图1-32所示的提示框,询问用户是否重启Eclipse。单击“Yes”按钮,则会重新启动Eclipse。

▲图1-31 安装提示框 2

▲图1-32 重启提示框

至此,用于开发Android应用程序的Eclipse集成开发环境的搭建及相关环境变量的配置就完成了,读者此时就可以正式开始Android应用开发之旅了。

 提示

整个安装过程需要下载的文件比较多,大约2GB,可能要耗费数小时的时间,读者需耐心等待。

开发环境搭建基本完成后,在正式开发Android应用程序之前,还有一个很重要的工作就是创建模拟器。模拟器可以在初学者没有实际设备的情况下在PC上对应用程序进行简单运行测试,很大程度上降低了学习的成本。且模拟器的创建很简单,具体步骤如下。

(1)首先在Eclipse中单击(Android Virtual Device Manager)按钮,如图1-33所示。单击按钮后,系统将弹出Android Virtual Device Manager对话框,如图1-34所示。

▲图1-33 Opens the Android SDK and AVD Manager按钮

▲图1-34 Android SDK and AVD Manager对话框

(2)在弹出的Android SDK and AVD Manager对话框中单击“New”按钮(如图1-35所示),系统将弹出Create new Android Virtual Device(AVD)对话框,如图1-36所示。

▲图1-35 New按钮

▲图1-36 Create new Android Virtual Device(AVD)对话框

(3)在Create new Android Virtual Device(AVD)对话框中输入模拟器的名称(这里输入的是android 4.2.2),下拉Target列表选中Android4.2.2-API Level 17,在Internal Storage面板中输入100(代表内部存储容量为100MB),在SD Card面板中的Size文本框中输入100(代表SD卡容量为100MB),如图1-37所示。

(4)然后设置模拟器显示屏分辨率为5.1”WVGA(480*800),最后勾选Use Host GPU开启本地GPU渲染支持,如图1-37所示。模拟器配置完成后,单击“OK”按钮,即可完成指定版本Android SDK模拟器的创建。

(5)创建完Android SDK的模拟器后,在Android SDK and AVD Manager对话框中就可以显示出创建的Android SDK模拟器了,如图1-38所示。在Android SDK and AVD Manager对话框中选中创建的Android模拟器,单击“Start”按钮(如图1-39所示),将弹出Launch Options对话框,如图1-40所示。

▲图1-37 模拟器配置

▲图1-38 创建完成的Android SDK模拟器

▲图1-39 Start按钮

▲图1-40 Launch Options对话框

(6)在弹出的Launch Options对话框中单击“Launch”按钮(如图1-41所示)系统将启动Android SDK模拟器,启动完成后的效果如图1-42所示。

▲图1-41 Launch按钮

▲图1-42 启动完成的Android SDK模拟器

 提示

由于官方模拟器的性能不是很好,启动可能需要较长(一般是几分钟)时间,这时读者请不要着急。

本节首先将介绍如何在Eclipse中创建一个基于Android的Hello World应用程序,之后将简单介绍Android应用程序的调试,为读者以后学习高级开发铺平道路。

本小节将向读者介绍如何在Eclipse中创建一个基于Android的Hello World应用程序,基本步骤如下所列。

(1)单击eclipse.exe,启动Eclipse,依次选择“File/New/ Android Application Project”,如图1-43所示。或者单击按钮后,系统将弹出新建项目对话框,如图1-44所示。

▲图1-43 创建项目方法

▲图1-44 创建项目对话框

(2)在创建项目对话框中选择“Android/Android Application Project”,如图1-45所示,然后单击“Next”按钮,执行下一步,即弹出New Android Application对话框,如图1-46所示。

(3)在New Android Application对话框中输入应用程序的名称(在这里输入的是Sample_1_1),然后在Project Name中输入项目名称(这里输入的名称与Application Name中的相同,当然也可以不同),同时在Package Name中输入应用程序的包名(这里输入的是wyf.ytl),如图1-46所示。

(4)接着在Minimum Required SDK中选择应用程序最低版本(这里选择的是Android 2.2版本),最后在Target SDK中选择应用程序的目标版本,都填完后,整体情况如图1-46所示。

▲图1-45 选择Android Application Project创建Android应用程序

▲图1-46 New Android Application对话框

 提示

创建项目输入项目包名时,需要至少使用二级包名,否则在该对话框上会显示“Package name must have at least two identifiers.”的出错信息。

(5)单击“Next”按钮后进入如图1-47所示的界面,根据需要勾选是否创建自定义图标,勾选“Create activity”,根据需要也可更改默认的项目路径,最后单击“Next”按钮执行下一步。接着在如图1-48所示的界面中选择Activity样式,此处选择的是“Blank Activity”,单击“Next”按钮执行下一步。

(6)在如图1-49所示的界面中输入Activity Name——Hello Android和Layout Name——main,单击“Finish”按钮完成项目的创建。此时在Eclipse的Project Explorer界面中会自动添加创建的项目Sample_1_1,其目录结构如图1-50所示。

▲图1-47 创建项目截图1

▲图1-48 创建项目截图2

▲图1-49 单击“Finish”按钮创建项目

▲图1-50 创建完成的Android项目

(7)在项目名上单击鼠标右键,在弹出的菜单中然后依次选择“Run As/Android Application”,如图1-51所示。即可运行刚刚创建的HelloAndroid项目,该项目在模拟器上的运行效果如图1-52所示。

▲图1-51 运行Android程序

▲图1-52 HelloAndroid项目运行效果

从上述步骤中可以看出,使用Eclipse创建并运行一个Android应用程序是非常简便的。

上一小节介绍了如何通过Android SDK自动生成Android项目,但是没有对该项目中的内容及组成进行介绍。本小节将对Android项目中的各重要组成部分逐一进行介绍,具体情况如图1-53所示。

▲图1-53 Android应用程序项目结构

1.src源代码文件夹

该文件夹下存放了应用程序中所有开发的Java源代码。要注意的是,在更为复杂的项目中,该文件夹下可以有很多不同的包用于存放不同功能目的的源代码。

2.gen文件夹

该文件夹中存放的是Eclipse中ADT插件自动生成的R.java文件。实际上R.java定义了一个R类,其中包含应用程序的用户界面、图像以及字符串等资源的编号。也就是每有一个资源实例,系统就为其在R类中生成一个相应的编号。这些编号都是系统自动生成的,用户不应该修改。

3.assets文件夹

该文件夹用于存放应用程序需要使用的一些外部资源,如音频、视频、数据文件等。在应用程序中可以使用“getResources().getAssets().open("aa.mp3")”得到指定资源文件的输入流。

4.res资源文件夹

该文件夹下一般有drawable系列、layout及values 等文件夹,下面对这些文件夹逐一进行介绍。

5.AndroidManifest.xml文件

此文件是整个应用程序的描述文件,应用程序中所需要的各个组件均需要在此文件中说明。同时,应用程序需要的一些权限,如网络访问权限、I/O读取权限等,也需要在此文件中进行声明。

 提示

除了上述文件夹及文件外,还有一个一般开发时不需要直接使用的default.properties文件。此文件为项目的配置文件,一般不需要人为改动,系统会自动对其进行管理,有兴趣的读者可以自行打开查看。

Android SDK提供了一个强大的调试工具DDMS(Dalvik Debug Monitor Service),通过DDMS可以调试并监控Android程序的运行,更好地帮助开发人员完成软件的调试和开发。接下来将会详细介绍DDMS的打开与使用的方法。

▲图1-54 DDMS按钮

▲图1-55 DDMS界面

▲图1-56 使用Open Perstective按钮打开DDMS

1.System.out.println方法

首先介绍Java开发人员十分熟悉的System.out.println方法,其在Android应用程序中的使用方法与传统Java中相同,具体步骤如下。

 提示

在这里就不再创建新的Android项目,直接使用上一小节已经创建的Android项目(Sample1_1)。

(1)首先在Eclipse中打开src文件夹下的com.bn.Sample1_1包中的MainActivity.java文件。

(2)然后在setContentView(R.layout.activity_main)语句后面添加代码System.out.println(“the first”)。

(3)代码修改完成后,再次运行本应用程序。

(4)应用程序运行后打开DDMS,找到LogCat面板,更改为DEBUG界面,如图1-57所示。

(5)在LogCat面板下的Log选项卡中可以看到输出的打印语句,如图1-58所示。

▲图1-57 DEBUG界面

▲图1-58 Log选项卡

有时可能觉得在Log中的输出信息太多,不便于查看。这时可以在LogCat中添加一个专门输出System.out信息的面板。单击右边区域的(Create Filter)按钮,系统会弹出Log Filter对话框,在Filter Name输入框中输入过滤器名称,在by Log Tag中输入用于过滤的标志,如图1-59所示。

 提示

由于输出的语句主要有System.out.println(换行)、System.out.print(不换行)两种,所以,设置by Log Tag中的内容为System.out用以进行过滤。

此时再次运行应用程序观察输出的情况,在LogCat下的System面板中将会只存在System.out的输出信息,效果如图1-60所示。

▲图1-59 Log Filter对话框

▲图1-60 只查看System.out输出的内容

2.android.util.Log类

除了Java开发人员熟知的System.out.println方法外,Android还专门提供了另外一个类android.util.Log来进行调试信息的输出。下面将介绍Log类的使用,具体步骤如下。

(1)在MainActivity.java中注释掉前面已经添加的打印输出语句“System.out.println(“the first”);”,然后在后面添加代码“Log.d(“Log”, “the first Log”);”。

(2)运行程序,在DDMS中找到LogCat面板,切换到All messages页面,观看打印的内容,如图1-61所示。

▲图1-61  使用Log输出测试数据

 提示

使用Log类时需要使用“import android.util.Log;”语句进行导入,使用 System.out.println方法或android.util.Log类输出调试信息各有优缺点,读者可以在开发项目时自行体会,选用自己所需要的方式。 同时需要注意的是,DDMS还有很多强大的功能,这里只介绍了其最基本的用法,有兴趣的读者可以参考人民邮电出版社出版的《Android应用案例开发大全(第3版)》一书的第1.4节“DDMS的灵活应用”或参考其他技术资料。

由于模拟器在实际项目开发中不是很方便,性能较差,因此,实际开发中一般使用真机对开发的应用程序进行联机调试。故本小节将介绍实际设备联机调试的方法,基本步骤如下。

(1)下载与真机型号相匹配的驱动程序(注意部分手机自带驱动程序,无须下载)。

(2)用数据线将手机和开发PC相连接。

(3)根据提示,在PC上安装手机驱动程序。

(4)将手机设置为允许安装未知来源软件,并打开手机上的USB调试。这两项设置一般都可以在手机设置选项下的应用程序项目中找到,根据手机具体型号和Android操作系统版本的不同可能略有区别。若读者的设备与这里介绍的不同可以参考其他资料或上网搜索相关机型的设置说明。

(5)再次运行前面开发的应用程序,系统将弹出Android Device Chooser窗口,如图1-62所示。

(6)在窗口中选择自己的设备,然后单击“OK”按钮,程序将被送入到连接的真机上运行。

(7)再打开DDMS,查看左上角的Devices窗口,窗口中列出了真机设备,如图1-63所示。

(8)选中真机设备,LogCat窗口就出现了真机调试的打印内容,这样就可以在真机中进行联机调试。

▲图1-64 Android Device Chooser窗口 ![](/api/storage/getbykey/original?key=160999c49c5e1ec97217)

▲图1-63 DDMS中的Devices窗口

1.4 已有Android项目的导入和运行

上一节介绍了如何搭建Android开发环境、如何开发Hello Android应用程序以及如何调试Android的应用程序,接下来在本节中将简要介绍已有Android项目的导入和运行。

1.4.1 项目的导入

首先介绍怎样将已有的Android项目导入Eclipse,此处以本书最后部分大案例中的“3D可爱抓娃娃”为例进行讲解,具体步骤如下。

(1)启动Eclipse,依次单击“File|Import”,系统将弹出导入项目的窗口Import,选择Android/Existing Android Code Into Workspace,然后单击“Next”按钮进入下一界面,如图1-64所示。

(2)在项目导入对话框中首先单击右上侧的“Browse...”按钮,如图1-65所示,找到工作区“D: \Android\workspace”,系统将工作区中的所有项目导入到“Projects”,选中“CatcherFun”项目,并按下“Finish”按钮,即可完成项目的导入。

▲图1-64 打开 Import界面

▲图1-65 打开项目路径

1.4.2 应用程序的运行

将项目导入到Eclipse中以后,读者便可以在模拟器或连接的真机上运行导入的应用程序了,具体步骤如下。

(1)将手机与PC连接后,用鼠标选中该项目并右键单击,在出现的菜单中选择Run As,然后单击二级菜单中的Android Application选项,该项目对应的程序便运行到了连接的手机上,如图1-66所示。

(2)运行成功后,过一会便进入了“3D可爱抓娃娃”游戏的菜单界面,如图1-67所示。

▲图1-66 在手机上运行该程序

▲图1-67 游戏界面

 提示

图1-67中的游戏界面是笔者在真机上运行游戏的截图,由于模拟器对OpenGL ES 3.0不支持,因此,本游戏大案例只能在真机上运行。

1.5 Android应用程序运行的机制

前面介绍了如何搭建Android开发环境、如何开发Hello Android应用程序以及Android应用程序的调试,接下来在本节中将简要地介绍Android应用程序的运行机制。

1.5.1 应用程序的系统架构

Android平台由应用程序、应用程序框架、Android运行时、系统库以及底层Linux内核构成,详细结构如图1-68所示。

▲图1-68 Android应用程序的系统架构

 说明

应用程序层里面包含的就是需要读者去发挥创意开发的、各种各样丰富多彩的应用程序,这也是一般Android应用开发人员工作的层次。

1.5.2 应用程序框架

Android应用程序框架是开发Android应用的基础,开发者在开发时有大部分的时间用在调用这一层。Android应用程序框架包含活动管理器、窗口管理器、内容提供者、视图系统、包管理器、电话管理器、位置管理器、资源管理器以及通知管理器等,如图1-69所示。

▲图1-69 Android应用程序框架

应用程序框架层各部分的具体功能如下所列。

1.5.3 Android运行时

Android运行时包括核心库(Core Libraires)以及Dalvik虚拟机(Dalvik Virtual Machine)两部分,如图1-70所示。

▲图1-70 Android运行时

 说明

与传统的JavaME不同的是,每个Android应用程序都有一个自己特有的进程,每个应用程序都是使用一个自己持有的Dalvik虚拟机来执行的,而不像在大部分JavaME实现中,多个JavaME应用程序共享一个Java虚拟机进程。

1.5.4 系统库

应用程序框架是最接近于应用程序的软件组件服务,而更底层则是Android的系统函数库(Libraries),这部分是应用程序框架的支撑,其基本架构如图1-71所示。

▲图1-71 系统库

1.5.5 底层Linux内核

Android平台开放性的基础是因为其采用了Linux内核,如图1-72所示。Android平台中的操作系统采用了Linux 2.6版的内核,其包括显示驱动、摄像头驱动、Flash内存驱动、Binder(IPC)驱动、键盘驱动、WIFI驱动、视频驱动及电源驱动等。

Linux内核层是软件层与硬件层之间的桥梁,软件开发人员不必关心底层Linux内核的实现细节,只需将全部精力投入到上层的软件开发中。而实现底层Linux内核的工作是由Google和手机厂商来完成,如驱动的更新或者新驱动的编写等。

▲图1-72 底层Linux内核

1.6 小结

本章介绍了Android平台的特点、如何搭建Android开发环境、如何开发Hello Android应用程序、如何调试Android应用程序、如何导入运行已有的Android项目以及Android应用程序的运行机制。

通过阅读本章,让读者初步掌握Android开发环境相关环境变量的配置,简单程序的开发与调试,并且对Android应用程序的运行机制有大概的了解。


虽然本书主要是介绍OpenGL ES 3.0 3D应用及游戏开发的,但由于很多3D游戏应用中还需要用到目标平台的一些其他应用开发方面的知识,而本书3D基础知识部分主要是借助于Android平台来介绍OpenGL ES 3.0 3D应用及游戏开发的。故本章将向读者简要介绍一些在Android游戏开发中必备的一些基础知识,如音效、文件的读取、游戏信息的存储、Socket网络、蓝牙网络等。

 提示

由于介绍Android应用开发非本书重点,若读者对Android的基础知识很不熟悉,可以参考笔者在人民邮电出版社出版的《Android应用开发完全自学手册——核心技术、传感器、2D/3D、多媒体与典型案例》与《Android平板电脑开发实战详解和典型案例》这两本书,其中详细介绍了Android应用开发各方面的知识。不准备在Android平台下开发3D应用的读者也不用担心,可以先略过此章,本书后面的章节还会介绍在iOS、WebGL等其他平台下的3D应用开发。

一款好游戏,除了具备优质的画面和较高的可玩性之外,还应该有出色的音效。音效一般指的是游戏中发生特定行为或进行特定操作时播放的效果音乐或为了渲染整体气氛播放的背景音乐等,如远处隆隆的炮声、由远而近的脚步声等。

开发人员通过精心准备的声音特效,并结合游戏酷炫的场景,可以渲染出一种紧张刺激的氛围,使玩家产生身临其境的感觉。这就像电影中的声音特效一样,假如没有了合适的音效,那么游戏和电影一样,真实感会大打折扣。

 提示

按照作用的不同,可以将音效划分为即时音效和背景音乐。两种音效在Android中的实现技术不同,本节将向读者详细介绍两种音效在Android中的具体实现。

游戏中有时需要根据情况播放即时音效,如枪炮声、碰撞声等。即时音效的特点是短暂、可以重复、可以同时播放的。由于Android提供的MediaPlayer(媒体播放器)会占用大量的系统资源,而且播放时还需要进行缓冲,有较大的时延,因此使用MediaPlayer无法实现即时音效。

Android系统的设计者也考虑到了这个问题,为即时音效的实现提供了一个专门的类——SoundPool。SoundPool类用于管理和播放应用程序中的声音资源,使用该类时首先需要通过该类将声音资源加载到内存中,然后在需要即时音效的地方播放即可,几乎没有时延,可以满足游戏实时性的需要。

 提示

由于SoundPool设计的初衷是用于无时延地播放游戏中的短促音效,因此实际开发中应该只将长度小于7s的声音资源放进SoundPool,否则可能会加载失败或内存占用过大。

SoundPool类的构造器及常用方法如表2-1所列。

表2-1 SoundPool类的构造器以及方法

构造器或方法签名

方 法 含 义

各个参数含义

public SoundPool
(int maxStreams, int streamType,int srcQuality)

创建SoundPool对象

maxStreams:该参数用于设置最多同时能够播放多少个音效
streamType:该参数设置音频类型,在游戏中通常设置为:STREAM_MUSIC
srcQuality:该参数设置音频文件的质量,目前还没有具体作用,设置为0(默认值)

public int load(Context context, int resId, int priority)

加载音频文件

context:应用程序的上下文
resId:该参数为要加载的音效资源的ID
priority:优先级,现在还没有作用,设置为1即可

public final int play (int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)

播放音效

soundID:该参数为要播放音效的ID
leftVolume:该参数用来控制左声道音量
rightVolume:该参数用来控制右声道音量
priority:该参数为优先级,0为最低
loop:该参数为音效播放的循环次数,0为不循环,−1为永远循环
rate:该参数为音效的回放速度,该值在0.5~2.0f,1.0f为正常速度

public final void pause (int streamID)

暂停音效的播放

streamID:要暂停音效的ID

public final void stop(int streamID)

停止音效的播放

streamID:要停止音效的ID

了解了SoundPool类的基本操作方法之后,接下来就可以开发游戏中用到的即时音效了。本小节将向读者展示一个播放和停止即时音效的简单案例,其主要功能为,通过SoundPool声音池技术来实现一个即时音效的播放和停止,运行效果如图2-1和图2-2所示。

了解了本案例的运行效果后,接下来对其具体开发步骤进行介绍,具体如下所列。

(1)首先在Eclipse中创建名称为Sample2_1的项目,然后在项目目录下的res文件夹下创建raw文件夹。接着将需要被播放的短促音效对应的音频文件musictest.ogg复制到raw文件夹下,如图2-3所示。

▲图2-1 播放即时音效

▲图2-2 停止播放音效

▲图2-3 导入音效文件

 提示

一般在Android手机平台上使用的即时音效文件越小越好,这有助于提高游戏的整体性能。对于同一个音效文件,在不改变其时长的情况下,可以采用降低采样率(如降低到16kbit/s)或由立体声改为单声道的方式来缩小体积。

(2)准备好声音资源后,接下来进行本案例中Sample2_1_Activity类的开发。该类中使用了声音池技术实现了即时音效的播放,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_1/src/com/bn/pp1目录下的Sample2_1_Activity.java。

1    package com.bn.pp1;                //声明包
2    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3    public class Sample2_1_Activity extends Activity {
4        SoundPool sp;                 //声明SoundPool的引用
5        HashMap<Integer, Integer> hm;     //声明HashMap来存放声音文件
6        int currStreamId;            //当前正播放的streamId
7        @Override
8        public void onCreate(Bundle savedInstanceState) {    //重写onCreate方法
9            super.onCreate(savedInstanceState);
10            setContentView(R.layout.main);                     //跳转到主界面
11            initSoundPool();                             //初始化声音池的方法
12            Button b1 = (Button) this.findViewById(R.id.Button01);     //获取播放按钮
13            b1.setOnClickListener                 //为播放按钮添加监听器
14            (new OnClickListener() {
15                @Override
16                public void onClick(View v) {
17                    playSound(1, 0);             //播放1号声音资源,且播放一次
18                    Toast.makeText(getBaseContext(),"播放即时音效",Toast.LENGTH_SHORT)
19                        .show();            //提示播放即时音效
20            }});
21            Button b2 = (Button) this.findViewById(R.id.Button02);     //获取停止按钮
22            b2.setOnClickListener                 //为停止按钮添加监听器
23            (new OnClickListener() {
24                @Override
25                public void onClick(View v) {
26                    sp.stop(currStreamId);         //停止正在播放的某个声音
27                    Toast.makeText(getBaseContext(),"停止播放即时音效",Toast.   
                               LENGTH_SHORT) .show();        //提示停止播放
28        
29        }});}
30        public void initSoundPool() {                //初始化声音池的方法
31            sp = new SoundPool(4, AudioManager.STREAM_MUSIC, 0);     //创建SoundPool对象
32            hm = new HashMap<Integer, Integer>();     //创建HashMap对象
33            //加载声音文件musictest并且设置为1号声音放入hm中
34            hm.put(1, sp.load(this, R.raw.musictest, 1));
35        }
36        public void playSound(int sound, int loop) {    //播放声音的方法
37            AudioManager am = (AudioManager) this         //获取AudioManager引用
38                    .getSystemService(Context.AUDIO_SERVICE);
39            float streamVolumeCurrent = am                //获取当前音量
40                    .getStreamVolume(AudioManager.STREAM_MUSIC);
41            float streamVolumeMax = am                    //获取系统最大音量
42                    .getStreamMaxVolume(AudioManager.STREAM_MUSIC);
43            float volume = streamVolumeCurrent / streamVolumeMax;//计算得到播放音量
44            //调用SoundPool的play方法来播放声音文件
45            currStreamId = sp.play(hm.get(sound), volume, volume, 1, loop, 1.0f);
46    }}

 提示

通过以上的案例,读者可以看出使用SoundPool播放即时音效是非常简单的。今后的游戏开发中,只要是游戏中的即时音效都应该用此方式来实现。

背景音乐也可以采用前一小节的声音池技术,在播放背景音乐时,只需要把loop(播放次数参数)设置成-1进行无限循环即可。但由于SoundPool只适合播放不大于7秒的音效文件,限制较大。而背景音乐对时延并不敏感,因此在实际的游戏开发中,时长较长的背景音乐一般采用媒体播放器(MediaPlayer)来进行播放。

要想很好地使用MediaPlayer进行音/视频文件的播放,首先要熟悉MediaPlayer的生命周期。这样不仅有利于开发人员开发出更加合理的代码,而且可以达到充分利用系统资源的目的。

1.MediaPlayer的生命周期

MediaPlayer的生命周期包括10种状态,每种状态下可以调用相应的方法来实现音/视频文件的管理或播放。其各个状态及状态间的关系可以用一个简单的流程图来表示,如图2-4所示。

▲图2-4 MediaPlayer的生命周期图

但通过以上两种不同方式进入的Idle状态还是有些区别的,主要体现为:如果在此状态下调用了getDuration等方法,并且是通过reset方法进入Idle状态的话会触发OnErrorListener.onError,同时MediaPlayer会进入Error状态;如果是新创建的MediaPlayer对象,则并不会触发onError,也不会进入Error状态。

通过release方法可以进入End状态,只要MediaPlayer对象不再被使用,就应当尽快将其通过release方法释放掉,以释放其占用的软、硬件资源,这其中有些资源是互斥的(相当于临界资源)。如果MediaPlayer对象进入了End状态,则不会再进入其他任何状态了。

这个状态比较简单,MediaPlayer调用setDataSource方法就进入了Initialized状态,表示此时要播放的文件已经设置好了。

初始化完成之后还需要通过调用prepare或prepareAsync方法进行准备,这两个方法一个是同步的,一个是异步的。只有进入了Prepared状态,才表明MediaPlayer到目前为止都工作正常,可以进行音乐文件的播放。

这个状态比较容易理解,主要是与prepareAsync异步准备方法配合,如果异步准备完成,会触发OnPreparedListener.onPrepared,进而进入Prepared状态。

MediaPlayer准备完成后,通过调用start方法,将进入Started状态。所谓Started状态,也就是播放中状态,开发中可以使用isPlaying方法测试MediaPlayer是否处于Started状态。

如果播放完毕,而又设置了循环播放,则MediaPlayer仍然会处于Started状态。类似地,如果在该状态下MediaPlayer调用了seekTo或者start方法均可以让MediaPlayer停留在Started状态。

Started状态下调用pause方法可以暂停播放,从而进入Paused状态。MediaPlayer暂停后若再次调用start方法则可以继续进行播放,并转到Started状态。暂停状态时可以调用seekTo方法,这时此MediaPlayer的状态是不变的。

Started或Paused状态下均可调用stop方法停止播放并进入Stopped状态,而处于Stopped状态的MediaPlayer要想重新播放,需要通过调用prepareAsync或prepare方法返回到先前的Prepared状态重新开始才可以。

文件正常播放完毕,而又没有设置循环播放的话就进入该状态,并会触发OnCompletionListener接口中的onCompletion方法。此时可以调用start方法重新从头播放文件,也可以调用stop方法停止播放,或者调用seekTo方法来重新定位播放位置。

由于某种原因MediaPlayer出现了错误,就会触发OnErrorListener.onError回调方法,此时MediaPlayer将会进入Error状态。及时捕捉并妥善处理这些错误是很重要的,这可以帮助应用程序及时释放相关的软、硬件资源,也可以改善用户体验。

如果MediaPlayer进入了Error状态,可以通过调用reset方法来恢复,使得MediaPlayer重新返回到Idle状态。

 提示

从上述对生命周期的介绍中可以看出,某些情况发生时MediaPlayer会回调特定监听接口中的事件处理方法。若读者在开发中希望使用回调,则需要首先向MediaPlayer注册实现指定监听接口的监听器。例如,可以使用setOnErrorListener方法注册实现OnErrorListener接口的监听器,当MediaPlayer进入Error状态时监听器中的onError方法就会被回调。

2.AudioManager类

AudioManager类在Android系统中主要用来进行音/视频播放时的音量控制,使用时的基本步骤如下所列。

AudioManager类中的常用方法如表2-2所列。

表2-2 AudioManager类中的常用方法

方 法 名

说  明

public void adjustVolume (int direction, int flags)

调用该方法可以调节音量大小。参数direction为调节的方向,ADJUST_LOWER减小音量、ADJUST_RAISE增大音量、ADJUST_SAME保持音量不变;参数flags为标记,通常设置为0

public int getStreamMaxVolume (int streamType)

调用该方法可以得到系统最大音量

public int getStreamVolume (int streamType)

调用该方法可以得到系统当前音量

void android.media.AudioManager.setStreamVolume(int streamType, int index, int flags)

调用该方法可以设置音量的大小,参数streamType为声音的类型,index为要设置的音量的索引值,参数flags为标记,通常设置为AudioManager.FLAG_PLAY_SOUND

 提示

MediaPlayer类还可以对视频文件进行操作,由于本书只介绍其与游戏音效相关的功能,因此不再对其视频文件的播放功能进行介绍,有兴趣的读者可以自行查阅相关资料。

了解了MediaPlayer和AudioManager类的基本操作方法之后,就可以对游戏的背景音乐功能进行开发了。本小节将通过这两个类来实现一个简易的音乐播放器,其主要功能为对手机SD卡中的音乐文件进行播放,运行效果如图2-5所示。

 提示

图2-5中从左到右分别为案例运行后,依次单击“播放音乐”按钮、“暂停播放”按钮、“增大音量”按钮后的效果图。停止播放音乐和减小音量的效果与图2-5中的已有效果类似,这里没有给出,请读者自行运行本案例进行查看。

▲图2-5 案例运行效果图

了解了本案例的具体运行效果后,接下来就介绍本案例的具体开发步骤,具体如下所列。

(1)首先需要准备好要播放的音乐文件,本案例中使用的是著名的高山流水古曲,文件名为“gsls.mp3”。准备完音乐文件后,将该音乐文件通过DDMS导入到模拟器或真机的SD卡中,如图2-6所示。

▲图2-6 导入音乐文件

 提示

高山流水曲的音乐资源文件见随书中源代码/第2章目录下的gsls.mp3。导入时直接用鼠标光标将文件拖曳到DDMS下“File Explorer”面板中的“sdcard”目录下即可。

(2)音乐资源在SD卡中放置完成后,接下来进行本案例中Sample2_2_Activity类的开发。该类使用了MediaPlayer实现了背景音乐的播放,具体代码如下。

代码位置:见随书中源代码/第2章/Sample2_2/src/com/bn/pp2目录下的Sample2_2_Activity.java。

1    package com.bn.pp2;
2    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3    public class Sample2_2_Activity extends Activity {
4        MediaPlayer mp;                             //声明MediaPlayer的引用
5        AudioManager am;                             //声明AudioManager的引用
6        private int maxVolume;                         //最大音量值
7        private int currVolume;                         //当前音量值
8        private int stepVolume;                         //每次调整的音量幅度
9        @Override
10        public void onCreate(Bundle savedInstanceState){     //重写onCreate方法
11            super.onCreate(savedInstanceState);
12            setContentView(R.layout.main);             //跳转到主界面
13            mp = new MediaPlayer();                 //创建MediaPlayer实例对象
14            try {
15                mp.setDataSource("/sdcard/gsls.mp3"); //为MediaPlayer设置要播放文件资源
16                mp.prepare();                     //MediaPlayer进行缓冲准备
17            }catch (Exception e) {
18                e.printStackTrace();
19            }
20             //获取AudioManager对象引用
21            am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
22            //获取最大音乐音量
23            maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
24            stepVolume = maxVolume / 6;          //每次调整的音量大概为最大音量的1/6
25            Button bstart = (Button) this.findViewById(R.id.Button01); //获取开始按钮
26            bstart.setOnClickListener             //为开始按钮添加监听器
27            (new OnClickListener() {
28                @Override
29                public void onClick(View v) {
30                    mp.start();             //调用MediaPlayer的start方法来播放音乐
31                    Toast.makeText(getBaseContext(), "开始播放'高山流水曲'",
32                            Toast.LENGTH_LONG).show();
33            }});
34            Button bpause = (Button) this.findViewById(R.id.Button02); //获取暂停按钮
35            bpause.setOnClickListener             //为暂停按钮添加监听器
36            (new OnClickListener() {
37                @Override
38                public void onClick(View v) {
39                    mp.pause(); //调用MediaPlayer的pause方法暂停播放音乐
40                    Toast.makeText(getBaseContext(), "暂停播放'高山流水曲'",
41                            Toast.LENGTH_LONG).show();
42            }});
43            Button bstop = (Button) this.findViewById(R.id.Button03); //获取停止按钮
44            bstop.setOnClickListener                 //为停止按钮添加监听器
45            (new OnClickListener() {
46                @Override
47                public void onClick(View v) {
48                    mp.stop(); //调用MediaPlayer的stop方法停止播放音乐
49                    try {
50                        mp.prepare();            //进入准备状态
51                    } catch (IllegalStateException e) {        //捕获异常
52                        e.printStackTrace();
53                    } catch (IOException e) {                //捕获异常
54                        e.printStackTrace();
55                    }
56                    Toast.makeText(getBaseContext(), "停止播放'高山流水曲'",
57                            Toast.LENGTH_LONG).show();
58            }});
59            Button bUp = (Button) this.findViewById(R.id.Button04); //获取增大音量按钮
60            bUp.setOnClickListener                 //为增大音量按钮添加监听器
61            (new OnClickListener() {
62                @Override
63                public void onClick(View v) {    //获取当前音量
64                    currVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
65                    int tmpVolume = currVolume + stepVolume;   
                                             //增加音量,但不超过最大音量值
66                    currVolume = tmpVolume < maxVolume ? tmpVolume:maxVolume;  
                                             //临时音量
67                    am.setStreamVolume(AudioManager.STREAM_MUSIC, currVolume,
68                            AudioManager.FLAG_PLAY_SOUND);
69                    Toast.makeText(getBaseContext(), "增大音量",
70                            Toast.LENGTH_SHORT).show();
71            }});
72            Button bDown = (Button) this.findViewById(R.id.Button05);   
                                             //获取减小音量按钮
73            bDown.setOnClickListener                 //为减小音量按钮添加监听器
74            (new OnClickListener() {
75                @Override
76                public void onClick(View v) {    //获取当前音量
77                    currVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
78                    //减小音量,但不小于0
79                    int tmpVolume = currVolume - stepVolume;        //临时音量
80                    currVolume = tmpVolume > 0 ? tmpVolume:0;
81                    am.setStreamVolume(AudioManager.STREAM_MUSIC, currVolume,
82                            AudioManager.FLAG_PLAY_SOUND);
83                    Toast.makeText(getBaseContext(), "减小音量",
84                            Toast.LENGTH_SHORT).show();
85    }});}}

进行游戏开发时,有时需要持久地存储一些十分简单的数据,例如,游戏中音效的开关状态、振动效果的开关状态等。Android设计者也充分注意到了这种存储简单数据的需求,提供了一个Preferences类专门用于完成此类任务。

Preferences使用非常方便,但功能不是很强大,一般适合用来保存一些用户设置的参数,是一种轻量级的数据存储机制。Preferences仅可以用来存储几种简单类型的数据,如boolean、int、float、long或者String。这些数据以键值对的形式存储在应用程序私有的Preferences目录(/data/data/应用程序包名/shared_prefs/)下的特定xml文件中,如图2-7所示。

▲图2-7 Preferences文件存放的位置

 提示

请读者注意,这个xml文件是设计为由程序自动维护的,不应该手动进行修改,以免影响程序的正确运行。

本小节将介绍一个使用Preferences实现的,具有记录上次访问时间功能的小案例。其具体功能为:第一次运行本案例中的程序,程序将显示“用户您好,欢迎您第一次光临本软件。”,若不是第一次运行,则显示上一次用户运行的时间,如“用户您好,您上次进入时间为:Oct 15,2011 2:38:40 AM”。程序运行的效果如图2-8和图2-9所示。

▲图2-8 第一次打开本案例

▲图2-9 再次打开本案例

介绍完本案例的运行效果之后,接下来将对本案例的开发进行介绍。主要是本案例的主控制类Sample2_3_Activity的开发,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_3/src/com/bn/pp3目录下的Sample2_3_Activity.java。

1    package com.bn.pp3;
2    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3    public class Sample2_3_Activity extends Activity {
4       @Override
5        public void onCreate(Bundle savedInstanceState) {    //重写onCreate方法
6            super.onCreate(savedInstanceState);
7            setContentView(R.layout.main);                //跳转主界面
8            //获取SharedPreferences引用,存储名称为bn.xml,读写模式为私有
9            SharedPreferences sp=this.getSharedPreferences("bn", Context.MODE_PRIVATE);
10            String lastLoginTime=sp.getString("time", null);    //获取键为“time”的值
11            if(lastLoginTime==null){                 //若值为空,则为第一次登录本程序
12                 lastLoginTime="用户您好,欢迎您第一次光临本软件。";
13            }else{                                  //不为空,则修改字符串为上次登录时间
14                lastLoginTime="用户您好,您上次进入时间为:"+lastLoginTime;
15            }
16            SharedPreferences.Editor editor=sp.edit();//取得编辑对象,来修改Preferences文件
17            editor.putString("time", new Date().toLocaleString());  
                                             //修改键为“time”的值为当前时间
18            editor.commit();                        //提交修改
19            TextView tv=(TextView)this.findViewById(R.id.TextView01);  
              //获取用来显示的TextView
20            tv.setText(lastLoginTime);                //设置显示的字符串
21    }}

上一节介绍了如何使用Preferences存储简单数据,而复杂的数据就需要存储到文件或数据库中了。Android自带了一款轻量级的关系数据库——SQLite,其具有体积小,功能强大等特点,成为嵌入式设备首选的数据库系统。本节将带领读者走进SQLite的世界,学习如何应用SQLite数据库进行数据的增、删、改、查等基本操作。

SQLite是一款满足ACID特性的具有完备功能的关系数据库系统,由于其设计目标为轻量级、开源、支持嵌入式使用,因此,目前已经在嵌入式设备领域被广泛采用。其运行需要的系统资源非常少,在嵌入式设备中可能只需要几百KB的内存就够了。

SQLite对主流编程语言的支持也非常全面,如C#、PHP、Java等,同时还支持ODBC接口。另外,SQLite的性能也是一流的,在一般应用情况下,其处理速度比MySQL、PostgreSQL这两款著名的开源数据库管理系统都快。

 提示

SQLite的最新版本为3.8.11.1,发布时间是2015年7月29日。其官方网站为:http://www.sqlite. org或者http://www.sqlite .com .cn,读者可以在该网站上获取SQLite的源代码和相关文档。

虽然SQLite占用的资源非常少,但是其功能、特性与服务器级数据库相比却丝毫不差,这也是SQLite能够受到Android系统青睐的主要原因,其部分特性如下所列。

Android系统中很多的用户数据都存储在SQLite数据库中,如联系人信息、通话记录、短信等,由此可见SQLite对于Android的重要性。

 提示

读者要想很好地使用SQLite数据库,必须熟练掌握SQL语言。这是由于SQL已经事实上成为关系数据库操作的标准语言,市面上的关系数据库几乎无一例外都支持SQL。因此,在数据库领域,有这样一句话“学好SQL,走遍天下都不怕”。

一般学习数据库相关课程的时候,首先介绍的就是数据库的一些基本操作,如数据的增、删、改、查等。按照惯例,本书也首先简单介绍SQLite数据库的创建、关闭及数据的增加、删除、修改、查询等基本操作,具体如下所列。

 提示

想在Android下通过Java编程对SQLite数据库进行操作,就必须要用到android.database.sqlite包下的SQLiteDatabase类,该类提供了对SQLite数据库进行基本操作的所有重要方法。

创建数据库需要用到的是openDatabase方法,此方法签名为“public static SQLiteDatabase openDatabase (String path, SQLiteDatabase.CursorFactory factory, int flags)”。其中path为数据库所在的路径;factory为游标工厂;flags为标志,可以用来控制数据库的访问模式。

关闭数据库需要用到的是close方法,此方法签名为“public void close()”。在实际开发中数据库使用完毕后,一定不要忘记使用该方法关闭数据库,以防止资源的浪费。

插入数据可以使用insert方法,此方法签名为“public long insert (String table, String nullColumnHack, ContentValues values)”。其中table为待插入的表名,nullColumnHack通常设置为null,values为待插入的数据。

更新数据可以使用update方法,其签名为“public int update(String table, ContentValues values, String whereClause, String[] whereArgs)”。其中table为待更新的表名;values为待更新内容;whereClause为where子句的内容,用来进行记录筛选;whereArgs为where子句的参数。

删除数据可以使用delete方法,其签名为“public int delete (String table, String whereClause, String[] whereArgs)”。其中table为要操作的表名;whereClause为where子句的内容,用来进行记录筛选;whereArgs为where子句的参数。

查询数据可以使用query方法,其方法签名为“public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)”。其中table为要查询的表,columns为要查询的列,selection为过滤记录的子句,selectionArgs为过滤的参数值,groupBy为分组子句,having为过滤分组的子句,orderBy为记录排序子句。

 提示

Android中被重载了的query方法有多个变体。这里由于篇幅所限,不再赘述,有需要的读者可以自行查阅API或其他相关资料。

对于不太熟悉SQL语言的初学者而言,插入、更新、删除数据可以用前面介绍的insert、update、delete方法。但对于熟练掌握SQL的开发人员而言,使用execSQL方法直接执行相应的SQL语句十分方便。

此方法签名为“public void execSQL (String sql)”或“public void execSQL (String sql, Object[] bindArgs)”。其中sql为需要执行的SQL语句,bindArgs为带参数SQL语句的参数值数组。

 提示

需要注意的是,此方法仅支持执行非查询的SQL语句,如CREATE TABLE、 DELETE、 INSERT、UPDATE等,不能用于执行SELECT语句。

对于熟练掌握SQL的开发人员而言,会觉得前面介绍的query方法使用过于繁琐。Android的设计人员也考虑到了这个问题,提供了支持执行SQL查询语句的rawQuery方法,其方法签名为“public Cursor rawQuery (String sql, String[] selectionArgs)”。其中sql为要执行的SQL查询语句(可以带参数),selectionArgs为查询参数的值。

 提示

SQLiteDatabase类中用于数据库操作的方法还有很多,本书只是介绍了其中一些常用的,若读者有需要可以查阅API或其他相关资料进一步学习。

上一小节介绍了SQLite数据库的基本操作方法,本小节将详细介绍一个使用SQLite数据库的简单案例,以使读者可以更加快速地掌握SQLite数据库的使用方法,从而在开发中进行合理地使用。本案例运行效果分别如图2-10、图2-11和图2-12所示。

▲图2-10  创建数据库

▲图2-11  插入记录

▲图2-12  查询记录

介绍完本案例的运行效果后,接下来将开发本案例中唯一的一个类——Sample2_4_Activity,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_4/src/com/bn/pp4目录下的Sample2_4_Activity.java。

1    package com.bn.pp4;
2    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3    public class Sample2_4_Activity extends Activity {
4        SQLiteDatabase sld; // 声明SQLiteDatabase引用
5        @Override
6        public void onCreate(Bundle savedInstanceState) {    // onCreate方法
7            super.onCreate(savedInstanceState);
8            setContentView(R.layout.main);                      //跳转到主界面
9            Button b = (Button) this.findViewById(R.id.Button01);        
                                             //获取打开/创建数据库按钮的引用
10            b.setOnClickListener(                 //为打开/创建按钮添加监听器
11            new OnClickListener() {
12                @Override
13                public void onClick(View v) {
14                    createOrOpenDatabase();     //调用方法打开或创建数据库
15            }});
16            b = (Button) this.findViewById(R.id.Button02);//获取关闭数据库按钮的引用
17            b.setOnClickListener(                 //为关闭按钮添加监听器
18            new OnClickListener() {
19                @Override
20                public void onClick(View v) {
21                    closeDatabase();             //调用方法关闭数据库
22            }});
23            b = (Button) this.findViewById(R.id.Button03);     //获取添加记录按钮的引用
24            b.setOnClickListener(                 //为添加按钮添加监听器
25            new OnClickListener() {
26                @Override
27                public void onClick(View v) {
28                    insert();                     //调用方法插入记录
29            }});
30            b = (Button) this.findViewById(R.id.Button04);    //获取删除记录按钮的引用
31            b.setOnClickListener(                 //为删除按钮添加监听器
32            new OnClickListener() {
33                @Override
34                public void onClick(View v) {
35                    delete();                     //调用方法删除记录
36            }});
37            b = (Button) this.findViewById(R.id.Button05);     //获取查询记录按钮的引用
38            b.setOnClickListener(                 //为查询按钮添加监听器
39            new OnClickListener() {
40                @Override
41                public void onClick(View v) {
42                    query();                     //调用方法查询记录
43        }});}
44        public void createOrOpenDatabase() {            //创建或打开数据库的方法
45            try {
46                sld = SQLiteDatabase.openDatabase(
47                        "/data/data/com.bn.pp4/mydb",         //数据库所在路径
48                        null,                 //游标工厂,默认为null
49                        SQLiteDatabase.OPEN_READWRITE |
50                        SQLiteDatabase.CREATE_IF_NECESSARY //模式为读写,若不存在则创建
51                );                            //生成创建数据库的SQL语句
52                String sql = "create table if not exists student" +
53                        "(sno char(5),stuname varchar(20)," +
54                        "sage integer,sclass char(5))";
55                sld.execSQL(sql);                 //执行SQL语句
56                Toast.makeText(getBaseContext(), "成功创建数据库。", 
57                        Toast.LENGTH_LONG).show();
58            } catch (Exception e) {
59                e.printStackTrace();    
60        }}
61        public void closeDatabase() {                //关闭数据库的方法
62            try {
63                sld.close();                     //关闭数据库
64                Toast.makeText(getBaseContext(), "成功关闭数据库。",
65                        Toast.LENGTH_LONG).show();
66            } catch (Exception e) {
67                e.printStackTrace();
68        }}
69        public void insert() {                    //插入记录的方法
70            try  {                            //生成插入记录的SQL语句
71                String sql = "insert into student values" +
72                        "('001','Android',22,'283')";
73                sld.execSQL(sql);                 //执行SQL语句
74                Toast.makeText(getBaseContext(), "成功插入一条记录。",
75                         Toast.LENGTH_LONG).show();
76            } catch (Exception e) {
77                e.printStackTrace();
78        }}
79        public void delete() {                    //删除记录的方法
80            try  {                            //生成删除所有记录的SQL语句
81                String sql = "delete from student;";
82                sld.execSQL(sql);                 //执行SQL语句
83                Toast.makeText(getBaseContext(), "成功删除所有记录。",
84                        Toast.LENGTH_LONG).show();
85            } catch (Exception e) {
86                e.printStackTrace();
87        }}
88        public void query(){                        //查询的方法
89            try {                                //生成查询记录的SQL语句
90                String sql = "select * from student where sage>?";
91                Cursor cur = sld.rawQuery(sql, new String[] { "20" });   
                                             //获取Cursor对象引用
92                while (cur.moveToNext()) {            //若存在记录
93                    String sno = cur.getString(0);        //获取第一列信息
94                    String sname = cur.getString(1);     //获取第二列信息
95                    int sage = cur.getInt(2);             //获取第三列信息
96                    String sclass = cur.getString(3);     //获取第四列信息
97                    Toast.makeText(
98                            getBaseContext(),
99                            "查询到的记录为:'" + sno + "'\t'" + sname 
100                            + "'\t\t'" + sage+ "'\t'" + sclass + "'", 
101                            Toast.LENGTH_LONG).show();
102                }
103                cur.close();                             //关闭Cursor
104            } catch (Exception e) {
105                e.printStackTrace();
106    }}}

前一小节介绍了SQLite数据库中的一些操作,但有时数据库中的信息不但创建其的应用程序要使用,还希望能够分享给其他应用程序使用。这时就需要使用ContentProvider组件了,ContentProvider组件的基本情况如下所列。

下面使用ContentProvider组件将上一小节的案例进行升级,使得此案例具有分享数据给其他应用程序的能力,其具体开发步骤如下。

(1)在案例Sample2_4的com/bn/pp4包下创建MyContentProvider类,该类继承自ContentProvider类,并要实现其中所有的抽象方法,具体代码如下。

代码位置:见随书中源代码/第2章/Sample2_4/src/com/bn/pp4目录下的MyContentProvider.java。

1    package com.bn.pp4;
2    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3    public class MyContentProvider extends ContentProvider {  //继承ContentProvider
4        private static final UriMatcher um;                //声明Uri匹配引用
5        static {
6            um = new UriMatcher(UriMatcher.NO_MATCH);     //创建UriMatcher
7            um.addURI("com.bn.pp4.provider.student", "stu", 1); //设置匹配字符串
8        }
9        SQLiteDatabase sld;                             //声明SQLiteDatabase引用
10        @Override
11        public String getType(Uri uri) {
12            return null;
13        }
14        @Override    //调用数据库的query方法时会自动调用该方法
15        public Cursor query(Uri uri, String[] projection, String selection,
16                String[] selectionArgs, String sortOrder) {
17            switch (um.match(uri)) {                //若匹配成功
18            case 1:                             //执行操作,获取Cursor对象引用
19                Cursor cur = sld.query("student", projection, selection,
20                        selectionArgs, null, null, sortOrder);
21                return cur;                     //返回Cursor对象引用
22            }
23            return null;
24        }
25        @Override
26        public int delete(Uri arg0, String arg1, String[] arg2) {    //空实现
27            return 0;
28        }
29        @Override
30        public Uri insert(Uri uri, ContentValues values) {            //空实现
31            return null;
32        }
33        @Override
34        public boolean onCreate() {                     //创建数据库时自动调用该方法
35            sld = SQLiteDatabase.openDatabase(
36                    "/data/data/com.bn.pp4/mydb",     //数据库所在路径
37                    null,                         //游标工厂,默认为null
38                    SQLiteDatabase.OPEN_READWRITE|
39                    SQLiteDatabase.CREATE_IF_NECESSARY //读写、若不存在则创建
40            );
41            return false;
42        }
43        @Override
44        public int update(Uri uri, ContentValues values, String selection,
45                String[] selectionArgs) {                //空实现
46            return 0;
47    }}

(2)仅仅是完成上面的代码还是不够的,在Android程序开发中,有一个很重要的配置文件AndroidManifest.xml。要想使用ContentProvider组件,在完成代码的开发后,还必须在该配置文件中进行相应的配置,将如下代码插入到AndroidManifest.xml文件中的application标签中。

代码位置:见随书中源代码/第2章/Sample2_4目录下的AndroidManifest.xml。

1    <provider
2         android:name="MyContentProvider"              <!--将调用的类名-->
3         android:authorities="com/bn/pp4.provider.student"    <!--要匹配的Uri字符串-->
4          android:exported="true"/>

升级完了Sample2_4使其具有了数据分享能力之后,就可以在别的应用程序中通过ContentResolver匹配到Sample2_4案例中的ContentProvider组件获取分享的数据了。具体的开发步骤如下所列。

(1)创建项目Sample2_4_From,将项目的包名设定为com.bn.pp4f,并创建一个继承自Activity的类ContentConsumerActivity,其代码如下。

代码位置:见随书中源代码/第2章/ Sample2_4_From/src/com/bn/pp4f目录下的Content ConsumerActivity.java。

1    package com.bn.pp4f;                                        //包声明
2    import android.app.Activity;                                //相关类的引入
3    //……此处省略了部分相关类的引入代码,读者可自行查看随书的源代码
4    import android.widget.EditText;                                //相关类的引入
5    public class ContentConsumerActivity extends Activity {
6        ContentResolver cr;                // ContentResolver的引用
7        @Override                                            //重写方法的标志
8        public void onCreate(Bundle savedInstanceState) {
9            super.onCreate(savedInstanceState);            //继承父类的onCreate方法
10            setContentView(R.layout.main);                //跳转到主界面
11            cr=this.getContentResolver();                    //获取ContentResolver的对象
12            //初始化查询按钮
13            Button b=(Button)this.findViewById(R.id.Button01);        //Button类的引用
14            b.setOnClickListener(                                //设置按钮监听
15              new OnClickListener(){
16                @Override                            //重写方法的标志
17                public void onClick(View v) {        //重写onClick方法
18                    String stuname="Android";        //设置查询的字符串
19                    Cursor cur=cr.query(    
20                       Uri.parse("content://com.bn.pp4.provider.student/stu"),
21                       new String[]{"sno","stuname","sage","sclass"},
22                       "stuname=?",                     //查询条件
23                       new String[]{stuname},
24                       "sage ASC"    
25                    );
26                    while(cur.moveToNext()){
27                        String sno=cur.getString(0);            //获取学号
28                        String sname=cur.getString(1);            //获取名称
29                        int sage=cur.getInt(2);                    //获取年龄
30                        String sclass=cur.getString(3);            //获取班级
31                        appendMessage(sno+"\t"+sname+"\t\t"+sage+"\t"+sclass);
32                    }
33                    cur.close();                        //关闭ContentResolver
34        }});}
35        public void appendMessage(String msg){            //向文本区中添加文本
36                EditText et=(EditText)this.findViewById(R.id.EditText02);  
                                                 //获取EditText的对象
37                et.append(msg+"\n");                        //添加显示的字符串
38    }}

(2)Sample2_4_From案例开发完成后,运行该案例,其效果如图2-13和图2-14所示。

▲图2-13 运行界面1

▲图2-14 运行界面2

 说明 

图2-13为运行该案例后的界面效果图,图2-14为单击“获取”按钮后,通过ContentResolver匹配到Sample2_4案例中的ContentProvider组件获取数据后的效果图。

I/O即输入与输出,几乎所有文件操作的工作都离不开I/O。对于Android游戏开发来说,I/O操作尤为重要,游戏中的地图数据、人物图片资源等的流畅读取是保证游戏可玩性的重要方面。因此,在开发时,不同的场合必须选用合适的I/O操作方式,才能保证满足用户的需求。

Android中文件I/O分为3种方式:SD卡文件读取、手机中文件夹的访问和应用程序中assets文件的读取,本节将结合简单的小案例对这几种文件I/O操作进行介绍。

伴随着游戏品质的不断提高,游戏数据占用的存储空间也成几何级数增加,以往几十KB、几MB的手机游戏已经发展为几十MB、几百MB甚至几个GB的大型手机游戏。为了适应存储需求的增长,SD卡也一直在进行更新换代,4GB、8GB、16GB大小的SD卡已随处可见。

Android的设计者自然不会忽略这一点,在Android平台上可以轻松地对手机SD卡中的文件进行读取和操作。下面通过一个案例来详细讲解在Android开发中如何访问SD卡中的文件。

1.案例的运行效果

运行本节案例Sample2_5,在输入框中输入要读取的文件名,然后单击“打开文件”按钮。若SD卡中存在该文本文件,则在文本区域显示出文件的内容,反之则提示没有找到指定文件。本案例的运行效果如图2-15和图2-16所示。

▲图2-15 成功加载SD卡中的文件

▲图2-16 未找到指定的文件

 提示

运行本案例时要注意SD卡中文本文件的编码格式,在Android系统中一般要采用UTF-8编码才能保证没有乱码出现。编码格式为UTF-8的文本文件,见随书中源代码/第2章目录下的AndroidIO.txt,读者可以将该文件导入到手机或模拟器的SD卡中再对本案例进行测试。

2.案例的开发

介绍完本案例的运行效果以及文本文件编码的注意事项后,接下来将对本案例的主控制类Sample2_5_Activity进行开发,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_5/src/com/bn/pp5目录下的Sample2_5_Activity.java。

1    package com/bn/pp5;
2    import java.io.File;                                    //引入相关包
3    public class Sample2_5_Activity extends Activity {        //创建Activity
4        @Override
5        public void onCreate(Bundle savedInstanceState) {        //重写onCreate方法
6            super.onCreate(savedInstanceState);
7            setContentView(R.layout.main);                    //跳转到主界面
8            Button ok=(Button)this.findViewById(R.id.Button01);//获取打开按钮的引用
9            ok.setOnClickListener(                            //为打开按钮添加监听器
10                    new OnClickListener(){
11                    public void onClick(View v) {
12                    EditText et1=(EditText)findViewById(R.id.EditText01);
13                    String nr=loadText(et1.getText().toString().trim());
14                    EditText et2=(EditText)findViewById(R.id.EditText02);
15                    et2.setText(nr);                    //设置显示框内容
16            }});}
17        public String loadText(String name){                    //加载SD卡文件方法
18              String nr=null;                                    //内容字符串
19            try{
20                File f=new File("/sdcard/"+name);                //创建对应文件
21                byte[] buff=new byte[(int) f.length()];        //创建响应大小的byte数组
22            FileInputStream fis=new FileInputStream(f);
23            fis.read(buff);                                //读入文件
24            fis.close();                                //关闭输入流
25            nr=new String(buff,"utf-8");                    //转码生成字符串
26            nr=nr.replaceAll("\\r\\n","\n");                 //替换换行符
27            }catch (Exception e) {
28            Toast.makeText(getBaseContext(),                //Toast提示用户
29                        "对不起,没有找到指定文件。", 
30                        Toast.LENGTH_LONG).show();
31        }
32        return nr;                                     //返回内容字符串
33    }}

 提示

通过上面的小案例可以看出,Android中对SD卡文件的I/O实现与标准Java相同,因此,开发成本很低,易于上手。

前一小节中是将文件存放到SD卡中,其实文件也可以存放在手机内部存储(ROM,相当于PC的硬盘)中。不过在Android系统中,其为每个应用程序在手机的ROM中都分配了一个私有的目录,命名规则为“/data/data/<应用程序的包名>”。例如,应用程序的包名为“com.bn”,则系统分配的私有目录为“/data/data/com.bn”。

应用程序开发时,若需要在ROM中存放文件,一般应该存放到系统分配的私有目录中。启动Eclipse,进入其中的DDMS面板,打开File Explorer,即可查看手机ROM中的文件组织情况,如图2-17所示。

▲图2-17  查看手机内部存储文件

下面给出一个浏览手机ROM中文件夹的小案例,通过单击文件夹的名称,可以打开相应的文件夹,显示其中的文件列表。运行本案例,其运行效果如图2-18、图2-19和图2-20所示。

▲图2-18  程序初始运行

▲图2-19  进入dev目录

▲图2-20  单击出错

 提示

图2-18为程序一开始运行,显示“/”目录下内容的情况。图2-19为单击进入了dev目录,图2-20为单击了不允许进入或不是文件夹的条目。

了解了本案例的运行效果之后,接下来对本案例中唯一的类——Sample2_6_Activity进行开发,具体代码如下。

代码位置:见随书中源代码/第2章/Sample2_6/src/com/bn/pp6目录下的Sample2_6_Activity.java。

1    package com.bn.pp6;                                //声明包
2    import java.io.File;                             //引入相关类
3    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
4    import android.widget.AdapterView.OnItemClickListener;    //引入相关类
5    public class Sample2_6_Activity extends Activity {        //创建Activity
6        String currPath;                     //当前路径字符串
7        String rootPath = "/";                 //根目录路径
8        TextView currDirTV;                    //显示当前路径的TextView引用
9        @Override
10        public void onCreate(Bundle savedInstanceState) {
11            super.onCreate(savedInstanceState);
12            setContentView(R.layout.main);         //跳转到主界面
13            final ListView lv = (ListView) this.findViewById(R.id.lv);//获取ListView
14            Button back = (Button) this.findViewById(R.id.back);   //获取返回按钮
15            final File[] files = getFiles(rootPath);   
                                         //调用getFiles方法获取根目录下文件列表
16            currDirTV = (TextView) this.findViewById(R.id.currDirTV); //获取ListView
17            currPath = rootPath;
18            currDirTV.setText("当前路径:" + currPath);        //设置当前路径
19            initListView(files, lv);                         //初始化显示列表
20            back.setOnClickListener                         //返回按钮监听器
21            (new OnClickListener() {
22                @Override
23                public void onClick(View v) {
24                    if (!currPath.equals(rootPath)) {  
                                         //若当前路径不是根目录,返回到上一层目录
25                        File f = new File(currPath); //获取当前路径下的文件列表
26                        f = f.getParentFile();     //获取当前路径的上层路径
27                        currPath = f.getPath();     //更改当前路径
28                        currDirTV.setText("当前路径:" + currPath);//设置当前路径
29                        initListView(getFiles(currPath), lv); //初始化显示列表
30        }}});}
31        //获取当前目录下的文件列表的方法
32        public File[] getFiles(String filePath) {
33            File[] files = new File(filePath).listFiles();//获取当前目录下的文件列表
34            return files;                         //返回文件列表
35        }
36        ……//此处省略了初始化ListView的方法,读者可以自行查阅随书中的源代码
37    }

 提示

由于篇幅所限,初始化文件列表的代码省略,有需要的读者请自行查阅中的源代码进行学习。

Android还能将应用程序所需的数据文件打包到apk文件中,省去了使用者安装应用程序后还需要下载数据包的情况。但是要注意的是,打包到apk文件中的数据文件并不是可以放在任何位置,一般应该放到项目中的assets文件夹下。

 提示

对于特别大(如50MB)的数据文件,在实际开发中一般还是与apk安装文件分开的。对于不大的数据文件,就非常适合打包到apk包中的assets文件夹下。

下面将通过一个小案例来说明如何编程访问assets文件夹下的内容,其主要功能是完成对应用程序内部的assets文件夹下文件的读取。操作过程为首先在案例初始界面输入文件名,然后单击打开按钮。若存在该文件,则在下方显示出文件内容;反之,则弹出Toast提示用户重新输入。案例运行效果如图2-21和图2-22所示。

了解了本案例的功能后,下面来介绍本案例的开发过程,具体步骤如下。

(1)首先将AndroidIO.txt文本文件复制到项目中的assets文件夹中,如图2-23所示。

▲图2-21 程序运行效果图

▲图2-22 无法找到指定文件

▲图2-23 添加文本文件

(2)将AndroidIO.txt文本文件复制到项目中的assets文件夹后,接下来将开发本案例中唯一的类Sample2_7_Activity,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_7/src/com/bn/pp7目录下的Sample2_7_Activity.java。

1    package com.bn.pp7;
2    import java.io.ByteArrayOutputStream;                    //引入相关包
3    public class Sample2_7_Activity extends Activity {    //创建Activity
4        @Override
5        public void onCreate(Bundle savedInstanceState) {    //重写onCreate方法
6            super.onCreate(savedInstanceState);
7            setContentView(R.layout.main);                //跳转至主界面
8            Button ok=(Button)this.findViewById(R.id.Button01);    //获取打开按钮引用
9            ok.setOnClickListener(                         //为打开按钮添加监听器
10              new OnClickListener() {
11               public void onClick(View v) {
12                EditText et1=(EditText)findViewById(R.id.EditText01);
13                //调用loadText方法获取对应文件名的文件
14                String nr=loadText(et1.getText().toString().trim());
15                EditText et2=(EditText)findViewById(R.id.EditText02);
16                et2.setText(nr);                    //设置显示框内容
17        }});}
18        public String loadText(String name){                //加载assets文件方法
19             String nr=null;                                //内容字符串
20            try {                                        //打开对应名称文件的输入流
21                InputStream is=this.getResources().getAssets().open(name);
22                int ch=0;
23                //创建字节数组输出流
24                ByteArrayOutputStream baos=new ByteArrayOutputStream();
25                while((ch=is.read())!=-1){baos.write(ch);}    //读取文件
26                byte[] buff=baos.toByteArray();                //转化为字节数组
27                baos.close();                            //关闭输入输出流
28                is.close();                            //关闭输入输出流
29            nr=new String(buff,"utf-8");                //转码生成新字符串
30            nr=nr.replaceAll("\\r\\n","\n");            //替换换行符等空白字符
31            } catch (Exception e) {                    //没有找到对应文件,进行提示
32                Toast.makeText(getBaseContext(), "对不起,没有找到指定文件。", 
33                Toast.LENGTH_LONG).show();
34            }
35            return nr;                                //返回内容字符串
36    }}

 提示

通过上述案例的介绍,相信读者对Android平台下文件的存储和读取已经有所掌握。在真正的开发过程中,开发人员应该考虑各方面的因素,在需要时采用最适合自己的存储方式进行存储。

伴随着移动互联网的高速增长,智能手机上的游戏娱乐应用也逐步由单机向网络化方向发展。因此,对于Android开发人员来说,开发具有网络功能的应用程序成为了一项必备的技能。本节将向读者介绍网络开发中非常重要的一项技术,Socket(网络套接字)应用开发。

网络应用程序的开发也有很多不同的架构与模式,常见的有B/S、C/S等,Socket主要是用于进行传统的C/S模式应用的开发。C/S模式中主要需要开发两个端,服务端与客户端,其基本的工作原理如图2-24所示。

从图2-24中可以看出,C/S模式通信的基本过程如下。

(1)首先启动服务器,监听指定端口,等待接收客户端的连接请求。

(2)客户端请求连接到服务器的指定端口。

(3)服务器收到客户端的连接请求,建立连接。

(4)客户端和服务器同时各打开一个输入流和输出流,客户端的输出流与服务器的输入流连接,服务器的输出流与客户端的输入流连接。

(5)客户端与服务器端通过输入输出流进行双向的消息通信。

(6)当通信完毕后,客户端和服务器同时关闭本次连接。

▲图2-24 C/S模式通信的基本原理

由于Android应用程序是使用Java进行开发,因此,在Android平台下开发基于Socket的C/S模式程序非常简单,直接使用Java中的Socket与ServerSocket类即可。这与传统Java的网络套接字开发几乎没有区别,对于熟悉Java的大部分读者来说非常简单。

上一小节简单介绍了基于Socket的C/S模式网络程序的基本工作原理,从本小节开始将向读者介绍一个基于Socket的简单案例。首先介绍服务端的开发,由于本案例比较简单,所以仅有一个类Sample2_8_Server,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_8_Server/src/com/bn/Sample2_8_Server目录下的Sample2_8_Server.java。

1    package com.bn.Sample2_8_Server;                    //声明包名
2    import java.io.DataInputStream;                        //导入相关类
3    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
4    import java.net.Socket;                            //导入相关类
5    public class Sample2_8_Server{
6        static ServerSocket sSocket;                    // ServerSocket的引用
7        public static void main(String[] args){        //主方法
8            try{
9                sSocket=new ServerSocket(8877);        //创建ServerSocket对象
10                System.out.println("监听8877接口……");//打印信息
11                while(true){//服务器启动后一直循环服务于不同的客户端
12                   Socket socket=sSocket.accept();    //创建Socket对象
13                   DataInputStream diStream=new DataInputStream(socket. getInputStream());
14                   DataOutputStream dotStream=new DataOutputStream(socket.   
                       getOutputStream());
15                   System.out.println("客户端信息:"+diStream.readUTF());
16                   dotStream.writeUTF("成功连接服务器端");    //写入到输出流中
17                   diStream.close();                        //关闭输入流
18                   dotStream.close();                        //关闭输出流
19                   socket.close();                        //关闭Socket套接字
20            } }catch(Exception e){                        //捕获并打印异常信息
21                e.printStackTrace();
22    } } }

运行本案例,其运行效果如图2-25所示。

▲图2-25 对8877接口进行监听

开发完本案例的服务器端后,接下来在本小节中将主要介绍客户端的开发。由于本案例功能简单,因此客户端也仅有一个类Sample2_8_ClientActivity,其代码如下。

代码位置:见随书中源代码/第2章/Sample2_8_Client/src/com/bn/Sample2_8_Client目录下的Sample2_8_ClientActivity.java。

1    package com.bn.Sample2_8_Client;                     //声明包名
2    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3    public class Sample2_8_ClientActivity extends Activity{//创建继承Activity的主控制类
4        @Override
5        public void onCreate(Bundle savedInstanceState){     //重写的onCreate方法
6            super.onCreate(savedInstanceState);
7            setContentView(R.layout.main);                 //跳转到主界面
8            Button button=(Button)findViewById(R.id.button);     //获得Button按钮的引用
9            button.setOnClickListener(                    //为Button按钮添加监听器
10                new OnClickListener(){
11            public void onClick(View v){                 //重写的onClick方法
12                new Thread(){
13                    public void run(){
14                        connectServer();        //调用connectServer连接服务器
15                }}.start();
16         }} );}
17        public void connectServer(){                  //自定义的连接服务器的方法
18            String serverIp="10.16.189.20";                //声明服务器端IP
19            try{
20                Socket socket=new Socket(serverIp,8877); //创建Socket套接字,发出连接请求
21                DataInputStream din=new DataInputStream(socket.getInputStream());
22                DataOutputStream dout=new DataOutputStream(socket.getOutputStream());
23                EditText et=(EditText)this.findViewById(R.id.et);      
                                             //获得EditText输入对话框对象
24                String tempStr=et.getText().toString();    //获取该对话框中的信息
25                dout.writeUTF(tempStr);                     //将信息写入到输出流中
26                TextView tv=(TextView)this.findViewById(R.id.tv); //获得TextView的对象
27                tv.setText(din.readUTF());//将输入流中的数据在TextView中显示
28                din.close();                            //关闭输入流
29                dout.close();                            //关闭输出流
30                socket.close();                            //关闭Socket套接字
31            }catch(Exception e){
32                e.printStackTrace();                            //捕获并打印异常信息
33    }}}

 提示

在创建Socket套接字对象时需要指明端口号以及服务器端的IP,这里的IP必须为真实IP,不能使用环回地址“127.0.0.1”。因此运行本案例时读者有可能需要根据自己计算机的具体情况修改服务器的IP地址,使得程序可以正常运行。同时,在开发客户端时需要在AndroidManifest.xml中声明联网权限,具体情况参见中的源代码。

运行本案例,在上面的文本框中输入需要发送的信息,然后单击“连接服务器端”按钮。若网络没有问题,则在服务器端可以收到客户端发送的信息,如图2-26所示。同时,客户端也会收到服务器的反馈信息,如图2-27所示。

▲图2-26 服务器端收到客户端信息

▲图2-27 客户端成功连接到服务器

随着硬件设备价格的不断降低,大部分智能手机上都已配备了蓝牙网络模块,Android设备也是如此。如果能为一些小型的休闲娱乐游戏增加蓝牙联网对战的功能,将会大大增加游戏的可玩性。本节将向读者详细介绍如何在Android平台下开发具有蓝牙互联功能的应用程序。

蓝牙是一种支持设备短距离通信(一般是10m以内)的无线技术,其数据传输时不仅不需要连线,而且传输速率也比传统手持设备的红外模式更加迅速、高效,主要优势如下所列。

蓝牙无线技术规格供全球的成员公司免费使用。除了设备费用外,制造商不需要为使用蓝牙技术再支付任何知识产权费用,这大大降低了蓝牙技术的普及门槛。

蓝牙技术得到了广泛的应用,集成该技术的产品从手机、汽车到医疗设备等应有尽有,使用该技术的用户从消费者、工业市场到企业等,不一而足。

蓝牙是一项即时技术,其不要求固定的基础设施,且易于安装和设置,而且无须电缆即可实现连接,使用起来非常方便。

 提示

上一小节介绍的Socket技术虽然开发很容易,但对用户而言要么需要使用3G网络,要么需要通过Wi-Fi网络。3G网络虽然没有位置限制,但费用不低;Wi-Fi虽然免费,但必须在AP附近或自己架设AP。可以看出,对于短距离即时互联而言,采用蓝牙技术更为合理、便捷。

蓝牙无线技术是当今市场上支持范围最广泛,功能最丰富且安全的无线标准之一,全球范围内的资格认证程序可以测试成员的产品是否符合标准。

介绍完了蓝牙技术的特点与优势后,下面简单介绍一下蓝牙设备的使用步骤,具体如下所列。

(1)开启要搜索的设备的蓝牙功能,并设置为可见。

(2)在一个设备中开启搜索设备的功能,开始搜索设备。

(3)当搜索到其他设备后,会将搜索的设备显示在本设备的列表中。

(4)选择列表中的某一设备,请求匹配。

(5)被选中的设备收到匹配请求后,经双方验证同意,设备匹配成功。

(6)设备匹配成功后就可以建立连接,并收发数据了。

 提示

蓝牙通信与Socket网络通信的基本思想非常类似,都是连接成功后建立双向数据流收发数据,但开发起来要比Socket复杂一些。这主要是因为蓝牙设备的搜索功能和配对列表的显示需要开发人员自行编写代码实现,下面几个小节的案例将对此进行详细介绍。

本小节将介绍一个用蓝牙技术实现聊天功能的案例,通过对本案例的学习,读者可以掌握关于蓝牙通信的整个开发过程。在开发具体代码之前,首先了解一下本案例的运行方法及运行效果,具体情况如下所列。

(1)准备两部Android手机,在两部手机上都安装本案例的apk(Sample2_9.k)。

 提示

笔者使用的两部手机分别为华为u8800和摩托罗拉ME525,其他支持蓝牙功能的Android智能手机也可以使用。之所以要使用真机是因为模拟器几乎无法进行蓝牙程序的模拟测试,真机则要方便得多。

(2)将两部手机的蓝牙功能都打开,并设置为可见。

(3)在两部手机中同时运行本案例。

(4)在其中的一部手机中单击Menu键(手机上自带的菜单键)弹出设备搜索列表,如图2-28所示。然后单击列表下方的“扫描设备”按钮搜索设备,搜索完毕后列表中出现另一手机的设备名称及硬件地址,如图2-29所示。

▲图2-28 在u8800中弹出设备搜索列表

▲图2-29 u8800搜索到其他设备

(5)单击列表中搜索到的设备名称,一段时间后两部手机会同时提示连接成功,如图2-30和图2-31所示。

 提示

本案例运行时的界面还有很多,由于篇幅所限,这里只给出主要的运行界面,其他界面请读者自行运行随书中的本案例进行测试。

▲图2-30 u8800提示已连接到ME 525

▲图2-31 ME 525提示已连接到u8800

介绍完本案例的运行过程及效果后,本小节将详细介绍本案例的开发过程。由于本案例中涉及的类比较多,因此,在开发代码之前首先介绍一下本案例中各个类之间的关系及各自的用途,如图2-32所示。

▲图2-32 案例中各个类之间的关系及各自的用途

从图2-32可以看出,本案例中包含两个Activity、一个Service、3个线程,各自的用途如下所列。

了解完本案例中各个类之间的关系及各自的用途后,就可以进行代码的开发了,具体步骤如下所列。

(1)首先开发本案例的主控制类Sample2_9_Activity,该类在程序开始时执行,其中重写了onCreate方法、onStart方法以及onDestroy等方法,具体代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的Sample2_9_Activity.java。

1    package com.bn.pp9;                                        //声明包
2    import android.app.Activity;                                //引入相关类
3    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
4    import android.widget.Toast;                                //引入相关类
5    public class Sample2_9_Activity extends Activity {
6        private EditText outEt;                                //布局中的控件引用
7        private Button sendBtn;
8        private String connectedNameStr = null;                //已连接的设备名称
9        private StringBuffer outSb;                            //发送的字符信息
10        private BluetoothAdapter btAdapter = null;                //本地蓝牙适配器
11        private MyService myService = null;                        // Service引用
12        public void onCreate(Bundle savedInstanceState) {
13            super.onCreate(savedInstanceState);
14            setContentView(R.layout.main);
15            btAdapter = BluetoothAdapter.getDefaultAdapter();    //获取本地蓝牙适配器
16        }
17        public void onStart() {
18            super.onStart();
19            //如果蓝牙没有开启,提示开启蓝牙,并退出Activity
20            if (!btAdapter.isEnabled()) {
21                Toast.makeText(this, "请先开启蓝牙!", Toast.LENGTH_LONG).show();
22                finish();
23            } else {                            //否则初始化聊天的控件
24                if (myService == null)
25                    initChat();
26        }}
27        public synchronized void onResume() {
28            super.onResume();
29            if (myService != null) {                //创建并开启Service
30                if (myService.getState() == MyService.STATE_NONE) {  
                                             //如果Service为空状态
31                    myService.start();            //开启Service
32        }}}
33        private void initChat() {
34            outEt = (EditText) findViewById(R.id.edit_text_out); //获取编辑文本框的引用
35            sendBtn = (Button) findViewById(R.id.button_send);   
                                             //获取发送按钮引用,并为其添加监听
36            sendBtn.setOnClickListener(new OnClickListener() {
37                public void onClick(View v) {
38                    //获取编辑文本框中的文本内容,并发送消息
39                    TextView view = (TextView) findViewById(R.id.edit_text_out);
40                    String message = view.getText().toString();
41                    sendMessage(message);
42            }});
43            myService = new MyService(this, mHandler);        //创建Service对象
44            outSb = new StringBuffer("");        //初始化存储发送消息的StringBuffer
45        }
46        public void onDestroy() {
47            super.onDestroy();
48            if (myService != null) {            //停止Service
49                myService.stop();
50        }}
51        private void sendMessage(String message) {    //发送消息的方法
52            ……//此处省略了部分源代码,将在后面步骤中给出
53        }}
54                                            //处理从Service发来消息的Handler
55        private final Handler mHandler = new Handler() {
56            ……//此处省略了部分源代码,将在后面步骤中给出
57        }}};
58        public void onActivityResult(int requestCode, int resultCode, Intent data) {
59            ……//此处省略了部分源代码,将在后面步骤中给出
60        }}
61        public boolean onPrepareOptionsMenu(Menu menu) {
62                                            //启动设备列表Activity搜索设备
63            Intent serverIntent = new Intent(this, MyDeviceListActivity.class);
64            startActivityForResult(serverIntent, 1);
65            return true;
66    }}

(2)接下来介绍Sample2_9_Activity类中省略的sendMessage方法。该方法负责发送消息,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的Sample2_9_Activity.java。

1    private void sendMessage(String message) {            //发送消息的方法
2        //先检查是否已经连接到设备
3        if (myService.getState() != MyService.STATE_CONNECTED) {
4            Toast.makeText(this, R.string.not_connected, Toast.LENGTH_SHORT) .show();
5            return;
6        }
7        if (message.length() > 0) {            //如果消息不为空再发送消息
8            byte[] send = message.getBytes();    //获取发送消息的字节数组,并发送
9            myService.write(send);
10            outSb.setLength(0);                //消除StringBuffer和文本框的内容
11            outEt.setText(outSb);
12    }}

(3)然后介绍前面省略的处理从Service发来消息的Handler类的开发,用下列代码替代前面Sample2_9_Activity类中的第55-57行代码。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的Sample2_9_Activity.java。

1    // 处理从Service发来消息的Handler
2    private final Handler mHandler = new Handler() {
3        public void handleMessage(Message msg) {
4            switch (msg.what) {
5            case Constant.MSG_READ:
6                byte[] readBuf = (byte[]) msg.obj;
7                String readMessage = new String(readBuf, 0, msg.arg1);  
                                                 //创建要发送信息的字符串
8                Toast.makeText(Sample2_9_Activity.this,
9                        connectedNameStr + ":  " + readMessage,
10                        Toast.LENGTH_LONG).show();
11                break;
12            case Constant.MSG_DEVICE_NAME:
13                                                //获取已连接的设备名称
14                connectedNameStr = msg.getData().getString(
15                        Constant.DEVICE_NAME);
16                Toast.makeText(getApplicationContext(),//并弹出提示信息
17                        "已连接到 " + connectedNameStr, Toast.LENGTH_SHORT)
18                        .show();
19                break;
20    }}};

(4)接下来开发重写的onActivityResult方法,用下列代码替代前面Sample2_9_Activity类中的第58-60行代码。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的Sample2_9_Activity.java。

1    public void onActivityResult(int requestCode, int resultCode, Intent data) {
2        switch (requestCode) {
3        case 1:// 如果设备列表Activity返回一个连接的设备
4            if (resultCode == Activity.RESULT_OK) {
5                String address = data.getExtras().getString(//获取设备的MAC地址
6                        MyDeviceListActivity.EXTRA_DEVICE_ADDR);
7                BluetoothDevice device = btAdapter
8                        .getRemoteDevice(address);    //获取BLuetoothDevice对象
9                myService.connect(device);                //连接该设备
10            }
11            break;
12    }};

(5)下一步将开发用于显示可连接设备列表的Activity类——MyDeviceListActivity类,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyDeviceList Activity.java。

1    package com.bn.pp9;
2    import java.util.Set;                                    //引入相关类
3    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
4    import android.widget.AdapterView.OnItemClickListener;        //引入相关类
5    public class MyDeviceListActivity extends Activity {
6        public static String EXTRA_DEVICE_ADDR = "device_address";//extra信息名称
7        private BluetoothAdapter myBtAdapter;                    //成员变量
8        private ArrayAdapter<String> myAdapterPaired;
9        private ArrayAdapter<String> myAdapterNew;
10        @Override
11        protected void onCreate(Bundle savedInstanceState) {
12            super.onCreate(savedInstanceState);
13            requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);//设置窗口
14            setContentView(R.layout.device_list);
15            //设置为当结果是Activity.RESULT_CANCELED时,返回到该Activity的调用者
16            setResult(Activity.RESULT_CANCELED);
17            Button scanBtn = (Button) findViewById(R.id.button_scan);//初始化搜索按钮
18            scanBtn.setOnClickListener(new OnClickListener() {
19                public void onClick(View v) {
20                    doDiscovery();
21                    v.setVisibility(View.GONE);                //使按钮不可见
22            }});
23            //初始化适配器
24            myAdapterPaired = new ArrayAdapter<String>(this,
25                    R.layout.device_name);                    //已配对的
26            myAdapterNew = new ArrayAdapter<String>(this,
27                    R.layout.device_name);                    //新发现的
28            //将已配对的设备放入列表中
29            ListView lvPaired = (ListView) findViewById(R.id.paired_devices);
30            lvPaired.setAdapter(myAdapterPaired);
31            lvPaired.setOnItemClickListener(mDeviceClickListener);
32            //将新发现的设备放入列表中
33            ListView lvNewDevices = (ListView) findViewById(R.id.new_devices);
34            lvNewDevices.setAdapter(myAdapterNew);
35            lvNewDevices.setOnItemClickListener(mDeviceClickListener);
36            //注册发现设备时的广播
37            IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
38            this.registerReceiver(mReceiver, filter);
39            //注册搜索完成时的广播
40            filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
41            this.registerReceiver(mReceiver, filter);
42            //获取本地蓝牙适配器
43            myBtAdapter = BluetoothAdapter.getDefaultAdapter();
44            //获取已配对的设备
45            Set<BluetoothDevice> pairedDevices = myBtAdapter.getBondedDevices();
46            //将所有已配对设备信息放入列表中
47            if (pairedDevices.size() > 0) {
48                findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
49                for (BluetoothDevice device : pairedDevices) {
50                    myAdapterPaired.add(device.getName() + "\n"
51                            + device.getAddress());
52            }} else {
53                String noDevices = getResources().getText(R.string.none_paired).   
                          toString();
54                myAdapterPaired.add(noDevices);
55        }}
56        protected void onDestroy() {
57            ……//此处省略了部分源代码,该方法非常简单,请读者自行查看随书中的源代码。
58        }
59        private void doDiscovery() {
60            ……//此处省略了用蓝牙适配器搜索设备方法的部分源代码,将在后面步骤中给出
61        }
62        private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
63            ……//此处省略了列表中设备按下时的监听器的部分源代码,将在后面步骤中给出
64        };
65        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
66            ……//此处省略了监听搜索到的设备的部分源代码,将在后面步骤中给出
67    };}

提示  

由于篇幅所限,设备列表布局文件device_list.xml代码的开发在此没有进行介绍,需要的读者请自行查看随书中的源代码。

(6)接下来将开发前面省略的负责搜索设备的doDiscovery方法,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyDeviceList Activity.java。

1    private void doDiscovery() {
2        //在标题上显示正在搜索的标志
3        setProgressBarIndeterminateVisibility(true);
4        setTitle(R.string.scanning);
5        //显示搜索到的新设备的副标题
6        findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE);
7        if (myBtAdapter.isDiscovering()) {                //如果正在搜索,取消本次搜索
8            myBtAdapter.cancelDiscovery();
9        }
10        myBtAdapter.startDiscovery();                //开始搜索
11    }

![](/api/storage/getbykey/original?key=16090529a36db4062af9)说明

在该方法中首先更改UI界面(如在标题上显示正在搜索的标志),完成相关操作后,调用蓝牙适配器的startDiscovery方法进行设备搜索。

(7)下一步将开发前面省略的监听器mDeviceClickListener的对应代码,具体内容如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyDeviceList Activity.java。

1    private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
2        public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
3            myBtAdapter.cancelDiscovery();                //取消搜索
4            String msg = ((TextView) v).getText().toString();    //获取设备的MAC地址
5            String address = msg.substring(msg.length() - 17);
6            Intent intent = new Intent();            //创建带有MAC地址的Intent
7            intent.putExtra(EXTRA_DEVICE_ADDR, address);
8            setResult(Activity.RESULT_OK, intent);    //设置结果并退出Activity
9            finish();
10    }};

(8)接下来将开发前面省略的用于接收设备搜索到的系统广播的广播接收器mReceiver,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyDeviceList Activity.java。

1    //监听搜索到的设备的BroadcastReceiver
2    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
3        @Override
4        public void onReceive(Context context, Intent intent) {
5            String action = intent.getAction();
6            if (BluetoothDevice.ACTION_FOUND.equals(action)) { //如果找到设备
7                //从Intent中获取BluetoothDevice对象
8                BluetoothDevice device = intent
9                        .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
10                //如果没有配对,将设备加入新设备列表
11                if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
12                    myAdapterNew.add(device.getName() + "\n"
13                            + device.getAddress());
14                }
15                //当搜索完成后,改变Activity的标题
16            } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
17                    .equals(action)) {
18                setProgressBarIndeterminateVisibility(false);
19                setTitle(R.string.select_device);
20                if (myAdapterNew.getCount() == 0) {            //没有找到设备
21                    String noDevices = getResources().getText(
22                            R.string.none_found).toString();
23                    myAdapterNew.add(noDevices);
24    }}}};

(9)再接着开发自定义的用于后台服务的MyService类,其提供的后台服务主要是关于蓝牙设备的连接、数据的收发等方面,具体代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyService.java。

1    package com.bn.pp9;                                //声明包
2    import java.io.IOException;                        //引入相关类
3    ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
4    import android.os.Message;                            //引入相关类
5    public class MyService {                            //用于管理连接的Service
6        //本应用的唯一 UUID
7        private static final UUID MY_UUID =
8            UUID.fromString("fa87c0d0-afac-11de-8a39-0800200c9a66");
9            private final BluetoothAdapter btAdapter;        //成员变量
10            private final Handler myHandler;
11            private AcceptThread myAcceptThread;
12            private ConnectThread myConnectThread;
13            private ConnectedThread myConnectedThread;
14            private int myState;
15            //表示当前连接状态的常量
16            public static final int STATE_NONE = 0;           //什么也没做
17            public static final int STATE_LISTEN = 1;         //正在监听连接
18            public static final int STATE_CONNECTING = 2;     //正在连接
19            public static final int STATE_CONNECTED = 3;      //已连接到设备
20            //构造器
21            public MyService(Context context, Handler handler) {
22            btAdapter = BluetoothAdapter.getDefaultAdapter();
23            myState = STATE_NONE;
24            myHandler = handler;
25            }
26            private synchronized void setState(int state) {    //设置当前连接状态的方法
27            myState = state;
28            }
29            public synchronized int getState() {             //获取当前连接状态的方法
30            return myState;
31            }
32            public synchronized void start() {                //开启Service的方法
33                                                    //关闭不必要的线程
34            if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
35            if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread=   
               null;}
36            if (myAcceptThread == null) {                    //开启线程监听连接
37                myAcceptThread = new AcceptThread();
38                myAcceptThread.start();
39            }
40            setState(STATE_LISTEN);
41            }
42            public synchronized void stop() {                //停止所有线程的方法
43            if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread =null;}
44            if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread= null;}
45            if (myAcceptThread != null) {myAcceptThread.cancel(); myAcceptThread = null;}
46                    setState(STATE_NONE);
47            }
48            public void write(byte[] out) {                //向ConnectedThread写入数据的方法
49            ConnectedThread tmpCt;                    //创建临时对象引用
50            synchronized (this) {                    //锁定ConnectedThread
51                if (myState != STATE_CONNECTED) return;
52                tmpCt = myConnectedThread;
53            }
54            tmpCt.write(out);                        //写入数据
55            }
56           public synchronized void connect(BluetoothDevice device) {//连接设备的方法
57            ……//此处省略了部分源代码,将在后面步骤中给出
58            }
59         //开启管理和已连接的设备间通话的线程的方法
60            public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
61            ……//此处省略了部分源代码,将在后面步骤中给出
62            }
63        ……//以下省略了3个主要线程类的代码,将在后面介绍
64    }

 提示

第56-63行省略了connect 方法、connected方法及3个主要线程类的代码,这些代码的开发将在下面进行详细讲解。

(10)接下来将开发负责连接设备的connect方法,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyService.java。

1    public synchronized void connect(BluetoothDevice device) {    //连接设备的方法
2        //关闭不必要的线程
3         if (myState == STATE_CONNECTING) {
4                if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
5         }
6         if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread = null;}
7            myConnectThread = new ConnectThread(device);    //开启线程连接设备
8            myConnectThread.start();
9            setState(STATE_CONNECTING);
10            }

![](/api/storage/getbykey/original?key=1609585dd5985d15aa91)说明

该方法的实现比较简单,首先关闭不必要的线程,然后开启线程连接设备,并设置当前状态为正在连接。

(11)接着开发负责开启连接线程(ConnectedThread)并向myHandler发送设备名称消息的connected方法,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyService.java。

1    public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
2        //关闭不必要的线程
3         if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
4         if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread = null;}
5         if (myAcceptThread != null) {myAcceptThread.cancel(); myAcceptThread = null;}
6         myConnectedThread = new ConnectedThread(socket); //创建并启动ConnectedThread
7         myConnectedThread.start();
8         //发送已连接的设备名称到主界面Activity
9         Message msg = myHandler.obtainMessage(Constant.MSG_DEVICE_NAME);
10         Bundle bundle = new Bundle();
11         bundle.putString(Constant.DEVICE_NAME, device.getName());
12         msg.setData(bundle);
13         myHandler.sendMessage(msg);
14         setState(STATE_CONNECTED);
15    }

 提示

在该方法中首先要关闭不必要的线程,然后创建并开启ConnectedThread线程,同时发送已连接的设备名称到主界面Activity,最后设置当前状态为已连接。

(12)接下来将开发前面省略的MyService类中的3个主要线程,首先开发负责监听连接请求的AcceptThread,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyService.java。

1    private class AcceptThread extends Thread {        //用于监听连接的线程
2        //本地服务器端ServerSocket
3         private final BluetoothServerSocket mmServerSocket;
4         public AcceptThread() {
5             BluetoothServerSocket tmpSS = null;
6                 try {                        //创建用于监听的服务器端ServerSocket
7                   tmpSS = btAdapter.listenUsingRfcommWithServiceRecord("BluetoothChat",   
                    MY_UUID);
8                 }catch (IOException e) {e.printStackTrace();}
9                mmServerSocket = tmpSS;
10        }
11         public void run() {
12             setName("AcceptThread");            //设置线程名称
13                 BluetoothSocket socket = null;
14                 while (myState != STATE_CONNECTED) {        //如果没有连接到设备
15                    try {
16                        socket = mmServerSocket.accept();    //获取连接的Socket
17                    }catch (IOException e) {e.printStackTrace();break;}
18                    if (socket != null) {                    //如果连接成功
19                        synchronized (MyService.this) {
20                            switch (myState) {
21                            case STATE_LISTEN:
22                            case STATE_CONNECTING:
23                               //开启管理连接后数据交流的线程
24                                connected(socket, socket.getRemoteDevice());
25                                break;
26                            case STATE_NONE:
27                            case STATE_CONNECTED:
28                                try {                        //关闭新Socket
29                                    socket.close();
30                                } catch (IOException e) {
31                                    e.printStackTrace();
32                                }
33                                break;
34        }}}}}
35         public void cancel() {                //关闭本地服务器端ServerSocket的方法
36               try {
37                    mmServerSocket.close();            //调用close方法关闭ServerSocket
38                }catch (IOException e) {e.printStackTrace();}
39            }}

(13)下面开发负责用于尝试连接其他设备的ConnectThread,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyService.java。

1    private class ConnectThread extends Thread {        //用于尝试连接其他设备的线程
2        private final BluetoothSocket myBtSocket;
3          private final BluetoothDevice mmDevice;
4          public ConnectThread(BluetoothDevice device) {
5                 mmDevice = device;
6               BluetoothSocket tmp = null;
7                 //通过正在连接的设备获取BluetoothSocket
8                 try {
9                     tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
10             }catch (IOException e) {e.printStackTrace();}
11             myBtSocket = tmp;
12        }
13            public void run() {
14                 setName("ConnectThread");
15                 btAdapter.cancelDiscovery();            //取消搜索设备
16                 try {                                //连接到BluetoothSocket
17                     myBtSocket.connect();            //尝试连接
18              }catch (IOException e) {
19                   setState(STATE_LISTEN);                //连接断开后,设置状态为正在监听
20                  try {                            //关闭Socket
21                       myBtSocket.close();
22                  }catch (IOException e2) {e.printStackTrace();}
23                        MyService.this.start();            //如果连接不成功,重新开启Service
24                        return;
25                 }
26             synchronized (MyService.this) {            //将ConnectThread线程置空
27                    myConnectThread = null;
28             }
29             connected(myBtSocket, mmDevice);            //开启管理连接后数据交流的线程
30        }
31          public void cancel() {
32             try {
33                   myBtSocket.close();
34             } catch (IOException e) {e.printStackTrace();}
35    }}

(14)接下来将开发负责连接成功后信息收发的ConnectedThread,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9/src/com/bn/pp9目录下的MyService.java。

1    private class ConnectedThread extends Thread {
2        private final BluetoothSocket myBtSocket;
3          private final InputStream mmInStream;
4          private final OutputStream myOs;
5          public ConnectedThread(BluetoothSocket socket) {
6                 myBtSocket = socket;
7                 InputStream tmpIn = null;
8                 OutputStream tmpOut = null;
9                 try {                        //获取BluetoothSocket的输入输出流
10                      tmpIn = socket.getInputStream();
11                      tmpOut = socket.getOutputStream();
12                 } catch (IOException e) {e.printStackTrace();}    //打印异常
13                     mmInStream = tmpIn;
14                        myOs = tmpOut;
15        }
16         public void run() {
17               byte[] buffer = new byte[1024];
18                 int bytes;
19                 while (true) {                //一直监听输入流
20                     try {
21                      bytes = mmInStream.read(buffer);    //从输入流中读入数据
22                       //将读入的数据发送到主界面Activity
23                       myHandler.obtainMessage(Constant.MSG_READ, bytes, -1, buffer)
24                                .sendToTarget();
25                    } catch (IOException e) {
26                        e.printStackTrace();
27                        setState(STATE_LISTEN);    //连接断开后设置状态为正在监听
28                        break;
29        }}}
30         public void write(byte[] buffer) {    //向输出流中写入数据的方法
31             try {
32                    myOs.write(buffer);
33                 } catch (IOException e) {e.printStackTrace();}
34         }
35         public void cancel() {
36                 try {
37                        myBtSocket.close();                //关闭Socket
38                 } catch (IOException e) {e.printStackTrace();}
39    }}

(15)代码开发完成后还需要在AndroidManifest.xml中声明BLUETOOTH权限,其代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9目录下的AndroidManifest.xml。

1    <!--声明BLUETOOTH权限-->
2    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
3    <uses-permission android:name="android.permission.BLUETOOTH" />

 提示

上述代码应插入到AndroidManifest.xml中的“</manifest>”标签之前。

(16)在AndroidManifest.xml文件中声明完BLUETOOTH权限后,接下来需要在AndroidManifest.xml文件中注册自己开发的MyDeviceListActivity,具体代码如下。

代码位置:见本书随书中源代码/第2章/Sample2_9目录下的AndroidManifest.xml。

1    <!--注册MyDeviceListActivity-->
2    <activity android:name=".MyDeviceListActivity"
3        android:label="@string/select_device"
4          android:theme="@android:style/Theme.Dialog"
5          android:configChanges="orientation|keyboardHidden"
6    />

 提示

上述代码应插入到AndroidManifest.xml中的<application>与</application>标签之间。请读者注意的是,在创建项目时,指定的Activity其配置代码系统会自动加入到AndroidManifest.xml文件中,但自己在项目中再开发其他的Activity时就需要自行添加相应的配置代码到AndroidManifest.xml文件中了。

本章主要对Android平台下游戏开发中可能会用到的Android平台的相关基础知识进行详细介绍,如文件的I/O操作、SQLite数据库的增、删、改、查,游戏中声音特效、蓝牙互连等。只有学好这些基础知识,开发人员才能开发出更好的Android手机游戏。读者一定要在这些基础上下足工夫,今后的开发道路才会一帆风顺。

 提示

Android平台的其他相关知识还有很多,由于本书侧重于介绍OpenGL ES 3.0 3D应用程序的开发,故本章只是简单介绍了一些与3D开发不直接相关的,但在游戏开发中非常重要的Android平台相关的知识。若读者对Android平台很不熟悉,可以参考笔者在人民邮电出版社出版的《Android应用开发完全自学手册——核心技术、传感器、2D/3D、多媒体与典型案例》一书,其对Android各方面的基础知识都进行了详细介绍。


相关图书

Python面向对象编程:构建游戏和GUI
Python面向对象编程:构建游戏和GUI
精通游戏测试(第3版)
精通游戏测试(第3版)
罗布乐思开发官方指南 从入门到实践
罗布乐思开发官方指南 从入门到实践
游戏引擎原理与实践 卷2 高级技术
游戏引擎原理与实践 卷2 高级技术
游戏数值设计
游戏数值设计
游戏引擎原理与实践 卷1 基础框架
游戏引擎原理与实践 卷1 基础框架

相关文章

相关课程