Android游戏开发大全(第二版)
上QQ阅读APP看书,第一时间看更新

第4章 Android游戏开发中的数据存储和传感器

本章将向读者介绍Android平台下的应用程序如何对数据进行存储和读取,这些知识对于开发一个功能完备的应用程序非常有必要。同时,本章还将简单介绍 Android 平台的一个特色——传感器的相关知识,读者可以使用传感器开发出各种新奇的应用。

4.1 在Android平台上实现数据存储

本节将会对Android平台上的数据存取方式做简单介绍,主要包括基于文件的流读取、轻量级数据库SQLite、Content Provider,以及Preference的应用介绍。

4.1.1 私有文件夹文件的写入与读取

在介绍如何在Android平台上进行文件读取之前,有必要了解Android平台上的数据存储规则。在其他的操作系统,如Windows平台上,应用程序可以自由地或者在特定的访问权限基础上访问或修改其他应用程序名下的文件等资源,而在Android平台上,一个应用程序中所有的数据都是私有的,只对自己是可见的。

当应用程序被安装到系统中后,其所在的包会有一个文件夹用于存放自己的数据,仅这个应用程序才具有对这个文件夹的写入权限,这个私有文件夹位于 Android 系统的/data/data/<应用程序包名>目录下,其他的应用程序都无法在这个文件夹中写入数据。除了存放私有数据的文件夹外,应用程序也具有SD卡的写入权限。

使用文件I/O方法可以直接往手机中储存数据,默认情况下,这些文件不可以被其他的应用程序访问。Android 平台支持 Java 平台下的文件 I/O 操作,主要使用 FileInputStream 和FileOutputStream这两个类来实现文件的存储与读取。获取这两个类对象的方式有两种。

● 第一种方式就是像 Java 平台下的实现方式一样,通过构造器直接创建,如果需要向打开的文件末尾写入数据,可以通过使用构造器FileOutputStream(File file,boolean append)将 append设置为true来实现。需要注意的是,采用这种方式获得FileOutputStream对象时,如果文件不存在或不可以写入,程序会抛出FileNotFoundException异常。

● 第二种获取FileInputStream和FileOutputStream对象的方式是调用Context.openFileInput和 Context.openFileOutput 两个方法来创建。除了这两个方法外,Context 对象还提供了其他几个用于对文件操作的方法,如表4-1所示。

表4-1 Context对象中文件操作的API及说明

续表

在使用 openFileOutput 方法打开文件用以写入数据时,需要指定打开模式。默认为零,即MODE_ PRIVATE。不同的模式对应的含义如表 4-2所示。

表4-2 openFileOutput方法打开文件时的模式

下面通过一个例子来说明Android平台上的文件I/O操作方式,本例主要的功能是在应用程序私有的数据文件夹下创建一个文件,并读取其中的数据显示到屏幕的TextView中。本例中应用程序的开发分为以下4个步骤。

(1)首先,在应用程序的布局文件main.xml中声明屏幕中要显示的TextView控件,其代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_1/res/layout目录下的main.xml。

1 <?xml version="1.0" encoding="utf-8"?>

2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

3  android:orientation="vertical"

4  android:layout_width="fill_parent" android:layout_height="fill_parent"

5  >      <!-- 声明一个LinearLayout线性布局 -->

6  <TextView android:id="@+id/tv"

7  android:layout_width="fill_parent" android:layout_height="wrap_content"

8   />     <!-- 声明一个TextView控件,id为tv -->

9 </LinearLayout>

说明

默认情况下,当新建一个工程时,会在main.xml中自动声明一个TextView,不过该TextView是没有ID的,所以需要添加第6行的代码进行ID的声明。

(2)Activity主要代码的开发。在Activity中主要进行的工作是,调用自己开发的文件写入和读取方法来获得数据信息,并将其内容显示在TextView中,其主要的代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_1/src/wyf/wpf目录下的Sample_4_1.java。

1 package wyf.wpf;         //声明包语句

2 import java.io.FileInputStream;      //引入相关类

3 import java.io.FileOutputStream;      //引入相关类

4 import org.apache.http.util.EncodingUtils;    //引入相关类

5 import android.app.Activity;       //引入相关类

6 import android.os.Bundle;        //引入相关类

7 import android.widget.TextView;       //引入相关类

8 //继承自Activity的子类

9 public class Sample_4_1 extends Activity {

10  public static final String ENCODING = "UTF-8";  //常量,为编码格式

11  String fileName = "test.txt";      //文件的名称

12  String message = "你好,这是一个关于文件I/O的示例。"; //写入和读出的数据信息

13  TextView tv;           //TextView对象引用

14  @Override

15  public void onCreate(Bundle savedInstanceState) { //重写onCreate方法

16    super.onCreate(savedInstanceState);

17    setContentView(R.layout.main);   //设置当前屏幕

18   writeFileData(fileName, message);   //创建文件并写入数据

19    String result = readFileData(fileName); //根据id获取TextView对象//的引用

20    tv = (TextView)findViewById(R.id.tv);  //设置TextView的内容

21    tv.setText(result);      //获得从文件读入的数据

22  }

23 ……//此处省略writeFileData方法的代码,将在后面的步骤补全

24 ……//此处省略readFileData方法的代码,将在后面的步骤补全

25 }

● 程序的第 10 行定义了一个常量“ENCODING”,该常量代表编码格式,在随后的读入文件数据并转换成字符串对象时会用到。

● 代码第11~12行为需要操作的文件名和要写入的数据信息。

● 代码18~19行分别调用了writeFileData和readFileData方法用于向文件中写入数据和从文件中读取数据,在下面的步骤中将会编写这两个方法的具体代码。

(3)编写对文件进行写入的方法代码,步骤(1)代码第23行省略的writeFileData方法的代码如下。

1 //方法:向指定文件中写入指定的数据

2 public void writeFileData(String fileName,String message){

3  try{

4  FileOutputStream fout = openFileOutput(fileName, MODE_PRIVATE);//获得FileOutputStream

5  byte [] bytes = message.getBytes(); //将要写入的字符串转换为byte数组

6  fout.write(bytes);      //将byte数组写入文件

7  fout.close();       //关闭FileOutputStream对象

8  }

9  catch(Exception e){

10  e.printStackTrace();     //捕获异常并打印

11 }

12 }

说明

writeFileData方法所做的工作比较简单,其接收表示文件名和数据信息的字符串,通过Context.openFileOutput方法打开输出流,将字符串转换成byte数组写入文件。

(4)最后来编写从文件中读取数据的readFileData方法的代码,该方法在Activity的onCreate方法中被调用,下面代码即是步骤(1)中代码中第24行省略的readFileData方法的代码。

1 //方法:打开指定文件,读取其数据,返回字符串对象

2 public String readFileData(String fileName){

3  String result="";

4  try{

5    FileInputStream fin = openFileInput(fileName);//获得FileInputStream对象

6    int length = fin.available();  //获取文件长度

7    byte [] buffer = new byte[length]; //创建byte数组用于读入数据

8    fin.read(buffer);     //将文件内容读入到byte数组中

9    result = EncodingUtils.getString(buffer, ENCODING);//将byte数组转换成指定格式的字符串

10  fin.close();        //关闭文件输入流

11  }

12  catch(Exception e){

13    e.printStackTrace();     //捕获异常并打印

14  }

15  return result;       //返回读到的数据字符串

16 }

● 代码第5行调用Context.openFileInput打开指定文件名的文件,获取FileInputStream对象,并在第6行调用其available方法获取文件的字节数。available方法返回文件输入流中从当前位置算起所剩的字节数。

● 代码第 7 行根据获取的字节数创建一个与之大小相等的 byte 数组,在第 8 行通过调用FileInputStream的read方法将文件中的数据读入到byte数组中。

● 代码第9行调用了EncodingUtils类的静态方法getString,将接收到的byte转换成指定编码格式的字符串。EncodingUtils中封装了用于字符串编码的一些静态方法。

程序运行后界面如图 4-1 所示。本例采用的是调用 Context 的openFileInput和openFileOutput方法来获取文件的输入/输出流对象,读者也可以采用构造器的方式来获取文件的输入/输出流对象,在此不再赘述。

图4-1 程序运行界面

4.1.2 读取Resources和Assets中的文件

在Android平台上,除了对应用程序的私有文件夹中的文件进行操作之外,还可以从资源文件和Assets中获得输入流读取数据,这些文件分别存放在应用程序的res/raw目录和assets目录下,这些文件将会在编译的时候和其他文件一起被打包。

需要注意的是,来自Resources和Assets中的文件只可以读取而不能够进行写操作,下面就通过一个例子来说明如何从 Resource 和Assets中的文件中读取信息。首先,分别在 res/raw 和 assets 目录下新建两个文本文件“test1.txt”和“test2.txt”用以读取。

为避免字符串转码带来的麻烦,可以将这两个文本文件的编码格式设置为UTF-8。设置编码格式的方法有多种,比较简单的一种是用 Windows 的记事本打开文本文件,在另存为对话框中的编码格式中选择“UTF-8”,如图4-2所示。

图4-2 在记事本的另存为对话框中选择编码格式

设置好文件的编码格式后,就可以开发源代码了,本例中的程序只包含一个 Activity,其开发步骤如下。

(1)首先,在应用程序的main.xml中声明两个TextView控件,具体代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_2/res/layout目录下的main.xml。

1 <?xml version="1.0" encoding="utf-8"?>

2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

3  android:orientation="vertical"

4  android:layout_width="fill_parent" android:layout_height="fill_parent"

5  >         <!-- 声明一个LinearLayout线性布局 -->

6  <TextView android:id="@+id/tv1"

7  android:layout_width="fill_parent" android:layout_height="wrap_content"

8  />         <!-- 声明一个TextView控件,id为tv1 -->

9  <TextView android:id="@+id/tv2"

10  android:layout_width="fill_parent" android:layout_height="wrap_content"

11  />         <!-- 声明一个TextView控件,id为tv2 -->

12 </LinearLayout>

说明

上述代码声明了2个TextView控件,将来在程序中将会分别显示读取自res/raw目录下的和读取自asserts目录下的文件数据。

(2)开发Activity的主要代码。Activity的主要功能是调用方法分别读取来自res/raw目录下和assets目录下的文件的数据。其主要代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_2/src/wyf/wpf目录下的Sample_4_2.java。

1 package wyf.wpf;        //声明包语句

2 import java.io.InputStream;      //引入相关类

3 import org.apache.http.util.EncodingUtils;  //引入相关类

4 import android.app.Activity;     //引入相关类

5 import android.os.Bundle;      //引入相关类

6 import android.widget.TextView;     //引入相关类

7 //继承自Activity的子类

