OPhone平台内置了非常多的传感器,通过最新的硬件技术配合强大的系统API,开发者可以轻松调用手机内置的传感器,编写极具特色的非常适合移动设备使用的应用程序,为用户带来更强的移动应用体验。
本文以最常用的重力传感器和位置传感器为例,详细介绍如何在OPhone平台上开发基于传感器的应用。
搭建OPhone开发环境
为了开发OPhone应用程序,我们需要首先搭建OPhone开发环境。目前,OPhone开发平台支持Windows和Linux,可以参考“RSS Reader实例开发之搭建OPhone开发环境”一文搭建OPhone开发环境。
使用重力传感器
重力传感器是OPhone应用中最常用的一种传感器,它用于探测手机在各个方向的倾斜角度。重力传感器一共有X,Y,Z三个方向的传感数据,X,Y,Z轴定义如下:
当手机沿某个轴左右晃动时,传感器将返回-9.81至+9.81之间的数值,表示手机的倾斜角度。可以理解为将一个小球放在手机当前位置时的重力加速度。当手机水平放置时,小球的重力加速度是0:
当手机垂直放置时,小球的重力加速度是9.81:
当手机以一定倾斜角放置时,小球的重力加速度介于0和9.81之间:
实际的返回值为-9.81至+9.81,使用正负是为了区分手机的两种方向的倾斜。通过X、Y、Z三个轴返回的重力加速度,就可以完全确定当前手机的旋转位置。
重力传感器对于开发感应游戏来说至关重要,由于手机按键的限制,使用键盘的上下左右键控制游戏很不容易,而重力传感器则能让玩家通过晃动手机来控制游戏,带来更好更具特色的游戏体验。著名的手机游戏“平衡木”就完全依靠重力传感器来让玩家控制游戏:
下面,我们用一个实际例子来演示如何使用重力传感器。这个简化版的“平衡木”例子将在手机屏幕中央绘制一个小球,当用户晃动手机时,小球也将上下左右移动。#t#
我们新建一个OPhone工程,命名为Accelerometer,然后,编辑XML布局。我们在LinearLayout中放置一个TextView,用于显示重力传感器返回的数值,一个FrameLayout,包含一个自定义的BallView,内容如下:
- xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <TextView xmlns:android=
- "http://schemas.android.com/apk/res/android"
- android:id="@+android:id/ball_prompt"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="Sensor: 0, 0, 0"
- />
- <FrameLayout xmlns:android=
- "http://schemas.android.com/apk/res/android"
- android:id="@+android:id/ball_container"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <org.expressme.wireless.accelerometer.BallView xmlns:android=
- "http://schemas.android.com/apk/res/android"
- android:id="@+android:id/ball"
- android:src="@drawable/icon"
- android:scaleType="center"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- />
- < span>FrameLayout>
BallView派生自ImageView,主要通过moveTo(x,y)方法方便地将小球移动到指定的位置:
- public class BallView extends ImageView {
- public BallView(Context context) {
- super(context);
- }
- public BallView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public BallView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
- public void moveTo(int l, int t) {
- super.setFrame(l, t, l + getWidth(), t + getHeight());
- }
- }
下面,我们就需要在MainActivity中编写主要逻辑,获得重力传感器返回的数值。重力传感器的API主要是SensorManager和Sensor,在Activity的onCreate()方法中,我们可以获得系统的SensorManager实例,然后,再获得对应的Sensor实例:
- public class MainActivity extends Activity {
- SensorManager sensorManager = null;
- Sensor sensor = null;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- prompt = (TextView) findViewById(R.id.ball_prompt);
- sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
- sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
- }
- ...
- }
我们编写一个register()方法,用于向SensorManager注册SensorEventListener,然后,在SensorEventListener中响应onSensorChanged事件,并处理即可。而unregister()方法则取消已注册的SensorEventListener。
SensorEventListener的onSensorChanged事件将返回SensorEvent对象,包含Sensor的最新数据,通过e.values获得一个float[]数组。对于不同的Sensor类型,其数组包含的元素个数是不同的,重力传感器总是返回一个长度为3的数组,分别代表X、Y和Z方向的数值。Z轴表示了手机是屏幕朝上还是屏幕朝下,一般不常用,我们主要关心X轴和Y轴的数值,以便能通过(x,y)定位小球在屏幕中的位置。通过重力传感器的X、Y值可以很容易地定位小球的位置:
- public class MainActivity extends Activity {
- private static final float MAX_ACCELEROMETER = 9.81f;
- // x, y is between [-MAX_ACCELEROMETER, MAX_ACCELEROMETER]
- void moveTo(float x, float y) {
- int max_x = (container_width - ball_width) / 2;
- int max_y = (container_height - ball_height) / 2;
- int pixel_x = (int) (max_x * x / MAX_ACCELEROMETER + 0.5);
- int pixel_y = (int) (max_y * y / MAX_ACCELEROMETER + 0.5);
- translate(pixel_x, pixel_y);
- }
- void translate(int pixelX, int pixelY) {
- int x = pixelX + container_width / 2 - ball_width / 2;
- int y = pixelY + container_height / 2 - ball_height / 2;
- ball.moveTo(x, y);
- }
- }
调用SensorManager的getDefaultSensor()就可以获得Sensor实例,这里,我们传入Sensor.TYPE_ACCELEROMETER,表示要获得重力传感器的实例。#p#
然后,我们需要向SensorManager对象注册一个SensorEventListener,这样,当Sensor变化时,我们就能获得通知:
- public class MainActivity extends Activity {
- SensorEventListener listener = new SensorEventListener() {
- public void onSensorChanged(SensorEvent e) {
- if (!init)
- return;
- float x = e.values[SensorManager.DATA_X];
- float y = e.values[SensorManager.DATA_Y];
- float z = e.values[SensorManager.DATA_Z];
- prompt.setText("Sensor: " + x + ", " + y + ", " + z);
- moveTo(-x, y);
- }
- public void onAccuracyChanged(Sensor s, int accuracy) {
- }
- };
- void register() {
- sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_GAME);
- }
- void unregister() {
- sensorManager.unregisterListener(listener);
- }
- }
需要注意的是,如何获得小球即BallView的大小,以及其容器FrameLayout的大小。在onCreate()方法中获取时我们发现,BallView和FrameLayout总是返回0,原因是OPhone系统在首次将UI组件绘制到屏幕之前,无法确定UI组件的具体大小,因此,getWidth()和getHeight()将返回0。那么,我们在何时才能获取UI组件的实际大小呢?答案仍然是第一次绘制UI组件时。由于第一次绘制UI组件会触发Focus事件,因此,我们可以响应onWindowFocusChanged()事件,在这里调用getWidth()和getHeight()能安全地返回UI组件的实际大小,因为此时Activity已绘制到手机屏幕。注意:布尔变量init用来保证仅第一次获得焦点时进行初始化:
- public class MainActivity extends Activity {
- boolean init = false;
- int container_width = 0;
- int container_height = 0;
- int ball_width = 0;
- int ball_height = 0;
- @Override
- public void onWindowFocusChanged(boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
- if (hasFocus && !init) {
- init();
- init = true;
- }
- }
- void init() {
- View container = findViewById(R.id.ball_container);
- containercontainer_width = container.getWidth();
- containercontainer_height = container.getHeight();
- ball = (BallView) findViewById(R.id.ball);
- ballball_width = ball.getWidth();
- ballball_height = ball.getHeight();
- moveTo(0f, 0f);
- }
- }
此外,传感器也是手机的系统资源,在不必要的时候我们应当及时释放资源。我们需要在onStart()、onResume()和onRestart()事件中注册,在onPause()、onStop()和onDestroy()事件中取消注册:#p#
- public class MainActivity extends Activity {
- @Override
- protected void onStart() {
- super.onStart();
- register();
- }
- @Override
- protected void onResume() {
- super.onResume();
- register();
- }
- @Override
- protected void onRestart() {
- super.onRestart();
- register();
- }
- @Override
- protected void onPause() {
- super.onPause();
- unregister();
- }
- @Override
- protected void onStop() {
- super.onStop();
- unregister();
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- unregister();
- }
- }
- public class MainActivity extends Activity { @Override protected void onStart() { super.onStart(); register(); } @Override protected void onResume() { super.onResume(); register(); } @Override protected void onRestart() { super.onRestart(); register(); } @Override protected void onPause() { super.onPause(); unregister(); } @Override protected void onStop() { super.onStop(); unregister(); } @Override protected void onDestroy() { super.onDestroy(); unregister(); } }
运行模拟器,运行效果如下:
由于模拟器无法模拟重力传感器,因此,这个应用程序需要在真机上才能看到实际效果,感兴趣的读者可以在真机上运行。
使用位置服务
位置服务是OPhone系统中另一种应用非常广泛的传感器。通过位置服务,开发基于位置的手机应用将为用户带来非常有价值的服务,如地图导航、餐厅搜索等等。
OPhone系统提供的基于位置的Location API可以提供GPS、AGPS和NETWORK三种模式的导航。其中,GPS是使用最广泛的卫星导航,我们可以利用GPS定位,开发出非常具有特色的手机应用。
下面,我们以一个具体的例子来演示如何使用位置服务。我们利用手机内置的GPS定位,通过Google地图显示当前手机用户所处的位置。
我们首先新建一个OPhone工程,命名为Location。Location API提供的主要接口是LocationManager和LocationListener。首先,我们需要在Activity的onCreate()事件中获取LocationManager的实例,并创建LocationListener实例:
- public class MainActivity extends Activity {
- private LocationManager locationManager;
- private LocationListener locationListener;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
- locationListener = new LocationListener() {
- public void onLocationChanged(Location newLocation) {
- MainActivity.this.onLocationChanged(newLocation);
- }
- public void onProviderDisabled(String provider) {
- }
- public void onProviderEnabled(String provider) {
- }
- public void onStatusChanged(String provider, int status, Bundle extras) {
- }
- };
- }
- private void onLocationChanged(Location newLocation) {
- }
- }
和使用重力传感器类似,我们需要将LocationListener注册到LocationManager,因此,在onStart()、onRestart()和onResume()事件中注册,在onStop()和onDestroy()事件中取消注册:
- public class MainActivity extends Activity {
- @Override
- protected void onStart() {
- super.onStart();
- register();
- }
- @Override
- protected void onRestart() {
- super.onRestart();
- register();
- }
- @Override
- protected void onResume() {
- super.onResume();
- register();
- }
- @Override
- protected void onStop() {
- super.onStop();
- unregister();
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- unregister();
- }
- private void register() {
- locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- 10000L,
- 0,
- locationListener
- );
- }
- private void unregister() {
- locationManager.removeUpdates(locationListener);
- }
- }
注册是通过requestLocationUpdates()方法完成的,第一个参数指定了位置服务的提供者,这里是GPS_PROVIDER,第二个和第三个参数指定了发送位置更新的最小时间间隔和最小距离间隔。我们指定了发送位置更新的时间间隔不小于10秒,而最小距离不限。最后一个参数是LocationListener的实例。注册后,就可以在onLocationChanged()事件中获得当前的Location。OPhone系统传入一个Location对象,使用如下代码即获得当前位置的经度和纬度:
- double lat = newLocation.getLatitude();
- double lng = newLocation.getLongitude();
有了经度和纬度,我们就可以在Google地图中做出标记,让用户在地图上看到自己的当前位置。
Google API提供了MapView,可以直接显示Google地图,并方便地对其进行控制。而OPhone 1.5 SDK并不包含Google API,因此,我们就无法使用MapView了,怎么办?答案是自己动手,丰衣足食。MapView归根结底也是在WebView基础上封装而成的,我们完全可以在WebView中显示Google地图并对其进行控制。#p#
我们首先在XML布局中添加一个WebView:
- xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android=
- "http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <WebView
- android:id="@+android:id/webview"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- />
- < span>FrameLayout>
WebView本质上就是浏览器,它和OPhone系统自带的浏览器完全一样。既然系统浏览器可以直接显示Google地图,那么,我们使用WebView也能显示Google地图。编写一个简单的HTML页面如下:
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
- <title>Map< span>title>
- <script src="http://ditu.google.cn/maps?hl=zh-CN&file=api&v=2&sensor=true&key=ABQIAAAANv4vRQVBvuMJA6tyhpEVYhT2yXp_ZAY8_ufC3CFXhHIE1NvwkxS5EvCTylQAqE2076RlFUaSV7w-gA" type="text/javascript">< span>script>
- <script type="text/javascript">
- var g_map = null;
- var g_marker = null;
- function getParam(_param) {
- var query = location.search.substring(1);
- var pairs = query.split("&");
- for(var i=0;i<pairs.length;i++) {
- var pos = pairs[i].indexOf("=");
- if(pos==-1)
- continue;
- var argname=pairs[i].substring(0,pos);
- if(argname==_param) {
- var value=pairs[i].substring(pos+1).replace(/\+/g,' ');
- return decodeURIComponent(value);
- }
- }
- return null;
- }
- function initialize() {
- d = document.getElementById("map_canvas");
- sw = getParam("w");
- sh = getParam("h");
- if (sw!=null && sh!=null) {
- w = parseInt(sw);
- h = parseInt(sh);
- d.style.width = w + "px";
- d.style.height = h + "px";
- }
- lat = parseFloat(getParam("lat"));
- lng = parseFloat(getParam("lng"));
- map = new GMap2(d);
- map.setCenter(new GLatLng(lat, lng), 14);
- setMarker(lat, lng);
- }
- function setMarker(lat, lng) {
- if (g_marker!=null)
- map.removeOverlay(g_marker);
- ll = new GLatLng(lat, lng);
- g_marker = new GMarker(ll);
- map.addOverlay(g_marker);
- map.panTo(ll);
- }
- < span>script>
- < span>head>
- <body onload="initialize()"
- onunload="GUnload()"
- style="margin: 0px; padding: 0px">
- <div id="map_canvas" style="width: 260px; height: 300px">< span>div>
- < span>body>
- < span>html>
其中,导入Google地图是通过JavaScript完成的:
- <script src="http://ditu.google.cn/maps?...">< span>script>
为了控制地图标记的显示,我们编写了一个JavaScript函数:
- function setMarker(lat, lng) { ... }
稍候我们会讲解如何调用这个JavaScript函数。
由于Google地图需要开发者申请一个Key才能使用,尽管申请Key是免费的,但是,不同的域名会对应不同的Key,为了简化应用程序的开发,我们直接使用localhost的Key,并通过如下代码将上面的HTML页面载入到WebView中,告诉WebView当前导航地址是http://localhost/map.html,这样,应用程序就无需再申请Key了:
- webView.loadDataWithBaseURL
- ("http://localhost/map.html?lat=0&lng=0&w=" +
- webView.getWidth() + "&h=" + webView.getHeight(), loadHtml(), "text/html", "UTF-8", null);
从assets中读取文件内容的loadHtml()方法如下:
- String loadHtml() {
- InputStream input = null;
- try {
- input = getAssets().open("map.html");
- ByteArrayOutputStream result = new ByteArrayOutputStream(4096);
- byte[] buffer = new byte[1024];
- for (;;) {
- int n = input.read(buffer);
- if (n==(-1))
- break;
- result.write(buffer, 0, n);
- }
- return result.toString("UTF-8");
- }
- catch (IOException e) {
- return "";
- }
- finally {
- if (input!=null) {
- try {
- input.close();
- }
- catch (IOException e) {}
- }
- }
- }
这里需要注意的要点是,更新WebView需要在UI线程中进行,通过Handler.post()方法很容易实现,而调用JavaScript函数则通过loadUrl("javascript:函数名(参数)")就完成了,非常简单。
- void onLocationChanged(Location newLocation) {
- final double lat = newLocation.getLatitude();
- final double lng = newLocation.getLongitude();
- handler.post(
- new Runnable() {
- public void run() {
- webView.loadUrl
- ("javascript:setMarker(" + lat + "," + lng + ")");
- }
- }
- );
- }
运行代码,我们发现,调用该JavaScript函数不起作用,原因是WebView默认状态不启用JavaScript功能,因此,还需要在Activity的onCreate()中添加一点初始化代码,顺便将WebView的滚动条去掉:
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- webView = (WebView) findViewById(R.id.webview);
- webView.getSettings().setJavaScriptEnabled(true);
- webView.setVerticalScrollBarEnabled(false);
- webView.setHorizontalScrollBarEnabled(false);
- }
最后,不要忘记在AndroidManifest.xml中添加权限声明,我们需要网络访问权限和位置访问权限:
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name=
- "android.permission.ACCESS_FINE_LOCATION" />
运行模拟器,我们可以通过Emulator Control向模拟器发送经度和纬度数据,应用程序运行效果如下:
通过对assets资源的国际化,我们还可以在一个应用程序中针对中英文用户分别显示中文地图和英文地图:
在真机上运行该应用程序时,随着用户的移动,地图会自动跟踪并刷新用户的当前位置。感兴趣的读者可以在真机上运行。