摘要: 详细介绍了如何利用Android 系统的传感器开发水平仪应用的全过程。通过对该案例开发的讲解, 介绍了传感器应用的开发方法以及通过Eclipse 开发Android 应用程序的过程。
1 Android 平台简介
互联网巨头Google 公司于2007 年11 月5 日推出了全新的嵌入式软件平台---Android, 该平台由操作系统、中间件、用户界面以及应用软件组成, 是一个真正开放的移动应用开发平台。
2007 年11 月初, Google 与其他33 家手机厂商、软硬件供应商、手机芯片供应商、移动运营商联合组成了开放手机联盟(Open Handset Alliance), 发布了名为Android 的手机软件平台, 并宣布该平台完全开放。同时Google 希望Android 平台成为一套标准化、开放式的移动嵌入式软件平台。
由于Android 系统具有开发性、平等性、无界性以及方便性等优点, 所以很快被业界所接受。从2008 年初开始, 越来越多的开发人员投身到Android 应用的开发当中。
而Android 系统的一大亮点之一就是传感器的使用, 利用传感器可以开发出很多新奇有趣的应用程序。例如计步器、水平仪, 甚至在很多游戏中都可以使用传感器来操作游戏。传感器的种类有很多种, 其中包括加速度传感器、姿态传感器、磁场传感器、温度传感器以及光传感器等, 介绍的水平仪应用就是使用姿态传感器的。
2 案例功能
将结合水平仪案例的开发详细介绍如何在Android 平台下开发传感器应用, 下面首先对水平仪的功能及界面进行简单的介绍。
2.1 程序界面
程序运行后的效果如图1 所示, 用户可以通过调整手机的姿态来控制界面中各个气泡的位置。与真正的水平仪一样, 在使用手机水平仪时, 需要将手机平放到某个平面上才可以。
图1 水平仪应用程序界面
2.2 软件功能
运行该程序, 当改变手机的姿态时, 界面中的气泡便会根据手机的姿态向高处进行相应的移动。
当手机所处的平面水平时, 各个气泡都应该位于中间的指定区域。
3 开发环境搭建
正式进入代码开发之前, 首先需要对开发环境进行搭建,其搭建步骤如下所列。
(1) 安装Java 开发环境JDK.
(2) 从网上下载Android 开发环境SDK 的压缩包, 并将其解压到磁盘上的某个位置。
(3) 将SDK 解压目录中的tools 目录添加到系统的PATH环境变量中。
(4) 下载并安装Eclipse 集成开发环境。
(5) 为Eclipse 安装Android 开发插件ADT, 并在Eclipse的Preferences 中配置Android 插件的SDK Location.
(6) 在Eclipse 的AVD Manager 中创建Android 虚拟设备(AVD), 并启动模拟器。
(7) 下载并安装用来调试Android 传感器应用的Sensorsimulator传感器模拟器软件。
(8) 在模拟器中安装Sensorsimulator 所对应的apk 文件并对其进行调试使Sensorsimulator 应用程序能够与Android 模拟器进行通信。
4 开发前的准备
前面完成了开发环境的搭建, 但在正式进行代码开发之前, 还需要再做一些开发前的准备工作, 其步骤如下:
(1) 首先启动之前安装好Eclipse.
(2) 然后依次点击File|New|Other|Android|Android Project进入项目的创建界面。
(3) 在项目创建界面中, 输入项目的名称、所使用的目标平台、所在的包名等信息, 如图2 所示。
图2 在Eclipse 中创建Android 项目
(4) 点击"Finish" 完成项目的创建。
(5) 在程序中将会用到的图片资源存放到项目文件夹的res/drawable-mdpi 目录下, 如图3 所示。
图3 图片资源
(6) 为应用程序引入调试时使用的Sensorsimulator 支持jar 包, 该jar 包位于Sensorsimulator 安装目录中的bin 目录下:
5 自定义View 的开发
本案例需要自定义一个View 来绘制水平仪的用户界面,首先需要在项目文件夹的src/wyf/ytl 目录下创建一个名为Main-View 的java 类, 并使其继承自View 类, 其代码框架如下:
package wyf.ytl; //声明所在包
import android.content.Context;//引入Context 类
import android.graphics.Bitmap; //引入Bitmap 类
import android.graphics.BitmapFactory; //引入相关类
import android.graphics.Canvas; //引入Canvas 类
import android.graphics.Color; //引入Color 类
import android.graphics.Paint; //引入Paint 类
import android.graphics.RectF; //引入RectF 类
import android.graphics.Paint.Style; //引入Style 类
import android.util.AttributeSet; //引入AttributeSet 类
import android.view.View; //引入View 类
public class MainView extends View{
Paint paint = new Paint(); //画笔
//图片资源的声明
Bitmap shangBitmap1; //上面的大矩形图
Bitmap shangBitmap2; //上面的气泡
Bitmap zuoBitmap1; //左面的大矩形图
Bitmap zuoBitmap2; //左面图的气泡
Bitmap zhongBitmap1; //中间的大圆图
Bitmap zhongBitmap2; //中间的小气泡
Bitmap xiaBitmap1; //右下的矩形图
Bitmap xiaBitmap2; //右下的气泡
//背景矩形的位置声明
int shang1_X = 60; //上面的大矩形图
int shang1_Y = 12;
int zuo1_X = 12; //左面的大矩形图
int zuo1_Y = 60;
int zhong1_X = 65; //中间的大圆图
int zhong1_Y = 65;
int xia1_X = 145; //右下的矩形图
int xia1_Y = 145;//水泡的位置声明
int shang2_X; //上面的气泡XY 坐标
int shang2_Y;
int zuo2_X; //左面图的气泡XY 坐标
int zuo2_Y;
int zhong2_X; //中间的小气泡XY 坐标
int zhong2_Y;
int xia2_X; //右下的气泡XY 坐标
int xia2_Y;
public MainView(Context context, AttributeSet attrs){
super(context, attrs);
initBitmap(); //初始化图片资源
initLocation(); //初始化气泡的位置
}
private void initBitmap(){ //初始化图片的方法
…//该处省略了部分代码,将在后面进行介绍
}
private void initLocation(){ //初始化气泡位置的方法
…//该处省略了部分代码,将在后面进行介绍
}
@Override
protected void onDraw(Canvas canvas){//重写的绘制方法
super.onDraw(canvas);
…//该处省略了部分代码,将在后面进行介绍
}
}
上述代码中的initBitmap 以及initLocation 方法是对界面进行初始化方法, 而onDraw 方法会根据需要绘制整个界面。
MainView 类构造器中调用了两个单独的方法对整个界面进行了初始化, 这是一种非常好的编程习惯。因为把不同功能的代码各自编写成独立的方法可以使主逻辑清晰, 且各个方法中的代码都不是很长, 会大大提高代码的可读性以及可维护性。
完成了代码框架的开发后就可以对其中各个方法进行开发了, 首先开发的是图片资源的初始化方法, 其代码如下:
private void initBitmap(){ //初始化图片资源的方法
shangBitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.shang1);
shangBitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.shang2);
zuoBitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.zuo1);
zuoBitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.zuo2);
zhongBitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.zhong1);
zhongBitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.zhong2);
xiaBitmap1 = BitmapFactory.decodeResource(getResources(), R.drawable.xia1);
xiaBitmap2 = BitmapFactory.decodeResource(getResources(), R.drawable.xia2);
}
上述代码为initBitmap 方法的全部代码, 其作用是对程序中所有的图片资源进行初始化, 在开发该方法之前, 应该确保所有的图片资源已经存放到了指定的目录下。
完成了图片资源初始化方法的开发后, 便可对气泡位置初始化方法initLocation 进行开发了, 其代码如下:
private void initLocation(){ //初始化气泡位置的方法
shang2_X = shang1_X + shangBitmap1.getWidth()/2- shangBitmap2.getWidth()/2;
shang2_Y = shang1_Y + shangBitmap1.getHeight()/2- shangBitmap2.getHeight()/2;
zuo2_X = zuo1_X + zuoBitmap1.getWidth()/2- zuoBitmap2.getWidth()/2;
zuo2_Y = zuo1_Y + zuoBitmap1.getHeight()/2- zuoBitmap2.getHeight()/2;
zhong2_X = zhong1_X + zhongBitmap1.getWidth()/2- zhongBitmap2.getWidth()/2;
zhong2_Y = zhong1_Y + zhongBitmap1.getHeight()/2- zhongBitmap2.getHeight()/2;
xia2_X = xia1_X + xiaBitmap1.getWidth()/2- xiaBitmap2.getWidth()/2;
xia2_Y = xia1_Y + xiaBitmap1.getHeight()/2- xiaBitmap2.getHeight()/2;
}
在该方法中通过相应图片的宽度和高度动态计算气泡的初始坐标, 采用此方法动态计算气泡坐标的好处是当日后更改图片资源后, 不需要重写修改源代码即可正常运行, 大大提高了程序的可维护性。
在完成了各个初始化方法的开发后就可以对绘制方法onDraw 进行开发, 该方法主要负责界面的绘制工作, 其代码如下:
@Override
protected void onDraw(Canvas canvas){//界面绘制方法super.onDraw(canvas);
canvas.drawColor(Color.WHITE); //设置背景色为白色
paint.setColor(Color.BLUE); //设置画笔颜色
paint.setStyle(Style.STROKE); //设置画笔为不填充
canvas.drawRect(5, 5, 315, 315, paint);//绘制外边框矩形
//画背景矩形
canvas.drawBitmap(shangBitmap1, shang1_X,shang1_Y, paint); //上
canvas.drawBitmap(zuoBitmap1, zuo1_X,zuo1_Y, paint); //左
canvas.drawBitmap(zhongBitmap1, zhong1_X,zhong1_Y, paint); //中
canvas.drawBitmap(xiaBitmap1, xia1_X,xia1_Y, paint); //下
//开始绘制气泡
canvas.drawBitmap(shangBitmap2, shang2_X,shang2_Y, paint); //上
canvas.drawBitmap(zuoBitmap2, zuo2_X,zuo2_Y, paint); //左
canvas.drawBitmap(zhongBitmap2, zhong2_X,zhong2_Y, paint); //中
canvas.drawBitmap(xiaBitmap2, xia2_X, xia2_Y, paint);//下
paint.setColor(Color.GRAY);//设置画笔颜色用来绘制刻度
//绘制上面方框中的刻度
canvas.drawLine (shang1_X+shangBitmap1.getWidth()/2-7,shang1_Y, shang1_X+shangBitmap1.getWidth()/2-7,shang1_Y+shangBitmap1.getHeight()-2, paint);
canvas.drawLine (shang1_X+shangBitmap1.getWidth()/2+7,shang1_Y, shang1_X+shangBitmap1.getWidth()/2+7,shang1_Y+shangBitmap1.getHeight()-2, paint);
//绘制左面方框中的刻度
canvas.drawLine(zuo1_X,zuo1_Y+zuoBitmap1.getHeight()/2-7,zuo1_X+zuoBitmap1.getWidth()-2,zuo1_Y+zuoBitmap1.getHeight()/2-7, paint);canvas.drawLine(zuo1_X,zuo1_Y+zuoBitmap1.getHeight()/2+7,zuo1_X+zuoBitmap1.getWidth()-2,zuo1_Y+zuoBitmap1.getHeight()/2+7, paint);
//绘制下面方框中的刻度
canvas.drawLine(xia1_X+xiaBitmap1.getWidth()/2-10,xia1_Y+xiaBitmap1.getHeight()/2-20,xia1_X+xiaBitmap1.getWidth()/2+20,xia1_Y+xiaBitmap1.getHeight()/2+10, paint);
canvas.drawLine(xia1_X+xiaBitmap1.getWidth()/2-20,xia1_Y+xiaBitmap1.getHeight()/2-10,xia1_X+xiaBitmap1.getWidth()/2+10,xia1_Y+xiaBitmap1.getHeight()/2+20, paint);
//中间圆圈中的刻度(小圆)
RectF oval = new RectF(zhong1_X+zhongBitmap1.getWidth()/2-10,zhong1_Y+zhongBitmap1.getHeight()/2-10,zhong1_X+zhongBitmap1.getWidth()/2+10,zhong1_Y+zhongBitmap1.getHeight()/2+10);
canvas.drawOval(oval, paint);//绘制基准线(圆)
}
在该方法中, 根据相应图片的X、Y 坐标将图片绘制到屏幕中, 在图片的绘制过程中, 同样动态根据相应图片的宽和高计算需要绘制到的位置坐标, 以提高程序的可维护性与灵活性。
6 相关XML 文件的编写
完成了用于显示水平仪界面的自定义View 的Java 代码开发之后, 就应该对布局XML 资源文件进行编写, 以将之前开发的自定义View 添加到用户界面中。打开项目中res/layout 目录下的main.xml, 在其中编写如下的xml 代码:
<?xml version="1.0" encoding="utf-8"?> <!--编码格式-->
<LinearLayoutxmlns:android = "http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"> <! --线性布局-->
<wyf.ytl.MainView
android:id="@+id/mainView"
android:layout_width="fill_parent"
android:layout_height = "fill_parent"/>< ! -- 自定义
View-->
</LinearLayout>
编写完布局文件main.xml 后, 还需要开发字符串资源文件strings.xml.打开res/values 下的strings.xml 文件, 编写如下的代码:
<?xml version="1.0" encoding="utf-8"?> <!--编码方式-->
<resources>
<string name="app_name">水平仪</string>
</resources>
在该文件中只是对字符串app_name 进行了定义, 在开发Android 应用程序时, 将字符串资源统一定义到一个xml 文件中是一个很好的编程习惯。
编写完上述的xml 资源文件后, 为了调试还需要为此应用程序添加网络权限, 打开项目根目录下的AndroidManifest.xml文件, 在"</manifest>" 标签之前加入下列代码:
<uses-permission android:name="android.permission.INTERNET"/>
上述代码的功能为此应用程序添加了访问网络的权限。
7 Activity 类的开发
完成了自定义View 以及XML 文件的开发后, 就可以对用户界面对应的Activity 类进行开发, 首先开发该类的代码框架,其代码如下:
package wyf.ytl; //声明所在包
import android.app.Activity; //引入相关类
import android.hardware.SensorListener;
import android.hardware.SensorManager;
import android.os.Bundle;
public class SPYActivity extends Activity { //继承Activity MainView mv; //主View
int k = 45; //灵敏度
//SensorManager mySensorManager;
//真机
SensorManagerSimulator mySensorManager; //测试时@Override
public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.main);//设置当前用户界面
mv = (MainView) findViewById(R.id.mainView);
mySensorManager =SensorManagerSimulator.getSystemService(this,SENSOR_SERVICE); //测试时
mySensorManager.connectSimulator();//测试时
//mySensorManager = (SensorManager)
// getSystemService(SENSOR_SERVICE);//真机
}
private final SensorListener mSensorLisener =new SensorListener(){ //传感器监听
//器类
…//该处省略了部分代码,将在后面进行介绍
};
@Override
protected void onResume(){ //添加监听
mySensorManager.registerListener(mSensorLisener,SensorManager.SENSOR_ORIENTATION);
super.onResume();
}
@Override
protected void onPause() { //取消监听
mySensorManager.unregisterListener (mSensorLisener);
super.onPause();
}
}
上述代码中除了重写了onCreate 方法外, 还重写了onRe-sume 以及onPause 方法为mySensorManager 添加或删除监听,并且定义了传感器监听器类mSensorLisener.
在完成了Activity 类代码框架的开发后就可以对其中传感器的监听类进行开发, 首先给出监听器类的代码框架:
private final SensorListener mSensorLisener =
new SensorListener(){//传感器监听器类
public void onSensorChanged(int sensor, float[] values){…//该处省略了部分代码,将在后面进行介绍
}
@Override
public void onAccuracyChanged(int sensor, int accuracy){}
public boolean isContain(int x, int y){//判断点是否在圆内
int tempx =(int) (x + mv.zhongBitmap2.getWidth()/2.0);
int tempy =(int) (y + mv.zhongBitmap2.getWidth()/2.0);
int ox = (int) (mv.zhong1_X+ mv.zhongBitmap1.getWidth()/2.0);
int oy = (int) (mv.zhong1_X+ mv.zhongBitmap1.getWidth()/2.0);
if(Math.sqrt((tempx-ox)*(tempx-ox)+(tempy-oy)*(tempy-oy))>(mv.zhongBitmap1.getWidth()/2.0-mv.zhongBitmap2.getWidth()/2.0)){//不在圆内return false;
}else{ //在圆内时
return true;
}
}
};
在传感器监听类中, onSensorChanged 方法用于监听传感器采样值的变化, 例如手机姿态的改变等。上述代码中的is-Contain 方法用于判断界面中间的气泡是否出界, 若出界则返回false.
完成了代码框架的开发后, 便可以对传感器的监听方法onSensorChanged 进行开发了, 其详细代码如下:
public void onSensorChanged(int sensor, float[] values){
if(sensor == SensorManager.SENSOR_ORIENTATION){
double pitch = values[SensorManager.DATA_Y];
double roll = values[SensorManager.DATA_Z];
int x=0; int y=0;//临时变量,算中间水泡坐标时用
int tempX=0; int tempY=0;//下面气泡的临时变量
//开始调整x 的值
if(Math.abs(roll)<=k){
mv.shang2_X = mv.shang1_X //上面的
+ (int)(((mv.shangBitmap1.getWidth()
-mv.shangBitmap2.getWidth())/2.0)
-(((mv.shangBitmap1.getWidth()
-mv.shangBitmap2.getWidth())/2.0)*roll)/k);
x = mv.zhong1_X //中间的
+ (int)(((mv.zhongBitmap1.getWidth()
-mv.zhongBitmap2.getWidth())/2.0)
-(((mv.zhongBitmap1.getWidth()
-mv.zhongBitmap2.getWidth())/2.0)*roll)/k);
}else if(roll>k){
mv.shang2_X=mv.shang1_X; x = mv.zhong1_X;
}else{
mv.shang2_X=mv.shang1_X+
mv.shangBitmap1.getWidth()
- mv.shangBitmap2.getWidth();
x = mv.zhong1_X+ mv.zhongBitmap1.getWidth()
- mv.zhongBitmap2.getWidth();
}
//开始调整y 的值
if(Math.abs(pitch)<=k){
mv.zuo2_Y=mv.zuo1_Y //左面的
+ (int)(((mv.zuoBitmap1.getHeight()
-mv.zuoBitmap2.getHeight())/2.0)
+(((mv.zuoBitmap1.getHeight()
-mv.zuoBitmap2.getHeight())/2.0)*pitch)/k);
y =mv.zhong1_Y+ //中间的
(int)(((mv.zhongBitmap1.getHeight()
-mv.zhongBitmap2.getHeight())/2.0)
+(((mv.zhongBitmap1.getHeight()
-mv.zhongBitmap2.getHeight())/2.0)*pitch)/k);
}else if(pitch>k){
mv.zuo2_Y=mv.zuo1_Y
+mv.zuoBitmap1.getHeight()
-mv.zuoBitmap2.getHeight();
y=mv.zhong1_Y+mv.zhongBitmap1.getHeight()
-mv.zhongBitmap2.getHeight();
}else{
mv.zuo2_Y = mv.zuo1_Y; y = mv.zhong1_Y;
}
//下面的
tempX = -(int) (((mv.xiaBitmap1.getWidth()/2-28)*roll
+(mv.xiaBitmap1.getWidth()/2-28)*pitch)/k);
tempY = -(int) ((-(mv.xiaBitmap1.getWidth()/2-28)*roll
-(mv.xiaBitmap1.getWidth()/2-28)*pitch)/k);
//限制下面的气泡范围
if(tempY>mv.xiaBitmap1.getHeight()/2-28){
tempY = mv.xiaBitmap1.getHeight()/2-28;
}
if(tempY < -mv.xiaBitmap1.getHeight()/2+28){
tempY = -mv.xiaBitmap1.getHeight()/2+28;
}
if(tempX > mv.xiaBitmap1.getWidth()/2-28){
tempX = mv.xiaBitmap1.getWidth()/2-28;
}
if(tempX < -mv.xiaBitmap1.getWidth()/2+28){
tempX = -mv.xiaBitmap1.getWidth()/2+28;
}
mv.xia2_X = tempX + mv.xia1_X
+ mv.xiaBitmap1.getWidth()/2
-mv.xiaBitmap2.getWidth()/2;
mv.xia2_Y = tempY + mv.xia1_Y
+ mv.xiaBitmap1.getHeight()/2
- mv.xiaBitmap2.getWidth()/2;
if(isContain(x, y)){//中间的水泡在圆内才改变坐标
mv.zhong2_X = x; mv.zhong2_Y = y;
}
mv.postInvalidate();//重绘MainView
}
}
在onSensorChanged 方法中首先得到pitch 轴以及roll 轴的数值, 然后根据该数值的大小调整水泡在屏幕中的位置, 同时需要对水泡的坐标进行判断, 使其保持在自身所在外框的范围内。
此时运行该程序, 并保证测试工具Sensorsimulator 与Android模拟器的连通, 便会观察到如图1 所示的效果, 通过Sensorsimulator 工具模拟手机的姿态的改变, 屏幕中的水泡便随之向高处运动。
8 程序发布
完成了所有代码的开发后, 就可以将应用程序打包发布了。本案例中只需将Eclipse 工具自动生成的apk 文件拷出即可, 按如下步骤操作。
(1) 进行正式发布之前首先需要将代码中注释为"测试时使用" 的两处代码删掉, 并将注释为"真机使用" 代码的注释去掉。
(2) 完成代码的修改后重新构建项目。
(3) 打开项目文件夹下的bin 目录, 其中名为SPY 的apk文件便为本应用程序的安装包。
(4) 将SPY.apk 文件拷贝到支持传感器的Android 手机中运行即可完成本应用程序的安装。
9 结语
通过开发基于Android 平台的传感器应用---水平仪程序, 读者应该对Android 程序的开发有了一定的了解, 同时读者也应该了解到在Android 平台下使用传感器来丰富自己软件的功能是十分方便的。
另外, 本案例虽然只对姿态传感器进行了应用, 但相信通过对本案例的学习, 读者已经有能力对其他传感器进行应用,开发出更具新意的吸引人的其他应用程序。