8 public class Sample_4_2 extends Activity {

9  public static final String ENCODING = "UTF-8"; //常量,代表编码格式

10 TextView tv1;         //TextView的引用

11 TextView tv2;         //TextView的引用

12  @Override

13  public void onCreate(Bundle savedInstanceState) {

14   super.onCreate(savedInstanceState);

15   setContentView(R.layout.main);   //设置显示屏幕

16   tv1 = (TextView)findViewById(R.id.tv1);

17   tv2 = (TextView)findViewById(R.id.tv2);

18   tv1.setText(getFromRaw("test1.txt")); //将tv1的显示内容设置为//Resource中的raw文件夹的文件

19   tv2.setText(getFromAsset("test2.txt")); //将tv2的显示内容设置为Asset//中的文件

20  }

21 ……//此处省略getFromRaw方法的代码

22 ……//此处省略getFromAsset方法的代码

21 }

● 代码第16~17行调用findViewById方法获得屏幕上两个TextView控件的对象引用。

● 代码第18~19行将Activity中的两个TextView的显示内容设置为了两个方法getFromRaw和getFromAsset的返回值,这两个方法的具体代码将在后面的步骤中详细介绍。

(3)编写getFromRaw方法的代码。getFromRaw方法的功能是从res/raw目录下读取指定文件名的文件,将其信息以字符串的形式返回,其代码为步骤(1)中第21行省略掉的部分,代码如下。

1 //方法:从resource中的raw文件夹中获取文件并读取数据

2 public String getFromRaw(String fileName){

3  String result = "";

4   try{

5   InputStream in = getResources().openRawResource(R.raw.test1);//从raw中的文件获取输入流

6   int length = in.available();   //获取文件的字节数

7   byte [] buffer = new byte[length];  //创建byte数组

8   in.read(buffer);      //将文件中的数据读取到byte数组中

9   result = EncodingUtils.getString(buffer, ENCODING);//将byte数组转换成指定格式的字符串

10   }

11   catch(Exception e){

12   e.printStackTrace();     //捕获异常并打印

13   }

14  return result;

15  }

● 代码第 5 行通过调用 Resources 的.openRawResource 方法从 raw 目录下的指定文件获取InputStream对象。

● 程序的第6~9行与4.1.1小节的代码比较类似,都是先获取文件的字节数,然后创建相同长度的byte数组,并将输入流中的数据读入到byte数组中,最后通过指定的编码格式将byte数组转换为字符串并将其返回。

(4)编写getFromAsset方法的代码。在步骤(1)代码中第22行省略的getFromAsset方法的代码与getFromRaw方法类似,只是获取输入流对象InputStream的方式不同,将步骤(2)代码的第5行改为以下代码就是getFromAsset方法的代码了。

1 InputStream in = getResources().getAssets().open(fileName);//从Assets中的文件获//取输入流

程序运行后,会通过这两个方法读取文件中的数据显示到屏幕上,如图4-3所示。

图4-3 程序运行后效果图

4.1.3 轻量级数据库SQLite简介

SQLite是一款开源的嵌入式数据库引擎,其对多数的SQL92标准都提供了支持。相比于同样开源的MySQL和PostgreSQL来说,SQLite具有以下独特的地方。

● 处理速度快。传统的数据库引擎采用的是客户端-服务端的访问方式,SQLite的数据库引擎嵌入到程序中作为其一部分,所以运行的速度要快很多。

● 占用资源少。SQLite源代码总共不到3万行,运行时所占内存不超过250KB,而且不需要安装部署,并支持多线程访问。

● SQLite 中所有的数据库信息,如表、索引等全部集中存放在一个文件中,SQLite 支持事务,在开始一个新事务时会将整个数据库文件加锁。

● 支持Windows、Linux等主流操作系统,可以采用多种语言进行操作,如Java、PHP等。

总之,SQLite 的这些特性都非常适合作为移动设备的数据存储。Android 平台提供了对创建和使用SQLite数据库的支持,其装载了sqlite3组件,任何一个SQLite数据库对于创建该数据库的应用程序来说都是私有的,SQLite的数据库文件位于/data/data/package-name/databases目录下。

下面介绍在Android平台上如何对SQLite数据库进行操作,本书把对数据库的操作分成了3个步骤,分别是创建数据库对象、操作数据和检索数据。

1.创建数据库对象

要想对数据库进行操作,首先要获得SQLiteDatabase对象,SQLiteDatabase类提供了一些静态方法用于创建或打开一个数据库。这些方法及说明如表4-3所示。

表4-3 SQLiteDatabase类提供的创建或打开数据库的方法

其中openDatabase方法中可以通过参数flags指定打开的模式,不同的模式及其说明如表4-4所示。

表4-4 openDatabase可指定的打开模式及其说明

同在4.1.1小节文件I/O操作类似,Context对象也提供了用于打开数据库的方法:openOrCreate Database (Sting name,int mode,SQLiteDatabase.CursorFactory factory),该方法打开/创建指定名称的数据库文件,不过这里传入的mode并不是表4-4中的几种模式之一,而是4.1.1小节中表4-2中的几种模式之一。

除了基于文件系统的数据库外,在 Android 中还可以创建内存数据库。调用 SQLiteDatabase的静态方法 create(SQLiteDatabase.CursorFactory factory)创建内存数据库,如果对数据库的操作速度要求较高,可以考虑采用内存数据库来实现。

除了上述几种创建或打开数据库的方式外,在实际开发过程中,为方便起见,还可以开发一个数据库辅助类来创建或打开数据库,这个辅助类继承自SQLiteOpenHelper类,在该类的构造器中,调用Context中的方法创建并打开一个指定名称的数据库对象。继承和扩展SQLiteOpenHelper类主要做的工作就是重写以下两个方法。

● onCreate(SQLiteDatabase db):当数据库被首次创建时执行该方法,一般将创建表等初始化操作放在该方法中执行。

● onUpgrade(SQLiteDatabase dv,int oldVersion,int newVersion):当打开数据库时传入的版本号与当前版本号不同时会调用该方法。

说明

除了上述两个实现的方法外,还可以选择性地使用onOpen方法,该方法会在每次打开数据库时被调用。

SQLiteOpenHelper类的基本用法是:当需要创建或打开一个数据库并获得数据库对象时,首先根据指定的文件名创建一个辅助对象,然后调用该对象的 getWritableDatabase 或 getReadable Database方法获得SQLiteDatabase对象。

提示

调用getReadableDatabase方法返回的并不总是只读的数据库对象,一般来说该方法和getWritableDatabase方法的返回情况相同,只有在数据库仅开放只读权限或磁盘已满时才会返回一个只读的数据库对象。

2.操作数据

对数据库的操作一般来讲就是增、删、改、查,SQLiteDatabase 类提供了很多方法用来对数据库进行操作,其中既含有直接接收SQL语句作为执行参数的方法,也含有专门用于增、删、改、查的方法。常用的数据库操作方法如表4-5所示。

表4-5 常用的数据库操作方法及参数说明

续表

3.检索数据

对数据的检索涉及的内容稍微多一些,所以在此单独介绍。SQLiteDatabase 中提供了直接解析SQL语句的查询方法和专门用于查询的方法,这些方法及说明如表4-6所示。

表4-6 用于查询数据的方法及说明

说明

表4-6列出的一系列的query方法,其实是将SQL语句进行分解,让整个SQL语句的每个部分和子句都独立出来作为供调用者选择的参数。需要注意,如果这些参数对应 SQL 中的某个子句,在传入参数的时候不应该包含这些子句的关键字,如selection参数中不应该包含“where”关键字。

4.1.4 SQLite的使用示例

前面的小节简单介绍了SQLite的相关知识,本小节将会举一个简单的例子来说明SQLite的用法。本例中使用数据库的辅助类创建和打开数据库,并在 Activity 中调用方法对数据库进行插入、更新和查询等操作,整个程序分以下4个步骤来完成。

(1)首先,在应用程序的main.xml中声明TextView控件。

代码位置:见随书光盘中源代码/第4章/Sample_4_3/res/layout目录下的main.xml。

1 <?xml version="1.0" encoding="utf-8"?>

2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

3  android:orientation="vertical"

4  android:layout_width="fill_parent" android:layout_height="fill_parent"

5  >      <!-- 声明一个LinearLayout线性布局 -->

6  <TextView android:id="@+id/tv"

7   android:layout_width="fill_parent" android:layout_height="wrap_content"

8  />     <!-- 声明一个TextView控件,id为tv -->

9 </LinearLayout>

说明

代码第6~8行声明的TextView控件将会在程序中负责显示从数据库中检索到的信息。

(2)创建数据库的辅助类。该类继承自SQLiteOpenHelper类,并将重写其onCreate等方法,其代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_3/src/wyf/wpf目录下的MySQLiteHelper. java。

1 package wyf.wpf;            //声明包语句

2 import android.content.Context;        //引入相关类

3 import android.database.sqlite.SQLiteDatabase;    //引入相关类

4 import android.database.sqlite.SQLiteOpenHelper;    //引入相关类

5 import android.database.sqlite.SQLiteDatabase.CursorFactory; //引入相关类

6 //继承自SQLiteOpenHelper的子类

7 public class MySQLiteHelper extends SQLiteOpenHelper{

8   public MySQLiteHelper(Context context, String name, CursorFactory factory,

9     int version) {

10    super(context, name, factory, version);   //调用父类的构造器

11  }

12  @Override

13  public void onCreate(SQLiteDatabase db) {    //重写onCreate方法

14    db.execSQL("create table if not exists hero_info("//调用execSQL方法创//建表

15      + "id integer primary key,"

16      + "name varchar,"

17      + "level integer)");

18  }

19 ……//在此省略重写的onUpgrade方法,读者可以自行查阅随书光盘

20 }

● 代码第10行调用了父类的构造器创建或打开数据库文件。

● 代码第13~18行为重写的onCreate方法,在该方法中创建了一张名为“hero_info”的表,表中有“id”、“name”和“level”3个字段。onCreate方法在第一次创建数据库文件时被调用。

● 代码第14行调用了execSQL方法,需要注意的是,传入的SQL语句字符串中不需要加上分号。

(3)开发 Activity 部分的代码。创建完数据库的辅助对象后,就需要在 Activity 中开发针对数据库具体操作的方法了,Activity中主要代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_3/src/wyf/wpf目录下的Sample_4_3.java。

1 package wyf.wpf;         //声明包语句

2 import android.app.Activity;      //引入相关类

3 import android.content.ContentValues;    //引入相关类

4 import android.database.Cursor;      //引入相关类

5 import android.database.sqlite.SQLiteDatabase;  //引入相关类

6 import android.os.Bundle;       //引入相关类

7 import android.widget.TextView;      //引入相关类

8 //继承自Activity的子类

