在OPhone應(yīng)用程序中使用傳感器和LBS
OPhone平臺內(nèi)置了非常多的傳感器,通過最新的硬件技術(shù)配合強大的系統(tǒng)API,開發(fā)者可以輕松調(diào)用手機內(nèi)置的傳感器,編寫極具特色的非常適合移動設(shè)備使用的應(yīng)用程序,為用戶帶來更強的移動應(yīng)用體驗。
本文以最常用的重力傳感器和位置傳感器為例,詳細介紹如何在OPhone平臺上開發(fā)基于傳感器的應(yīng)用。
搭建OPhone開發(fā)環(huán)境
為了開發(fā)OPhone應(yīng)用程序,我們需要首先搭建OPhone開發(fā)環(huán)境。目前,OPhone開發(fā)平臺支持Windows和Linux,可以參考“RSS Reader實例開發(fā)之搭建OPhone開發(fā)環(huán)境”一文搭建OPhone開發(fā)環(huán)境。
使用重力傳感器
重力傳感器是OPhone應(yīng)用中最常用的一種傳感器,它用于探測手機在各個方向的傾斜角度。重力傳感器一共有X,Y,Z三個方向的傳感數(shù)據(jù),X,Y,Z軸定義如下:
當(dāng)手機沿某個軸左右晃動時,傳感器將返回-9.81至+9.81之間的數(shù)值,表示手機的傾斜角度??梢岳斫鉃閷⒁粋€小球放在手機當(dāng)前位置時的重力加速度。當(dāng)手機水平放置時,小球的重力加速度是0:
當(dāng)手機垂直放置時,小球的重力加速度是9.81:
當(dāng)手機以一定傾斜角放置時,小球的重力加速度介于0和9.81之間:
實際的返回值為-9.81至+9.81,使用正負是為了區(qū)分手機的兩種方向的傾斜。通過X、Y、Z三個軸返回的重力加速度,就可以完全確定當(dāng)前手機的旋轉(zhuǎn)位置。
重力傳感器對于開發(fā)感應(yīng)游戲來說至關(guān)重要,由于手機按鍵的限制,使用鍵盤的上下左右鍵控制游戲很不容易,而重力傳感器則能讓玩家通過晃動手機來控制游戲,帶來更好更具特色的游戲體驗。著名的手機游戲“平衡木”就完全依靠重力傳感器來讓玩家控制游戲:
下面,我們用一個實際例子來演示如何使用重力傳感器。這個簡化版的“平衡木”例子將在手機屏幕中央繪制一個小球,當(dāng)用戶晃動手機時,小球也將上下左右移動。#t#
我們新建一個OPhone工程,命名為Accelerometer,然后,編輯XML布局。我們在LinearLayout中放置一個TextView,用于顯示重力傳感器返回的數(shù)值,一個FrameLayout,包含一個自定義的BallView,內(nèi)容如下:
- 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中編寫主要邏輯,獲得重力傳感器返回的數(shù)值。重力傳感器的API主要是SensorManager和Sensor,在Activity的onCreate()方法中,我們可以獲得系統(tǒng)的SensorManager實例,然后,再獲得對應(yīng)的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中響應(yīng)onSensorChanged事件,并處理即可。而unregister()方法則取消已注冊的SensorEventListener。
SensorEventListener的onSensorChanged事件將返回SensorEvent對象,包含Sensor的最新數(shù)據(jù),通過e.values獲得一個float[]數(shù)組。對于不同的Sensor類型,其數(shù)組包含的元素個數(shù)是不同的,重力傳感器總是返回一個長度為3的數(shù)組,分別代表X、Y和Z方向的數(shù)值。Z軸表示了手機是屏幕朝上還是屏幕朝下,一般不常用,我們主要關(guān)心X軸和Y軸的數(shù)值,以便能通過(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);
- }
- }
調(diào)用SensorManager的getDefaultSensor()就可以獲得Sensor實例,這里,我們傳入Sensor.TYPE_ACCELEROMETER,表示要獲得重力傳感器的實例。#p#
然后,我們需要向SensorManager對象注冊一個SensorEventListener,這樣,當(dāng)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()方法中獲取時我們發(fā)現(xiàn),BallView和FrameLayout總是返回0,原因是OPhone系統(tǒng)在首次將UI組件繪制到屏幕之前,無法確定UI組件的具體大小,因此,getWidth()和getHeight()將返回0。那么,我們在何時才能獲取UI組件的實際大小呢?答案仍然是第一次繪制UI組件時。由于第一次繪制UI組件會觸發(fā)Focus事件,因此,我們可以響應(yīng)onWindowFocusChanged()事件,在這里調(diào)用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);
- }
- }
此外,傳感器也是手機的系統(tǒng)資源,在不必要的時候我們應(yīng)當(dāng)及時釋放資源。我們需要在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(); } }
運行模擬器,運行效果如下:
由于模擬器無法模擬重力傳感器,因此,這個應(yīng)用程序需要在真機上才能看到實際效果,感興趣的讀者可以在真機上運行。
使用位置服務(wù)
位置服務(wù)是OPhone系統(tǒng)中另一種應(yīng)用非常廣泛的傳感器。通過位置服務(wù),開發(fā)基于位置的手機應(yīng)用將為用戶帶來非常有價值的服務(wù),如地圖導(dǎo)航、餐廳搜索等等。
OPhone系統(tǒng)提供的基于位置的Location API可以提供GPS、AGPS和NETWORK三種模式的導(dǎo)航。其中,GPS是使用最廣泛的衛(wèi)星導(dǎo)航,我們可以利用GPS定位,開發(fā)出非常具有特色的手機應(yīng)用。
下面,我們以一個具體的例子來演示如何使用位置服務(wù)。我們利用手機內(nèi)置的GPS定位,通過Google地圖顯示當(dāng)前手機用戶所處的位置。
我們首先新建一個OPhone工程,命名為Location。Location API提供的主要接口是LocationManager和LocationListener。首先,我們需要在Activity的onCreate()事件中獲取LocationManager的實例,并創(chuàng)建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()方法完成的,第一個參數(shù)指定了位置服務(wù)的提供者,這里是GPS_PROVIDER,第二個和第三個參數(shù)指定了發(fā)送位置更新的最小時間間隔和最小距離間隔。我們指定了發(fā)送位置更新的時間間隔不小于10秒,而最小距離不限。最后一個參數(shù)是LocationListener的實例。注冊后,就可以在onLocationChanged()事件中獲得當(dāng)前的Location。OPhone系統(tǒng)傳入一個Location對象,使用如下代碼即獲得當(dāng)前位置的經(jīng)度和緯度:
- double lat = newLocation.getLatitude();
- double lng = newLocation.getLongitude();
有了經(jīng)度和緯度,我們就可以在Google地圖中做出標記,讓用戶在地圖上看到自己的當(dāng)前位置。
Google API提供了MapView,可以直接顯示Google地圖,并方便地對其進行控制。而OPhone 1.5 SDK并不包含Google API,因此,我們就無法使用MapView了,怎么辦?答案是自己動手,豐衣足食。MapView歸根結(jié)底也是在WebView基礎(chǔ)上封裝而成的,我們完全可以在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本質(zhì)上就是瀏覽器,它和OPhone系統(tǒng)自帶的瀏覽器完全一樣。既然系統(tǒng)瀏覽器可以直接顯示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>
其中,導(dǎo)入Google地圖是通過JavaScript完成的:
- <script src="http://ditu.google.cn/maps?...">< span>script>
為了控制地圖標記的顯示,我們編寫了一個JavaScript函數(shù):
- function setMarker(lat, lng) { ... }
稍候我們會講解如何調(diào)用這個JavaScript函數(shù)。
由于Google地圖需要開發(fā)者申請一個Key才能使用,盡管申請Key是免費的,但是,不同的域名會對應(yīng)不同的Key,為了簡化應(yīng)用程序的開發(fā),我們直接使用localhost的Key,并通過如下代碼將上面的HTML頁面載入到WebView中,告訴WebView當(dāng)前導(dǎo)航地址是http://localhost/map.html,這樣,應(yīng)用程序就無需再申請Key了:
- webView.loadDataWithBaseURL
- ("http://localhost/map.html?lat=0&lng=0&w=" +
- webView.getWidth() + "&h=" + webView.getHeight(), loadHtml(), "text/html", "UTF-8", null);
從assets中讀取文件內(nèi)容的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()方法很容易實現(xiàn),而調(diào)用JavaScript函數(shù)則通過loadUrl("javascript:函數(shù)名(參數(shù))")就完成了,非常簡單。
- 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 + ")");
- }
- }
- );
- }
運行代碼,我們發(fā)現(xiàn),調(diào)用該JavaScript函數(shù)不起作用,原因是WebView默認狀態(tài)不啟用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中添加權(quán)限聲明,我們需要網(wǎng)絡(luò)訪問權(quán)限和位置訪問權(quán)限:
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name=
- "android.permission.ACCESS_FINE_LOCATION" />
運行模擬器,我們可以通過Emulator Control向模擬器發(fā)送經(jīng)度和緯度數(shù)據(jù),應(yīng)用程序運行效果如下:
通過對assets資源的國際化,我們還可以在一個應(yīng)用程序中針對中英文用戶分別顯示中文地圖和英文地圖:
在真機上運行該應(yīng)用程序時,隨著用戶的移動,地圖會自動跟蹤并刷新用戶的當(dāng)前位置。感興趣的讀者可以在真機上運行。