9 public class Sample_4_3 extends Activity {

10 MySQLiteHelper myHelper;       //数据库辅助类对象的引用

11 TextView tv;          //TextView对象的引用

12 @Override

13  public void onCreate(Bundle savedInstanceState) {

14   super.onCreate(savedInstanceState);

15   setContentView(R.layout.main);    //设置显示的屏幕

16   tv = (TextView)findViewById(R.id.tv);  //获得TextView对象的引用

17   myHelper = new MySQLiteHelper(this, "my.db", null, 1);//创建数据库辅助类对象

18   insertAndUpdateData(myHelper);    //向数据库中插入和更新数据

19   String result = queryData(myHelper);  //向数据库中查询数据

20   tv.setText("名字\t等级\n"+result);   //将查询到的数据显示到屏幕上

21  }

22 ……//在此省略insertAndUpdateData方法的代码,将在随后补全

23 ……//在此省略queryData方法的代码,将在随后补全

24 @Override

25 protected void onDestroy () {

26  SQLiteDatabase db = myHelper.getWritableDatabase(); //获取数据库对象

27  db.delete("hero_info", "1", null); //删除hero_info表中的所有数据

28  super. onDestroy ();

29 }

30 }

● 代码第17行创建了数据库的辅助对象,其主要的作用是,在随后的代码中通过调用getWritable Database或getReadableDatabase方法来获得数据库对象。

● 代码第25~29行重写了onDestroy方法,即在程序退出时删除hero_info表中所有的数据。

(4)编写insertAndUpdateData和queryData方法的代码。在步骤(2)中的代码第18行和第19行分别调用了insertAndUpdateData和queryData方法,这两个方法的功能是对数据库进行各种操作,其代码如下。

1 //方法:向数据库中的表中插入和更新数据

2 public void insertAndUpdateData(MySQLiteHelper myHelper){

3   SQLiteDatabase db = myHelper.getWritableDatabase(); //获取数据库对象

4  //使用execSQL方法向表中插入数据

5   db.execSQL("insert into hero_info(name,level) values('Hero1',1)");

6  //使用insert方法向表中插入数据

7   ContentValues values = new ContentValues();//创建ContentValues对象存储“列名-列值”映射

8  values.put("name", "hero2");

9  values.put("level", 2);

10  db.insert("hero_info", "id", values);   //调用方法插入数据

11  //使用update方法更新表中的数据

12  values.clear();        //清空ContentValues对象

13  values.put("name", "hero2");

14  values.put("level", 3);

15  db.update("hero_info", values, "level = 2", null);//更新表中level为2的那行数据

16  db.close();         //关闭SQLiteDatabase对象

17 }

18 //方法:从数据库中查询数据

19 public String queryData(MySQLiteHelper myHelper){

20  String result="";

21  SQLiteDatabase db = myHelper.getReadableDatabase(); //获得数据库对象

22  Cursor cursor = db.query("hero_info", null, null, null, null, null, "id asc");//查询表中数据

23  int nameIndex = cursor.getColumnIndex("name");   //获取name列的索引

24  int levelIndex = cursor.getColumnIndex("level");  //获取level列的索引

25  for(cursor.moveToFirst();!(cursor.isAfterLast());cursor.moveToNext()){//遍历结果集,提取数据

26    result = result + cursor.getString(nameIndex)+" ";

27    result = result + cursor.getInt(levelIndex)+" \n";

28  }

29  cursor.close();         //关闭结果集

30  db.close();          //关闭数据库对象

31  return result;

32 }

● 代码第5、第10行分别采用不同的方式向表中插入数据,而代码第15行通过调用update方法将之前插入level为2的那行数据进行修改。

● 代码第22行调用了query方法对表进行查询,由于传入的各项条件(如where等子句)都为null,该方法返回的将是表中的所有行,返回的结果按id列的升序排列。

● 代码第25行是对查询返回的Cursor对象进行遍历的代码,moveToFirst方法将Cursor移动到数据的第一行,isAfterLast 方法判断 Cursor 是否已经位于最后一行之后,moveToNext方法将Cursor移动到下一行。

● 代码第29行和第30行分别调用Cursor和SQLiteDatabase的close方法将其关闭。程序运行后如图4-4所示。

图4-4 程序运行效果图

提示

由于本例中只是说明 SQLite 的用法,所以数据库中涉及的表名、列名并没有使用将其定义为常量的做法,建议读者在开发的过程中将这些信息定义为常量,这样代码的可读性和可维护性都会得到提高。

4.1.5 数据共享者——Content Provider的使用

Content Provider属于Android应用程序的组件之一,这点在本书的第3章已经有所介绍。作为应用程序之间惟一的共享数据的途径,Content Provider主要的功能就是存储并检索数据以及向其他应用程序提供访问数据的接口。

Android 系统为一些常见的数据类型(如音频、视频、图像、手机通信录联系人信息等)内置了一系列的Content Provider,这些都位于 android.provider包下。持有特定的许可,可以在自己开发的应用程序中访问这些Content Provider。

让自己的数据和其他应用程序共享有两种方式:创建自己的 Content Provider(即继承自ContentProvider的子类)或者是将自己的数据添加到已有的Content Provider中去,后者需要保证现有的Content Provider和自己的数据类型相同,并且具有该Content Provider的写入权限。对于Content Provider,最重要的就是数据模型(data model)和URI。

1.数据模型(data model)

Content Provider将其存储的数据以数据表的形式提供给访问者,在数据表中每一行为一条记录,每一列为具有特定类型和意义的数据。每一条数据记录都包括一个“_ID”数值字段,该字段惟一标识一条数据。

2.URI

URI,每一个 Content Provider都对外提供一个能够惟一标识自己数据集(data set)的公开URI,如果一个Content Provider管理多个数据集,其将会为每个数据集分配一个独立的URI。所有的Content Provider的URI都以“content://”开头,其中“content:”是用来标识数据是由Content Provider管理的scheme。

在几乎所有的Content Provider的操作中都会用到URI,因此,如果是自己开发Content Provider,最好将URI定义为常量,这样在简化开发的同时也提高了代码的可维护性。

首先来介绍如何访问Content Provider中的数据,访问Content Provider中的数据主要通过ContentResolver对象,ContentResolver类提供了成员方法可以用来对Content Provider中的数据进行查询、插入、修改和删除等操作。以查询为例,查询一个Content Provider需要掌握如下的信息。

● 惟一标识Content Provider的URI。

● 需要访问的数据字段名称。

● 该数据字段的数据类型。

提示

如果需要访问特定的某条数据记录,只需该记录的ID即可。

查询 Content Provider 的方法有两个:ContentResolver 的 query()和 Activity 对象的managedQuery(),二者接收的参数相同,返回的都是Cursor对象,惟一的不同是使用managedQuery方法可以让Activity来管理Cursor的生命周期。

被管理的Cursor会在Activity进入暂停态的时候调用自己的deactivate方法自行卸载,而在Activity 回到运行态时会调用自己的requery方法重新查询生成Cursor 对象。如果一个未被管理的Cursor对象想被Activity管理,可以调用Activity的startManagingCursor方法来实现。

下面通过一个例子来说明访问Content Provider的方式,本例中通过 ContentResolver 对象访问 Android 中存储了联系人信息的 Content Provider并将数据显示到TextView上,其开发步骤如下。

(1)首先在手机模拟器上运行“联系人(Contacts)”程序,在其中添加两个联系人“Tom”和“Jerry”,添加成功后如图4-5所示。

图4-5 联系人(Contacts)程序中存在的联系人信息

(2)在应用程序的main.xml文件中声明TextView控件,代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_4/res/layout目录下的main.xml。

1 <?xml version="1.0" encoding="utf-8"?>

2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

3  android:orientation="vertical"

4  android:layout_width="fill_parent" android:layout_height="fill_parent"

5  >      <!-- 声明一个LinearLayout线性布局 -->

6  <TextView android:id="@+id/tv"

7   android:layout_width="fill_parent" android:layout_height="wrap_content"

8  />     <!-- 声明一个TextView控件,id为tv -->

9 </LinearLayout>

说明

代码第6~8行声明的TextView控件将会在程序中负责显示从ContentProvider中查询到的信息。

(3)开发 Activity 的代码。本程序中 Activity 的主要功能是得到持有联系人信息的 Content Provider中的数据,其代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_4/src/wyf/wpf目录下的Sample_4_4.java。

1 package wyf.wpf;          //声明包语句

2 import android.app.Activity;       //引入相关类

3 import android.content.ContentResolver;     //引入相关类

4 import android.database.Cursor;       //引入相关类

5 import android.net.Uri;         //引入相关类

6 import android.os.Bundle;        //引入相关类

7 import android.provider.Contacts.People;    //引入相关类

8 import android.widget.TextView;       //引入相关类

9 //继承自Activity的子类

10 public class Sample_4_4 extends Activity {

11  String [] columns = {     //查询Content Provider时希望返回的列

12   People._ID,

13   People.NAME,

14  };

15  Uri contactUri = People.CONTENT_URI; //访问Content Provider需要的Uri

16  TextView tv;       //TextView对象引用

17  @Override

18  public void onCreate(Bundle savedInstanceState) { //重写onCreate方法

19   super.onCreate(savedInstanceState);

20    setContentView(R.layout.main);

21    tv = (TextView)findViewById(R.id.tv); //获得TextView对象引用

22    String result = getQueryData();  //调用方法访问Content Provider

23    tv.setText("ID\t名字\n"+result);  //将查询到的信息显示到TextView中

24  }

25  //方法:获取联系人列表信息,返回String对象

26  public String getQueryData(){

27  String result = "";

28  ContentResolver resolver = getContentResolver();//获取ContentResolver对象

29  Cursor cursor = resolver.query(contactUri, columns, null, null, null);//调用方法查询Content Provider

30    int idIndex = cursor.getColumnIndex(People._ID);//获得_ID字段的列索引

31    int nameIndex = cursor.getColumnIndex(People.NAME);//获得NAME字段的列索引

32    for(cursor.moveToFirst();(!cursor.isAfterLast());cursor.moveToNext()){//遍历Cursor, 提取数据

33     result = result + cursor.getString(idIndex)+ "\t";

34     result = result + cursor.getString(nameIndex)+ "\t\n";

35    }

36    cursor.close();      //关闭Cursor对象

37    return result;

38  }

39 }

● 代码第11行定义了一个String数组,该数组中包含了查询Content Provider时希望返回的列,其将会作为ContentResolver的query方法的一个参数传入。

● 代码第 15行获取了存储联系人信息的 Content Provider的数据表的 URI。URI对于 Content Provider很重要,ContentResolver中几乎所有方法的第一个参数都是一个URI对象,URI对象告诉了ContentResolver应该和哪个Content Provider交互、应该与其中的哪个数据表关联。

● 代码第22行调用了getQueryData方法,该方法在代码的第25~38行定义,在getQueryData方法中首先获得ContentResolver对象,然后调用其query方法查询给定URI的数据表,最后遍历返回的Cursor对象,将内容提取并存储到一个字符串中并返回。

(4)最后还需要在AndroidManifest.xml中为应用程序声明访问联系人信息的权限,否则在运行时会抛出异常,声明权限的代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_4目录下的AndroidManifest.xml。

1 …… //此处省略不相关代码,读者可以自行查阅随书光盘

2 <uses-permission android:name="android.permission.READ_CONTACTS" />

3 …… //此处省略不相关代码,读者可以自行查阅随书光盘

程序运行后如图4-6所示。

图4-6 程序运行后效果图

由上面的程序不难看出,访问一个Content Provider并不复杂,只需要牢牢掌握3个要素:URI、数据字段、数据类型即可。

提示

对于向 Content Provider中添加新的数据记录以及修改、删除数据记录,其实现方式同查询类似,只不过需要用到ContentValues对象来存放数据表的“列名-列值”的映射,在此不再赘述。

如果需要创建一个Content Provider,则需要进行的工作主要分为以下 3个步骤。

(1)建立数据的存储系统。

数据的存储系统可以由开发人员任意决定,一般来讲,大多数的Content Provider都通过Android的文件存储系统或SQLite数据库建立自己的数据存储系统。

(2)扩展ContentProvider类。

开发一个继承自ContentProvider类的子类代码来扩展ContentProvider类,这个步骤主要的工作是将要共享的数据包装并以ContentResolver和Cursor对象能够访问到的形式对外展示。具体来说需要实现ContentProvider类中的6个抽象方法。

● Cursor query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder):将查询的数据以Cursor对象的形式返回。

● Uri insert(Uri uri,ContentValues values):向Content Provider中插入新数据记录,ContentValues为数据记录的列名和列值的映射。

● int update(Uri uri,ContentValues values,String selection,String[] selectionArgs):更新Content Provider中已存在的数据记录。

● int delete(Uri uri,String selection,String[] selectionArgs):从Content Provider中删除数据记录。

● String getType(Uri uri):返回Content Provider中数据的(MIME)类型。

● boolean onCreate( ):当Content Provider启动时被调用。

以上方法将会在 ContentResolver 对象中被调用,所以很好地实现这些抽象方法会为ContentResolver提供一个完善的外部接口。除了实现抽象方法外,还可以做一些提高可用性的工作。

● 定义一个URI类型的静态常量,命名为CONTENT_URI。必须为该常量对象定义一个惟一的 URI 字符串,一般的做法是将 ContentProvider 子类的全程类名作为 URI 字符串,如“content://wyf.wpf.MyProvider”。

● 定义每个字段的列名,如果采用的数据存储系统为SQLite数据库,数据表列名可以采用数据库中表的列名。不管数据表中有没有其他的惟一标识一条记录的字段,都应该定义一个“_id”字段来惟一标识一条记录。一般将这些列名字符串定义为静态常量,如“_id”字段名定义为一个名为“_ID”值为“_id”的静态字符串对象。

(3)在应用程序的AndroidManifest.xml文件中声明Content Provider组件。

创建好一个 Content Provider必须要在应用程序的 AndroidManifest.xml中进行声明,否则该Content Provider对于Android系统将是不可见的。声明一个Content Provider组件的方法在第3章已经有所介绍,如果有一个名为MyProvider的类扩展了ContentProvider类,声明该组件的代码如下。

1 <provider name="wyf.wpf.MyProvider"

2   authorities="wyf.wpf.myprovider"

3   . . . />   <!-- 为<provider>标记添加name、authorities属性 -->

4 </provider>

其中 name 属性为 ContentProvider 子类的全称类名, authorities 属性惟一标识了一个ContentProvider。除了这些,还有设置读写权限等内容,在此将不赘述,读者可以查阅第 3 章的相关知识或其他书籍。

提示

创建一个Content Provider所要进行的工作比较复杂,涉及的代码量较多,且不属于本书的研究范畴,由于篇幅有限,便不对Content Provider的创建进行举例。

4.1.6 简单的数据存储——Preferences的使用

Preferences是一种应用程序内部轻量级的数据存储方案。Preferences主要用于存储和查询简单数据类型的数据,这些简单数据类型包括boolean、int、float、long以及String等,存储方式为以键值对的形式存放在应用程序的私有文件夹下。

Preferences 一般用来存储应用程序的设置信息,如应用程序的色彩方案、文字字体等。在应用程序中获取Preferences的方式有如下两种。

● 调用 Context 对象的 getSharedPreferences 方法获得 SharedPreferences 对象。需要传入SharedPreferences的名称和打开模式,名称为Preferences文件的名称,如果不存在则创建一个以传入名称为名的新的Preferences文件;打开模式为PRIVATE、MODE_WORLD_ READABLE和MODE_WORLD_WRITEABLE其中之一。

● 调用Activity对象的getPreferences方法获得SharedPreferences对象。需要传入打开模式,打开模式为PRIVATE、MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE其中之一。

通过两种不同途径获得的SharedPreferences对象并不是完全相同的,区别如下所列。

● 通过Context对象的getSharedPreferences方法获得的SharedPreferences对象可以被同一应用程序中的其他组件共享。

● 使用 Activity 对象的 getPreferences 方法获得的 SharedPreferences 对象只能在相应的Activity中使用。

SharedPreferences 对象中提供了一系列的 get 方法用于接收键返回对应的值。如果需要对Preferences文件中存储的键值对进行修改,首先需要调用SharedPreferences的edit方法获得一个Editor对象,该对象可以用来修改Preferences文件中存储的内容。下面将通过一个小例子来说明Preferences的用法,整个例子的开发分为如下几个步骤。

(1)本例的Activity中有一个EditText控件,需要在布局文件main.xml中声明它,实现代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_5/res/layout目录下的main.xml。

1 <?xml version="1.0" encoding="utf-8"?>

2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

3  android:orientation="vertical"

4  android:layout_width="fill_parent" android:layout_height="fill_parent"

5  >        <!-- 创建一个线性布局LinearLayout -->

6  <EditText android:id="@+id/et"

7  android:layout_width="fill_parent" android:layout_height="wrap_content"

8  />       <!-- 创建一个EditText控件,id为“et” -->

9 </LinearLayout>

说明

上述代码在线性布局中声明了一个EditText控件,并为EditText控件指定id,以便在Activity的代码中可以通过id找到该EditText控件。

(2)每次启动Activity时,会从应用程序的Preferences文件中读取数据并显示出来;每次退出Activity时,将EditText控件当前的内容存储到Preferences文件中,程序的Activity代码如下。

代码位置:见随书光盘中源代码/第4章/Sample_4_5/src/wyf/wpf目录下的Sample_4_5.java。

1 package wyf.wpf;        //声明包语句

2 import android.app.Activity;     //引入相关类

3 import android.content.SharedPreferences;  //引入相关类

4 import android.os.Bundle;      //引入相关类

5 import android.widget.EditText;     //引入相关类

6 //继承自Activity的子类

7 public class Sample_4_5 extends Activity {

8  EditText etPre;       //EditText对象的引用

9   SharedPreferences sp;      //SharedPreference对象的引用

10  public final String EDIT_TEXT_KEY = "EDIT_TEXT";//定义Preferences文件中的键

11  @Override

12  public void onCreate(Bundle savedInstanceState) { //重写onCreate方法

13   super.onCreate(savedInstanceState);

14   setContentView(R.layout.main);

15   etPre = (EditText)findViewById(R.id.et); //获得屏幕中EditText对象引用

16   sp = getPreferences(MODE_PRIVATE);  //获得SharedPreferences对象

17   String result = sp.getString(EDIT_TEXT_KEY, null);

18   if(result != null){    //判断获取的值是否为空

19   etPre.setText(result);   //EditText对象显示的内容设置为读取的数据

20   }

21  }

22  @Override

23  protected void onDestroy() {   //重写onDestroy方法

24   SharedPreferences.Editor editor = sp.edit();//获得SharedPreferences的Editor对象

25   editor.putString(EDIT_TEXT_KEY, String.valueOf(etPre.getText()));//修改数据

26   editor.commit();     //必须调用该方法以提交修改

27   super.onDestroy();

28  }

29 }

● 代码第9行声明了SharedPreferences对象的引用,第10行定义了String类型的常量,用来表示Preferences中的一个键。

● 代码第16行调用Activity的getPreferences方法获得SharedPreferences对象,这种方式获得的SharedPreferences对象只能被Activity使用。

● 代码第17行调用SharedPreferences对象的getString方法读取以EDIT_TEXT_KEY为键的值,第二个参数null表示如果该键值对不存在则返回null。

● 代码第23~28行重写了Activity的onDestroy方法,在该方法中首先获得Editor对象,然后使用Editor对象修改Preferences中的数据。数据修改完毕后一定要调用Editor对象的commit方法类提交修改。

启动应用程序后,在EditText控件中输入一些内容,然后关闭程序,再次启动程序,EditText控件中显示的内容为上次退出时的内容。两次启动程序时屏幕的显示内容如图4-7和图4-8所示。

图4-7 第一次启动程序时屏幕的显示

图4-8 第二次启动程序时屏幕的显示

说明

其实对于每个SharedPreferences对象,Android都在应用程序的私有文件夹下建立了一个以SharedPreferences对象名称命名的XML文件(对于通过Activity的getPreferences创建的Preferences文件,其文件名为Activity的名称),键值对都存放在相应的标记中。

4.2 Android平台下传感器应用的开发

Android 平台之所以吸引人,最重要的原因之一便是传感器的应用能带给人们奇妙的体验。开发者能够利用传感器探测到的外界变化的物理量开发出各种更人性化的软件。Android 平台支持很多种传感器,本节将一一介绍光传感器、温度传感器、接近传感器、加速度传感器、磁场传感器,以及姿态传感器等。

4.2.1 基本开发步骤

尽管不同传感器的开发过程不尽相同,但其整体的框架和思路还是有章可循的。笔者先在这里介绍完各种传感器开发过程中相同的部分后,再介绍Android平台下传感器的开发细节。

1.获取SensorManager对象

开发传感器的应用第一步是获取SensorManager对象,其具体代码如下所列。

1 SensorManager mysm= (SensorManager)getSystemService(SENSOR_SERVICE);

说明

从上述代码可以看出,只要调用API中提供的Context对象(包括Activity对象、Service对象)下的getSystemService方法就可以获取SensorManager对象,可见其使用方法很简单。调用时传入的参数SENSOR_SERVICE是Context类下的常量。

2.获取Sensor对象

获取SensorManager对象后就可以调用其getDefaultSensor方法获取某种具体类型的传感器对象,调用getDefaultSensor方法时需要传入一个描述指定传感器类型的常量,常用的常量如表4-7所列。

表4-7 常用描述传感器类型的常量

获取传感器对象以及各个方面的描述信息的基本代码如下所列。

1   Sensor myS=mySm.getDefaultSensor(Sensor.TYPE_LIGHT);

2   StringBuffer str=new StringBuffer();    //创建StringBuffer对象

3   str.append("\n名称:");

4   str.append(myS.getName());      //获取名称

5   str.append("\n耗电量(mA):");

6   str.append(myS.getPower());      //获取耗电量

7   str.append("\n类型编号:");

8   str.append(myS.getType());      //获取编号

9   str.append("\n版本:");

10   str.append(myS.getVersion());      //获取版本

11   str.append("\n最大测量范围:");

12   str.append(myS.getMaximumRange());    //获取最大测量范围

说明

该代码是以光传感器为例开发的,读者可以参照此例获取其他类型的传感器各个方面的描述性信息。

3.实现SensorEventListener接口

完成传感器对象的获取后,就要为传感器注册监听器了。注册监听器后,当监听的传感器所测量的物理量发生变化时,系统就会自动回调监听器中的特定方法。因此,在介绍注册监听器之前,介绍的是实现SensorEventListener接口的监听器的开发,主要是该接口中的两个方法。

● onAccuracyChanged方法

此方法的签名为“public void onAccuracyChanged(Sensor sensor, int accuracy)”,该方法在传感器精度发生变化时被回调,第一个参数为Sensor对象,第二个参数为当前的精度。从第二个参数类型可以看出其类型是整型。实际开发中精度有4种可能的取值,都是SensorManager类的下属常量,如表4-8所列。

表4-8 SensorManager下属的常量

● onSensorChanged方法

此方法的签名为“public void onSensorChanged (SensorEvent event)”,该方法在传感器所测量的物理量发生变化时被回调,参数为传感器对象的引用。此方法可以获取当前传感器测量值的数组value。

根据传感器类型不同,value数组的长度也不同。光传感器对应的value数组长度为1,如下面代码所示。

1 private SensorEventListener mySel=new SensorEventListener(){ //传感器监听器

2  @Override

3   public void onAccuracyChanged(Sensor sensor, int accuracy) {}//省略精度发生变化的代码

4  @Override

5   public void onSensorChanged(SensorEvent event) {

6    float[] value=event.values;     //获取value数组

7    tvl.setText("光照强度为:"+value[0]); } }  //数组长度为1,获取光照值

说明

以上代码的功能就是获得传感器的value值,并放在TextView中显示出来。

4.注册与注销监听器

开发完监听器后就可以注册监听器了,一般在Activity中的onResume方法中实现,具体代码如下。

1 protected void onResume(){

2  super.onResume();

3  mySm.registerListener (         //注册监听器

4    mySel,            //监听器引用

5    myS,            //传感器的引用

6    SensorManager.SENSOR_DELAY_NORMAL);}    //传感器的采样频率

提示

传感器采样频率都是用SensorManager类下的常量来表示的,共4种,具体信息见表4-9表所列。

表4-9 SensorManager下属的常量

传感器是很耗电的设备,因此,当不使用时要及时注销监听器来减少耗电量。注销监听器通过Activity中的onPause方法实现,具体代码如下。

1 @Override

2 protected void onPause() {      //重写onPause()方法

3  super.onPause();

4  mySm.unregisterListener(mySel); }   //注销传感器监听器

提示

以上给出的示例代码均来自光传感器案例Sample4_6。此外,本节给出的传感器应用案例需要在真机上有对应的硬件才能运行,在没有相应传感器硬件的机器上,案例不能正常运行。

4.2.2 光传感器

本小节将介绍光传感器,主要内容包括光传感器的一些基本知识,以及一个简单的应用案例Sample4_6。

1.基本知识

光传感器主要是探测手机所处环境的光照强度,其返回值是一个长度为 1 的数组(value),单位为勒克斯。灵活地使用光传感器能开发出更人性化的程序。

设想如下场景,为了适应不同光照强度,应用程序的显示模式分为两种:白天和黑夜。当应用程序不够人性化时可能需要用户手动设定应用程序的工作模式。若采用光传感器,则可以根据当前光照情况自动切换工作模式,大大提高程序对用户的吸引力。

2.简单的案例

通过基本知识的介绍,读者对光传感器已经有了基本的了解。下面将通过一个简单的案例Sample4_6,使读者进一步掌握光传感器的相关开发,其运行效果如图4-9所示。

图4-9 Sample4_6 运行效果图

了解了本小节案例的运行效果后,下面将进一步介绍此案例的相关代码。由于本案例涉及的传感器功能简单,仅有一个返回值,因此案例的核心代码很短,具体内容如下。

代码位置:见本书随书光盘中的源代码/第4章/Sample4_6/src/com/bn/ Sample4_6Activity.java。

1 package com.bn.sample4_6;       //声明包

2 ……//该处省略部分类的导入代码

3 public class Sample4_6Activity extends Activity {

4  SensorManager mySm;        //声明 SensorManager对象引用

5  Sensor myS;          //声明Sensor对象引用

6  TextView tv1;         //声明 TextView对象 tv1

7  TextView tv2;         //声明 TextView对象 tv2

8  @Override          //重写onCreate方法

9 public void onCreate(Bundle savedInstanceState) {

10   super.onCreate(savedInstanceState);

11   setContentView(R.layout.main);     //设置布局文件

12    //获取SensorManager对象

13   mySm=(SensorManager)this.getSystemService(SENSOR_SERVICE);

14   myS=mySm.getDefaultSensor(Sensor.TYPE_LIGHT);  //获取Sensor对象

15   tv1=(TextView)this.findViewById(R.id.textView1); //获取tv1对象引用

16   tv2=(TextView)this.findViewById(R.id.textView2); //获取tv2对象引用

17   StringBuffer str=new StringBuffer(); //声明并初始化StringBuffer对象str

18   str.append("\n名称:");

19   str.append(myS.getName());      //获取名称

20   str.append("\n类型编号:");

21   str.append(myS.getType());      //获取类型编号

22   str.append("\n耗电量(mA):");

23   str.append(myS.getPower());      //获取耗电量

24   str.append("\n测量最大范围:");

25   str.append(myS.getMaximumRange());    //获取测量最大范围

26   str.append("\n版本:");

27   str.append(myS.getVersion());      //获取版本

28   tv2.setText(str);         //设置tv2显示的文字

29   tv2.setTextSize(25); }       //设置字的大小

30    //实现SensorEventListener接口

31  private SensorEventListener mySel=new SensorEventListener(){

32  @Override         //重写onAccuracyChanged方法

33  public void onAccuracyChanged(Sensor sensor, int accuracy) {}

34  @Override         //重写onSensorChanged方法

35  public void onSensorChanged(SensorEvent event) {

36   float[] value=event.values;      //获取value数组

37   tv1.setText("\n光照强度是:"+value[0]);   //设置tv1显示的文字

38   tv1.setTextSize(25);} };       //设置字的大小

39  @Override

40  protected void onResume(){

41   super.onResume();

42   mySm.registerListener(       //注册监听器

43   mySel,

44   myS,

45   SensorManager.SENSOR_DELAY_NORMAL );}

46  @Override

47  protected void onPause() {

48  super.onPause();

49  mySm.unregisterListener(mySel);}}     //注销监听器

● 第4~7行是声明开发过程中要用到的对象的引用,第10~11行设置全屏、设置布局文件。

● 第13~16行是获得开发过程中用到的对象的引用,第17~27行是创建StringBuffer对象str,并把获得各种传感器信息添加到str中。

● 第 28~29 行设置 TextView ,第 31~38 行实现 SensorEventListener 监听器并重写onAccuracyChanged和onSensorChanged方法。

● 第42~45行注册监听器,第49行注销监听器。

至此,该案例的开发完成,读者可以自己运行体会效果。

4.2.3 温度传感器

介绍完光传感器后,下面将介绍温度传感器。主要内容包括温度传感器的一些基本知识,以及一个简单的应用案例Sample4_7。

1.基本知识

温度传感器主要用于探测手机所处环境的温度,其返回值是一个长度为1的数组(value),单位为摄氏度。应用温度传感器可以开发出更人性化、实用化的程序,读者可以充分发挥想象力。

2.应用案例

介绍完基本知识后,读者对温度传感器已经有了基本的了解。下面通过一个应用案例Sample4-7的介绍,使读者进一步掌握温度传感器的相关开发,其运行效果如图4-10所示。

图4-10 Sample4_7运行效果

说明

从图4-10中3幅图可以看出,环境不同,获得相应的返回值也不同。

了解完本小节案例的运行效果后,下面将介绍具体的开发步骤。由于本小节案例的基本套路与前面小节案例的基本一致,因此,这里仅着重讲解有区别的两处,具体步骤如下。

(1)首先给出的是案例中Sample4_7Activity.java类中的onCreate方法,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/Sample4_7/src/com/bn/ Sample4_7Activity.java。

1 public void onCreate(Bundle savedInstanceState) {

2  super.onCreate(savedInstanceState);

3   setContentView(R.layout.main);       //设置布局文件

4  //获取SensorManager对象

5  mySm=(SensorManager)this.getSystemService(SENSOR_SERVICE);

6  myS=mySm.getDefaultSensor(Sensor. TYPE_TEMPERATURE); //获取Sensor对象

7  tv1=(TextView)this.findViewById(R.id.textView1);  //获取tv1对象引用

8  tv2=(TextView)this.findViewById(R.id.textView2);  //获取tv2对象引用

9   StringBuffer str=new StringBuffer(); //声明并初始化StringBuffer对象str

说明

相比前面的案例,主要区别就是调用getDefaultSensor方法时获取传感器对象传入的参数改成Sensor. TYPE_TEMPERATURE。

(2)接下来开发实现SensorEventListener接口的监听器,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_7/src/com/bn/ Sample4_7Activity.java。

1  //实现SensorEventListener接口

2  private SensorEventListener mySel=new SensorEventListener() {

3   @Override         //重写onAccuracyChanged方法

4  public void onAccuracyChanged(Sensor sensor, int accuracy) {}

5   @Override         //重写onSensorChanged方法

6  public void onSensorChanged(SensorEvent event) {

7   float[] value=event.values;      //获取value数组

8   tv1.setText("\n温度为:"+value[0]);    //设置tv1显示的文字

9   tv1.setTextSize(22);} };       //设置字的大小

说明

从代码 7-8 行可以看出温度传感器返回值只有一个(以摄氏度衡量),与前面光传感器相同。

4.2.4 接近传感器

本小节将介绍的是接近传感器,主要内容包括接近传感器的一些基本知识和一个简单的应用案例Sample4_8。

1.基本知识

接近传感器用于探测的是,是否有物体接近手机屏幕非常近的范围内(1厘米左右的范围),其返回值是一个长度为 1 的数组(value),表示物体是否在近距离范围内。返回值随着手机型号的不同会有所差异,但总的来说只有两种可能,一种表示在手机屏幕近的范围内,另一种表示不在近的范围内。

如笔者使用的三星I9308用的是8.0,表示不在近的范围内;用0表示在近的范围内。而有的手机型号是用9.0表示不在近的范围内,用0表示在近的范围内。

巧妙地应用接近传感器可以开发出更人性化的应用程序。手机上的通话程序就是如此,当拿着手机贴近耳朵时,接近传感器探测到在近的范围内,则手机关闭屏幕以节约电量;当结束通话手机离开耳朵时,接近传感器探测到不在近的范围内,手机打开屏幕,这样更加人性化。

提示

接近传感器不是距离传感器,有些资料解释为距离传感器是不正确的。因为接近传感器不能探测连续的距离值,只是能探测到在于不在近范围内。另外接近传感器位于屏幕左上方。

2.应用案例

通过前面的介绍,读者对接近传感器已经有了基本的了解,下面通过一个简单的案例Sample4_8的介绍,使读者进一步掌握接近传感器的相关开发,其运行效果如图4-11所示。

说明

从图 4-11 中可以看出物体离手机距离不同,接近传感器的返回值也不同,同一手机只可能有两个值。不同型号的手机返回值也不相同,前两幅为htc纵横手机的运行效果,后两幅为三星I9308的运行效果。

图4-11 Sample4_8运行效果

了解完本小节案例的运行效果后,下面将介绍具体的开发步骤。由于本小节案例的基本套路与前面两个小节案例的基本一致,因此,这里仅着重讲解有区别的两处,具体步骤如下。

(1)首先给出Sample4_8Activity中的onCreate方法,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_8/src/com/bn/ Sample4_8Activity.java。

1 public void onCreate(Bundle savedInstanceState) {

2 super.onCreate(savedInstanceState);

3 setContentView(R.layout.main); //设置布局文件

4 //获取SensorManager对象

5 mySm=(SensorManager)this.getSystemService(SENSOR_SERVICE);

6 myS=mySm.getDefaultSensor(Sensor. TYPE_PROXIMITY); //获取Sensor对象

7 tv1=(TextView)this.findViewById(R.id.textView1); //获取tv1对象引用

8 tv2=(TextView)this.findViewById(R.id.textView2); //获取tv2对象引用

9 //声明并初始化StringBuffer对象str

10 StringBuffer str=new StringBuffer();

说明

相比前面的案例,主要区别就是调用getDefaultSensor方法时获取传感器对象传入的参数改成Sensor. TYPE_PROXIMITY。

(2)接下来开发实现SensorEventListener接口的监听器,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_8/src/com/bn/ Sample4_8Activity.java。

1  //实现SensorEventListener接口

2  private SensorEventListener mySel=new SensorEventListener() {

3   @Override         //重写onAccuracyChanged方法

4  public void onAccuracyChanged(Sensor sensor, int accuracy) {}

5   @Override         //重写onSensorChanged方法

6  public void onSensorChanged(SensorEvent event) {

7   float[] value=event.values;    //获取value数组

8   tv1.setText("\n距离为:"+value[0]);  //设置tv1显示的文字

9   tv1.setTextSize(22);} };     //设置字的大小

说明

从代码第7-8行可以看出,接近传感器返回值只有一个,与前面光传感器、温度传感器相同。另外这里的返回值只有两种可能的离散值,一个表示接近,另一个表示不接近。

4.2.5 磁场传感器

本小节将介绍磁场传感器的开发,主要内容包括磁场传感器的一些基本知识,以及一个简单的应用案例Smaple4_9。

1.基本知识

磁场传感器使用的坐标系中 x 轴平行于屏幕短边,从左到右;y 轴平行于屏幕长边,从上到下;z轴垂直于屏幕与x轴、y轴正交。3个坐标轴是绑定在手机上的,即坐标轴不会随着手机姿态的改变而改变,具体情况如图4-12所示。

图4-12 手机空间坐标系

磁场传感器主要是用于探测手机周围的磁场强度,返回的是一个长度为3的数组,分别代表磁场强度在x轴、y轴、z轴上的分量。返回数据的单位是uT,即特斯拉。具体情况如表4-10所列。

表4-10 磁场传感器中value值的意义

2.一个简单的案例

通过前面的介绍,读者对磁场传感器已经有了基本的了解,下面通过一个案例的介绍,使读者进一步掌握磁场传感器的相关开发,其运行效果如图4-13所示。

图4-13 Sample4_9运行效果

了解完本小节案例的运行效果后,下面将介绍具体的开发步骤。由于本小节案例的基本套路与前3个小节案例的基本一致,因此,这里仅着重讲解有区别的两处,具体步骤如下。

(1)首先给出Sample4_9Activity中的onCreate方法,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_9/src/com/bn/ Sample4_9Activity.java。

1 public void onCreate(Bundle savedInstanceState) {

2  super.onCreate(savedInstanceState);

3   setContentView(R.layout.main);       //设置布局文件

4  //获取SensorManager对象

5  mySm=(SensorManager)this.getSystemService(SENSOR_SERVICE);

6  myS=mySm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); //获取Sensor对象

7  tvX=(TextView)findViewById(R.id.textView1);

8  tvY=(TextView)findViewById(R.id.textView2);    //获取Textview对象

9  tvZ=(TextView)findViewById(R.id.textView3);

10  tv=(TextView)findViewById(R.id.textView4);

说明

相比前面的案例,主要区别就是调用getDefaultSensor方法时获取传感器对象传入的参数改成Sensor.TYPE_MAGNETIC_FIELD。

(2)接下来开发实现SensorEventListener接口的监听器,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_9/src/com/bn/ Sample4_9Activity.java。

1  //实现SensorEventListener接口

2  private SensorEventListener mySel=new SensorEventListener() {

3   @Override          //重写onAccuracyChanged方法

4  public void onAccuracyChanged(Sensor sensor, int accuracy) {}

5   @Override          //重写onSensorChanged方法

6  public void onSensorChanged(SensorEvent event) {

7    float []values=event.values;

8   tvX.setText("X轴方向上的磁场强度为:"+values[0]); //设置Textview显示的文字

9   tvY.setText("Y轴方向上的磁场强度为:"+values[1]);

10  tvZ.setText("Z轴方向上的磁场强度为:"+values[2]);

说明

从第7-10行可以看出磁场传感器的返回值有3个。

4.2.6 加速度传感器

本小节将介绍的是加速度传感器,主要内容包括加速度传感器的一些基本知识,以及一个简单的应用案例Sample4_10。

1.基本知识

加速度传感器是用来感应手机加速度的,其返回值与磁场传感器相同,是一个长度为3的数组(value),分别代表加速度在x轴、y轴、z轴上的分量。返回数据的单位是m/s2(米/平方秒),具体情况如表4-11所列。

此外,加速度传感器使用的坐标系与磁场传感器坐标系相同,即x轴平行于屏幕短边,从左到右;y轴平行于屏幕长边,从上到下;z轴垂直于屏幕与x轴、y轴正交。3个坐标轴同样也是绑定在手机上,即坐标轴不会随着手机姿态的改变而改变。

表4-11 加速度传感器中value值的意义

了解手机的空间坐标系后,下面具体说明各个坐标轴上加速度值的计算方法,如图4-14所示。

图4-14 几种情况下的加速度值的计算

从图4-14中读者已经了解了手机不同的运动情况,接下来将详细介绍图4-14所示的不同运动状态下加速度的计算方法。

● 图4-14(a)所示的运动情况

当手机屏幕与重力加速度方向垂直时,以图4-14(a)中的姿态以加速度a向上运动时,z轴的加速度值为(a+g) m/s2。这是因为此时手机本身 z轴的加速度为 a,重力加速度方向沿 z轴负方向,也就是重力加速度在z轴上的分量为-g,而“a-(-g)”的结果为a+g。

● 图4-14(b)所示的运动情况

当手机的短边与重力加速度方向平行时,以图4-14(b)中的姿态以加速度a向上运动时,x轴的加速度值为(a+g) m/s2。这是因为此时手机本身在X轴的加速度为 a,重力加速度方向沿着 x轴负方向,也就是重力加速度在x轴上的分量为-g,而“a-(-g)”的结果为a+g。

● 图4-14(c)所示的运动情况

当手机屏幕与重力加速度平行时,以图4-14(c)中的姿态以加速度a向上运动时,x轴的加速度值为(a*sinα+g*sinα)m/s2,y 轴的加速度值为(a*cosα+g*cosα)m/s2。此情况下的加速度计算与前面两种情况类似,只需要将加速度a与重力加速度g沿x轴与y轴两个方向分解。

2.一个简单的案例

通过前面的介绍,读者对加速度传感器已经有了一个基本的了解,下面将通过一个简单的案例Sample4_10使读者进一步掌握加速度传感器的相关开发,其案例运行效果如图4-15所示。

说明

从图 4-15 中可以看出,本案例是一个水平仪,水平放置和竖直放置的管子里面的小球分别显示磁场传感器测量值在x方向和y方向的分量,中间的圆形区域中的球显示的是x轴、y轴叠加后的情况。

图4-15 Sample4_10运行效果

了解了本小节案例的运行效果后,下面将介绍案例具体的开发步骤,具体步骤如下。

(1)首先开发的是案例Sample4_10Activity.java类,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_10/src/com/bn/ Sample4_10Activity.java。

1 package com.bn.sample4_10;

2 ……//该处省略了部分类的导入代码,读者可以自行查阅随书光盘中源代码

3 public class Sample4_10Activity extends Activity {

4  SensorManager mySensorManager;   //声明SensorManager对象引用

5  Sensor sensor;       //声明Sensor对象引用

6  Bitmap yuan;        //声明Bitmap对象,圆形区域

7  Bitmap shang;        //声明Bitmap对象,上面的管子

8  Bitmap zuo;        //声明Bitmap对象,左面的管子

9  Bitmap xian;        //声明Bitmap对象,压在球上的黑线

10  Bitmap qiuzuo;       //声明Bitmap对象,上面的球

11  Bitmap qiushang;       //声明Bitmap对象,上面的球

12  Bitmap qiuzhong;       //声明Bitmap对象,中间的球

13  String str1;        //声明String对象

14  String str2;        //声明String对象

15  String str3;        //声明String对象

16  String str4;        //声明String对象

17  String str5;        //声明String对象

18  MyView mv;        //声明MyView对象引用

19 private SensorEventListener mel=new SensorEventListener(){//实现SensorEventListener接口

20 @Override

21 public void onAccuracyChanged(Sensor sensor, int accuracy) {}//重写onAccuracyChanged方法

22 @Override

23 public void onSensorChanged(SensorEvent event){ //重写onSensorChanged方法

24  float []values=event.values;    //获得加速度传感器的测量值

25  mv.dx=values[0];       //为dx赋值

26  mv.dy=values[1];       //为dy赋值

27  mv.dz=values[2];}}      //为dz赋值

28 @Override

29 public void onCreate(Bundle savedInstanceState) {

30  super.onCreate(savedInstanceState);

31  requestWindowFeature(Window.FEATURE_NO_TITLE); //全屏

32  getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN ,

33  WindowManager.LayoutParams.FLAG_FULLSCREEN); //获得SensorManager对象

34  mySensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);

35  sensor=mySensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

36  str1="名称:"+sensor.getName();     //获取名称

37  str2="耗电量(mA):"+sensor.getPower();   //获取耗电量

38  str3="类型编号:"+sensor.getType();    //获取类型编号

39  str4="版本:"+sensor.getVersion();    //获取版本

40  str5="最大测量范围:"+sensor.getMaximumRange(); //获取测量最大范围

41  //获取图片圆形区域的引用

42  yuan = BitmapFactory.decodeResource(getResources(), R.drawable.yuan);

43  zuo = BitmapFactory.decodeResource(getResources(), R.drawable.zuo);

44  shang = BitmapFactory.decodeResource(getResources(), R.drawable.shang);

45  xian = BitmapFactory.decodeResource(getResources(), R.drawable.xian);

46  qiuzuo = BitmapFactory.decodeResource(getResources(), R.drawable.qiuzuo);

47  qiushang = BitmapFactory.decodeResource(getResources(), R.drawable.qiushang);

48  //获取图片中间球的引用

49  qiuzhong = BitmapFactory.decodeResource(getResources(), R.drawable.qiuzhong);

50  mv = new MyView(this);       //初始化MyView对象

51  this.setContentView(mv);}

52 @Override

53 protected void onResume() {       //重写onResume方法注册监听器

54  mySensorManager.registerListener(mel, sensor, SensorManager.SENSOR_DELAY_UI);

55  mv.mvdt.pauseFlag=false;

56  super.onResume();}

57 @Override

58 protected void onPause() {       //重写onPause方法

59  mySensorManager.unregisterListener(mel);  //注销监听器

60  mv.mvdt.pauseFlag=true;

61  super.onPause();}}

● 第4~18行是声明开发过程中要用到的对象的引用,第19~27行是实现了SensorEventListener接口的监听器,并重写了该接口的onAccuracyChanged方法和onSensorChanged方法。

● 第28~51行重写onCreate方法,并获取引用、初始化MyView对象。

● 第52~56行重写onResume方法,并注册监听器。第57~61行重写onPause方法,并注销监听器。

(2)完成Activity的开发后,下面将介绍绘图类MyView.java的开发,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_10/src/com/bn/ MyView.java。

1 package com.bn.sample4_10;

2 ……//此处省略一些类的导入

3 public class MyView extends SurfaceView implements SurfaceHolder.Callback{

4   Sample4_10Activity activity;     //声明Activity的引用

5   MyViewDrawThread mvdt;       //声明MyViewDrawThread引用

6  Paint paint;         //声明画笔

7   float dx;          //声明dx x轴上的分量

8   float dy;          //声明dy y轴上的分量

9   float dz;          //声明dz z轴上的分量

10  float x;          //声明x轴上的位移

11  float y;          //声明y轴上的位移

12  float rx;          //声明rx 圆形区域的坐标

13  float ry;          //声明ry 圆形区域的坐标

14  float juli2;         //声明juli2

15  float juli;         //声明juli

16 public MyView(Sample4_10Activity activity){  //构造器

17  super(activity);

18  this.activity = activity;

19  this.getHolder().addCallback(this);

20  paint = new Paint();       //创建画笔

21  paint.setColor(Color.WHITE);     //设置颜色

22  paint.setTextSize(30);       //设置字体颜色

23  paint.setAntiAlias(true);      //打开抗锯齿

24  mvdt=new MyViewDrawThread(this);}    //实例化MyViewDrawThread

25 @Override

26 public void draw(Canvas canvas) {     //重写draw方法

27  super.draw(canvas);

28  canvas.drawBitmap(activity.shang, 0, 0,paint); //画上面的管子

29  canvas.drawBitmap(activity.yuan, 0, 0,paint); //画中间圆形区域

30  canvas.drawBitmap(activity.zuo, 0, 0,paint); //画左面的管子

31  y=dy*34;          //对在y轴上的位移赋值

32  if(y>170)y=170;       //如果位移大于170,y值不再变化

33  if(y<-170)y=-170;       //如果位移小于-170,y值不再变化

34  canvas.drawBitmap(activity.qiuzuo, 0, -y,paint); //画左面的球

35  x=dx*34;         //对在x轴上的位移赋值

36  if(x>170)x=170;       //如果位移大于170,x值不再变化

37  if(x<-170)x=-170;       //如果位移小于-170,x值不再变化

38  canvas.drawBitmap(activity.qiushang, x,0,paint); //画上面的球

39  juli=(float) Math.sqrt((dx*34)*(dx*34)+(dy*34)*(dy*34));//求得坐标点到原点的距离

40  juli2=juli/170;         //单位化距离

41  if(juli2<=1){          //如果小于1,直接赋值

42  rx=(dx*34)/170;

43  ry=(dy*34)/170;}

44  else{          //如果大于1,求与单位圆的交点

45  if(dx>0)         //再赋值

46  {rx=(float) Math.sqrt(dx*dx/(dx*dx+dy*dy));}

47  else{rx=-(float) Math.sqrt(dx*dx/(dx*dx+dy*dy));}

48  ry=dy/dx*rx;}

49  canvas.drawBitmap(activity.qiuzhong, rx*110, -ry*110,paint); //画中间球

50  canvas.drawBitmap(activity.xian, 0, 0,paint);  //画压在球上的线条

51  canvas.drawText("x轴 : "+dx, 0 510, paint);  //画x轴分量

52  canvas.drawText("y轴 : "+dy,0, 540, paint);  //画y轴分量

53  canvas.drawText("z轴 : "+dz, 0, 570, paint);  //画z轴分量

54  canvas.drawText(activity.str1, 0, 600, paint);  //画名称信息

55  canvas.drawText(activity.str2, 0, 630, paint);  //画耗电量信息

56  canvas.drawText(activity.str3, 0, 660, paint);  //画类型编号信息

57  canvas.drawText(activity.str4, 0, 690, paint);  //画版本信息

58  canvas.drawText(activity.str5, 0, 720, paint);} //画最大测量范围信息

59 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {}

60 public void surfaceCreated(SurfaceHolder holder) {

61  mvdt.start();}          //启动线程

62 public void surfaceDestroyed(SurfaceHolder arg0) {}}

● 第4~6行是声明开发过程中要用到的对象的引用,第7~15行是声明要用到的数值。第16-24行是创建构造器,并初始化画笔。

● 第25~58行重写draw方法,绘制界面图,第31~34行绘制左面小球,第35-38行绘制右面小球。

● 第39~48行计算圆形区域的球的坐标,第49行绘制中间球,第50行绘制球上的黑线。第51-58行绘制加速度传感器信息。第61行启动线程。

(3)最后要开发的是绘制图形的线程MyViewDrawThread.java类,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_10/src/com/bn/ MyViewDrawThread.java。

1 package com.bn.sample4_10;

2 //定时重新绘制画面的线程

3 public class MyViewDrawThread extends Thread{

4   boolean flag = true;       //声明标志位

5   boolean pauseFlag=false;      //声明标志位

6  MyView mv;          //声明MyView引用

7   SurfaceHolder surfaceHolder;     //声明SurfaceHolder引用

8  public MyViewDrawThread(MyView mv){    //创建构造器

9   this.mv = mv;

10  this.surfaceHolder = mv.getHolder();}

11  public void run(){

12   Canvas c;         //声明画布的引用

13    while (this.flag) {

14    c = null;

15     if(!pauseFlag) {

16      try{

17      c = this.surfaceHolder.lockCanvas(null);

18      synchronized (this.surfaceHolder) {

19      mv.draw(c); }     //绘制

20      finally {

21      if (c != null) {    //并释放锁

22      this.surfaceHolder.unlockCanvasAndPost(c);} }} //睡眠指定毫秒数

23    try{Thread.sleep(50); }

24    catch(Exception e){e.printStackTrace();}}} //打印堆栈信息

● 第8~11行是MyViewDrawThread构造器,在其他Java类创建该类对象时调用。

● 第11~24行重写run方法,其实现的功能是根据标志位来实时绘制图形,并在绘制过程中休眠50毫秒。

4.2.7 姿态传感器

本小节将介绍姿态传感器,主要内容包括姿态传感器的一些基本知识,以及一个简单的应用案例Sample4_11。

1.基本知识

本小节所介绍的姿态传感器多用于游戏的开发,很多体感游戏,如都市赛车、极品飞车等都是采用姿态传感器进行操控的。玩家在操控游戏的过程中,根据操控的需要改变手机的姿态,姿态传感器获得姿态数据后传递给应用程序进行分析、计算,得出被操控物体的运动情况。

从上述介绍可以看出,姿态传感器主要用于感知手机姿态的变化,其每次读取的都是静态的状态值,表示当前的姿态。每组姿态值包括3个值,分别代表手机在Yaw、Pitch、Roll轴的旋转角度。Yaw、Pitch、Roll轴与手机的关系比较复杂,详细情况如图4-16所示。

图4-16 手机姿态传感器

说明

图4-16中从左到右分别为手机在原始姿态,手机绕Yaw轴顺时针旋转90°后的姿态,再绕 Roll 轴逆时针旋转 90°姿态。从几幅图的对比中可以看出,Yaw、Pitch、Roll 3个轴之间的关系不都是固定的。

Yaw、Pitch、Roll3个轴的详细情况如下所列。

● Yaw轴

无论手机处于何种姿态,此轴都是与重力加速度方向相反的,竖直向上。即此轴是固定不变的,因此姿态传感器工作时,获得此轴的角度表示的是手机方位。

具体情况为:0°时手机指向北方,90°时指向东方,180°指向南方,270°时指向西方,其他的方位都可以以此类推。因此,在原始状态时手机屏幕水平向上(即屏幕的法向量与重力加速度方向相反),指向北方。

● Pitch轴

Pitch轴的方向与当前手机的方位角度密切相关,原始情况下Pitch轴指向东方。当手机绕Yaw轴旋转一定角度后,Pitch轴也绕Yaw轴旋转相同的角度。如图4-16中间的小图所示,当手机绕Yaw轴顺时针旋转90°后,Pitch轴也绕Yaw轴旋转90°,指向南方。

简单来说,可以将Pitch轴理解为焊死在Yaw轴上的。另外需要注意的是,Pitch轴与手机之间的关系是不固定的。如图4-16中右小图所示,当手机又绕Roll轴旋转后,Pitch轴与手机之间的关系就发生了变化。

● Roll轴

从图4-16中可以看出,Roll轴的方向依赖于手机绕Yaw轴和Pitch轴旋转的情况,但是Roll轴的确定并不复杂。细心观察一下就可以看出Roll轴与手机之间的关系是固定的,就像焊死在手机上一样。

看完上面的介绍,读者可能会认为这3个轴很古怪,其实不然,这3个轴是来自数学上的欧拉角。欧拉角是一种表示物体姿态的方法,在飞机飞行中很常用,具体情况如图4-17所示。

图4-17 用欧拉角表示手机姿态

从图 4-17 中可以看出,Yaw 轴角度是飞机的方位角,Pitch轴角度为飞机的俯仰角,Roll轴角度为飞机左右的倾角。知道了这3个角度后,飞机的姿态就被唯一确定了。上述说法是从飞机的角度出发的,乘客也是如此,做过飞机的读者可以很容易理解。

早期姿态传感器是我们手机上一种非常重要的传感器,但是本质上,其实并不存在一个硬件传感器称为姿态传感器,姿态传感器并不是一个硬件,而是一个逻辑传感器,其返回值是由重力传感器3个轴的值和磁场传感器3个轴的值6轴联算得到的。

说明

Yaw轴在较新版本中已经更名为Azimuth轴了,Pitch轴和Roll轴名称不变,所代表的含义也没有变化。

随着硬件的发展,谷歌可能希望传感器就是代表实际存在的硬件,所以,姿态传感器在以后较新的 Android 版本中可能会被废弃(deprecated)。虽然姿态传感器会被废掉,但是这不代表姿态传感器的功能不能使用了。这是因为重力传感器和磁场传感器的硬件还是存在的,那6个轴值还是可以获得的。

这就要求我们想获取姿态传感器的返回值时必须使用重力传感器的3个值与磁场传感器的3个值,6轴联算计算出姿态传感器的返回值。

谷歌也考虑到了这方面的使用,因此,在 SensorManager 类中提供了很多工具方法,其中比较重要的两个为getRotationMatrix方法和getOrientation方法,具体情况如表4-12所列。

表4-12 SensorManager类提供的重要工具方法

说明

表4-12中所列的只是两种重要的方法,辅助计算的工具方法还有其他,这里只用到以上两种方法,其他的方法在这里不做详细说明,感兴趣的读者可自行查阅API。

2.一个简单的应用案例

通过前面的介绍,读者对姿态传感器已经有了基本的了解,下面通过介绍案例Sample4_11,使读者更进一步掌握姿态传感器的相关开发,其运行效果如图4-18所示。

图4-18 Sample4_11运行效果

说明

从图4-18所示的3幅图中,读者可以看出这是一个指南针的案例,红头指北,蓝头指南。表盘为一幅背景图,针头为一幅图,根据姿态传感器返回值计算后转动针头图。

了解运行效果之后,将介绍代码的具体开发步骤,此案例的绘制框架与Sample4_10水平仪很类似,主要的区别之处在于绘制逻辑,因此,在这里只给出Sample4_11Activity.java与Myview.java的代码,MyViewDrawThread.java与Sample4_10中的MyViewDrawThread.java相同,这里就不再重复介绍,具体步骤如下。

(1)首先介绍Sample4_11Activity.java类的开发,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/ Sample4_11/src/com/bn/ Sample4_11Activity.java。

1 package com.bn.sample4_11;

2 ……//此处省略一些类的导入

3 public class Sample4_11Activity extends Activity{

4   SensorManager mySensorManager;     //声明SensorManager对象引用

5   Sensor sensorAccelerometer;     //声明Sensor对象引用

6   Sensor sensorMagneticField;     //声明Sensor对象引用

7  Bitmap jiantou;        //声明Bitmap引用箭头

8  Bitmap beijing;        //声明Bitmap引用背景

9  MyView mv;          //声明MyView对象引用

10  Object lock=new Object();      //声明并初始化Object对象

11 private SensorEventListener mel=new SensorEventListener(){//磁场传感器的监听器

12  @Override

13  public void onAccuracyChanged(Sensor sensor, int accuracy) {}

14  @Override

15  public void onSensorChanged(SensorEvent event) {

16  synchronized(lock){

17  mv.mx=event.values[0];        //获取磁场传感器x值

18  mv.my=event.values[1];        //获取磁场传感器y值

19  mv.mz=event.values[2];}}};        //获取磁场传感器z值

20 private SensorEventListener mek=new SensorEventListener(){ //重力传感器的监听器

21  @Override

22  public void onAccuracyChanged(Sensor sensor, int accuracy) {}

23  @Override

24  public void onSensorChanged(SensorEvent event) {

25  synchronized(lock){

26  mv.ax=event.values[0];        //获取加速度传感器x值

27  mv.ay=event.values[1];        //获取加速度传感器y值

28  mv.az=event.values[2];}}};       //获取加速度传感器z值

29 @Override

30 public void onCreate(Bundle savedInstanceState) {

31  super.onCreate(savedInstanceState);

32  requestWindowFeature(Window.FEATURE_NO_TITLE);  //全屏

33  getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN ,

34  WindowManager.LayoutParams.FLAG_FULLSCREEN);

35  this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

36  //获取SensorManag引用

37  mySensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);

38  ensorAccelerometer=mySensorManager.getDefaultSensor(

39  Sensor.TYPE_MAGNETIC_FIELD);      //获取磁场传感器引用

40  sensorMagneticField=mySensorManager.getDefaultSensor(

41  Sensor.TYPE_ACCELEROMETER);      //获取加速度传感器引用

42  //获取箭头图引用

43  jiantou = BitmapFactory.decodeResource(getResources(), R.drawable.jiantou);

44  //获取背景图引用

45  beijing = BitmapFactory.decodeResource(getResources(), R.drawable.beijing);

46  mv = new MyView(this);         //获取MyView引用

47  this.setContentView(mv); }

48 @Override

49 protected void onResume() {

50  mySensorManager.registerListener

51  (mel, sensorMagneticField, SensorManager.SENSOR_DELAY_UI);//注册磁场传感器

52  mySensorManager.registerListener

53  (mek, sensorAccelerometer, SensorManager.SENSOR_DELAY_UI); //注册重力传感器

54  mv.mvdt.pauseFlag=false;

55  super.onResume();}

56 @Override

57 protected void onPause() {

58  mySensorManager.unregisterListener(mel);    //注销磁场传感器

59  mySensorManager.unregisterListener(mek);    //注销加速度传感器

60  mv.mvdt.pauseFlag=true;

61  super.onPause()}}

● 第4~10行声明一些开发过程中要用到的对象的引用,第11~19行实现磁场传感器的监听器并获得磁场传感器x、y、z轴的值,第20~28行实现加速度传感器的监听器并获得加速度传感器x、y、z轴的值。

● 第32~35行设置全屏,第37行获取SensorManager的引用,第38~39行获取磁场传感器引用,第40~41行获取加速度感器引用。第42~45行获取图的引用。

● 第50~53行注册监听器,第58~59行注销监听器。

(2)介绍完Activity的开发后,下面将介绍MyView.java的开发,具体代码如下。

代码位置:见本书随书光盘中的源代码/第4章/Sample4_11/src/com/bn/MyView.java。

1 package com.bn.sample4_11;

2 ……//此处省略了一些类的导入

3 public class MyView extends SurfaceView implements SurfaceHolder.Callback {

4  Sample4_11Activity activity;    //声明Activity的对象引用

5   MyViewDrawThread mvdt;     //声明MyViewDrawThread对象引用

6  Paint paint;        //声明画笔

7   float mx;         //磁场传感器的x值

8   float my;         //磁场传感器的y值

9   float mz;         //磁场传感器的z值

10  float ax;         //重力传感器的x值

11  float ay;         //重力传感器的y值

12  float az;         //重力传感器的z值

13  public MyView(Sample4_11Activity activity){//创建构造器

14  super(activity);

15  this.activity = activity;

16  this.getHolder().addCallback(this);  //设置生命周期回调接口的实现者

17  paint = new Paint();      //初始化画笔

18  paint.setAntiAlias(true);     //设置抗锯齿

19  mvdt=new MyViewDrawThread(this);}   //实例化MyViewDrawThread类

20 @Override

21 public void draw(Canvas canvas) {    //重写draw方法

22  super.draw(canvas);

23  canvas.drawBitmap(activity.beijing, 0,0, paint);//画背景图

24  Matrix m1=new Matrix();     //创建并初始化旋转Matrix

25  m1.setTranslate(0,0);      //在此处画箭头图

26  Matrix m2=new Matrix();     //创建并初始化Matrix

27  m2.setRotate(getDirection(), 240, 240); //绕此点以此角度旋转

28  Matrix mz=new Matrix();

29  canvas.drawBitmap(activity.jiantou, mz, paint);} //画箭头图

30  public float getDirection(){      //创建getDirection方法

31  float result=0;

32  synchronized(activity.lock){

33  float[] R=new float[9];       //声明旋转矩阵

34  SensorManager.getRotationMatrix(     //获取旋转矩阵的各项值

35    R,

36    null,

37    new float[]{ax,ay,az},

38    new float[]{mx,my,mz} );

39  float[] values=new float[3];      //姿态值数组

40  SensorManager.getOrientation(R, values);   //获取姿态值

41  result=(float) Math.toDegrees(values[0]);//将姿态值中方位角(Yaw或azimuth)转换为角度

42  result=(result+360)%360.0f;}      //设置result

43  return result;}         //返回result

44 public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {}

45 public void surfaceCreated(SurfaceHolder holder) {mvdt.start();}//启动线程

46 @Override

47 public void surfaceDestroyed(SurfaceHolder holder) {}}

● 第4~6行声明开发过程中要用到的引用,第7~12行声明一些计算中要用到的值,第13~19行创建构造器并并初始化画笔和MyViewDrawThread类。

● 第20~29行重写draw方法,其中第24~29行实现针头图的旋转,第30~43行计算得到姿态传感器返回值。第45行启动线程。

4.3 小结

本章主要介绍了在 Android 平台下如何进行数据的存储和共享。数据的存储包括文件存储、SQLite数据库存储等;数据的共享方式有在应用程序间共享数据的Content Provider,也有在应用程序内部的组件之间共享数据的 Preference,利用这些数据存储的处理方式使开发出的应用功能更完备。

本章还介绍了 Android 平台的一大亮点—传感器应用的开发,并介绍了如何使用 Sensor Simulator工具模拟传感器变化,这些内容将会在后面的案例中再次提到。