Chapter 14 感測器 作者 : 林致孙 手機和感測器的結合, 讓手機產生更多的應用, 除了應用於遊戲軟體, 感測器也讓手機上實作擴增實境變得更容易 本章將介紹應用程式如何讀取手機上的感測器, 同時也會提供範例, 讓讀者瞭解方位感測器 (Orientation Sensor) 與加速度感測器 (Accelerometer Sensor) 的應用 14.1 讀取感測資料 首先我們先學習感測器相關類別的使用方法, 請讀者引進光碟中 \ 範例程式 \Chapter14\SensorList 這個專案, 這個專案有兩個 Activity: SensorList: 繼承了 ListActivity, 會列出手機上所的感測器, 點選個別的感測器後, 會跳至 SensorReader 讀取所選取的感測器的讀值 SensorReader: 讀取特定感測器的數值 讓我們先來看 SensorList 這個 Activity, 其內容如下所示 : 1 public class SensorList extends ListActivity { 2 3 private SensorManager smgr; 4 List<Sensor> slist; 5 6 @Override 7 public void oncreate(bundle savedinstancestate) { 8 super.oncreate(savedinstancestate); 9 setcontentview(r.layout.main); 10 11 smgr = (SensorManager)getSystemService( 12 Context.SENSOR_SERVICE); 13 slist = smgr.getsensorlist(sensor.type_all); 14 ArrayList<String> snlist = new ArrayList<String>(); 15 16 for (int i = 0; i < slist.size(); i++) 17 snlist.add(slist.get(i).getname()); 18 19 ListAdapter adapter = new ArrayAdapter<String>(this, 20 R.layout.list_item, snlist);
21 setlistadapter(adapter); 22 } 23 24 @Override 25 protected void onlistitemclick(listview l, View v, 26 int position, long id) { 27 super.onlistitemclick(l, v, position, id); 28 29 Intent intent = new Intent(this, SensorReader.class); 30 intent.putextra("key_type", slist.get(position).gettype()); 31 startactivity(intent); 32 } 33 } 首先為了存取手機上的感測器資訊, 程式於第 11 行先呼叫 getsystemservice 取得一個 SensorManager 物件 [1], 第 13 行呼叫 SensorManager 物件的 getsensorlist 方法取得 Sensor 物件 [2], 參數 Sensor. TYPE_ALL 代表所有種類的感測器都要取得, 如果是 Sensor. TYPE_ACCELEROMETER 代表只取得加速度感測器的 Sensor 物件 ( 一個手機有可能擁有兩個以上的同種類測器 ) 第 17 行呼叫 Sensor 物件的 getname 方法取得感測器的名稱, 這名稱即要呈現於列表介面元件 (ListView) 的 資料 而當使用者點選某個感測器後, 會將感測器的型別 ( 可想成種類 ) 夾帶在 Intent 裡傳送給 SensorReader 下圖是筆者截取實機所得到的執行畫面: 接著討論 SensorReader 這個 Activity, 其內容如下所示 : 1 public class SensorReader extends Activity {
2 3 private SensorManager smgr; 4 private TextView tv; 5 private Sensor sensor; 6 7 @Override 8 public void oncreate(bundle savedinstancestate) { 9 super.oncreate(savedinstancestate); 10 setcontentview(r.layout.reader); 11 12 tv = (TextView)findViewById(R.id.tv_sresult); 13 14 smgr = (SensorManager)getSystemService( 15 Context.SENSOR_SERVICE); 16 Intent intent = getintent(); 17 int stype = intent.getintextra("key_type", -1); 18 19 sensor = smgr.getdefaultsensor(stype); 20 } 21 22 @Override 23 protected void onresume() { 24 smgr.registerlistener(slistener, sensor, 25 SensorManager.SENSOR_DELAY_UI); 26 super.onresume(); 27 } 28 29 @Override 30 protected void onpause() { 31 smgr.unregisterlistener(slistener, sensor); 32 super.onpause(); 33 } 34 35 private final SensorEventListener slistener = 36 new SensorEventListener() { 37 public void onsensorchanged (SensorEvent event) { 38 if (event.sensor!= sensor) return; 39
40 String str = ""; 41 42 switch (sensor.gettype()) { 43 case Sensor.TYPE_ACCELEROMETER: 44 str = "Accelerometer Sensor\n"; 45 break; 46 case Sensor.TYPE_GYROSCOPE: 47 str = "Gyroscope Sensor\n"; 48 break; 49 case Sensor.TYPE_LIGHT: 50 str = "Light Sensor\n"; 51 break; 52 case Sensor.TYPE_MAGNETIC_FIELD: 53 str = "Magnetic Field Sensor\n"; 54 break; 55 case Sensor.TYPE_ORIENTATION: 56 str = "Orientation Sensor\n"; 57 break; 58 case Sensor.TYPE_PRESSURE: 59 str = "Pressure Sensor\n"; 60 break; 61 case Sensor.TYPE_PROXIMITY: 62 str = "Proximity Sensor\n"; 63 break; 64 case Sensor.TYPE_TEMPERATURE: 65 str = "Temperature Sensor\n"; 66 break; 67 } 68 69 for (int i = 0; i < event.values.length; i++) 70 str = str + "values[" + i + "]: " + 71 event.values[i] + "\n"; 72 73 str = str + "Accuracy: " + event.accuracy; 74 75 tv.settext(str); 76 } 77 }
78 public void onaccuracychanged (Sensor sensor, int accuracy) { 79 } 80 }; 81 } 首先在第 14 行, 同樣使用 getsystemservice 方法取得 SensorManger 物件, 接著利用 getdefaultsensor 取得該種類的 Sensor 物件, 若同種類的感測器數目有兩個以上,Intent 需另外攜帶感測器的名稱, 同時應該使用 getsensorlist 取代 getdefaultsensor 為了讀取該感測器的資料, 我們必須設計一個傾聽者給感測器, 傾聽者類別需實作 SensorEventListener 介面 [3], 程式碼是位於 35~80 行, 一樣使用了匿名類別的技巧, 有兩個抽象方法需要實作 : onaccuracychanged: 當量測值的精準度改變時, 這個方法會被呼叫 onsensorchanged: 當量測值改變時, 這個方法會被呼叫 我們把焦點放在 onsensorchanged 方法,onSensorchanged 會傳入一個 SensorEvent 物件 [4], 感測的值可從這個 SensorEvent 物件中抓取, 首先在 38 行, 先判斷發生事件的感測器是否是我們所要量測的感測器, 接著 42~67 行, 我們將感測器的種類抓出來, 並設定之後要用來顯示於 TextView 上的字串, 字串是告訴使用者讀取到的感測器是哪一種感測器 讀取感測器值的程式是寫在 69~71 行, 感測值會儲存在一個浮點數陣列 (float [] values), 這個浮點數陣列是 SensorEvent 類別的成員變數, 不同種類的感測器有不同的意義 [4], 以方位感測器 (Orientation Sensor) 為例, 當手機是正著直著拿在手上時, 如下圖所示,values[0] 的值意義為 :0 代表北方 90 代表東方 180 代表南方 270 代表西方,values[1] 是縱向旋轉角, 現在一樣正著直著拿著手機, 向外旋轉, 垂直向上時值為 90 面朝上平置時值為 0 垂直向下時值為 90 面朝下平置時值為 180,values[2] 是橫向旋轉角, 現在一樣正著直著拿著手機, 左或向右旋轉, 朝前時值為 0 往右橫放是-90 往左橫放是 90, 詳細的定義可參閱說明文件 [4], 若對說明文件 [4] 不是十分瞭解, 筆者建議讀者可利用本程式自行做一些測詴, 便能體會出值是如何變化的
而以加速度感測器為例的話,values[0] 代表手機的 X 軸所承受的加速度減去重力於 X 軸所帶來的加速度, 以上圖為例, 手機的 X 軸是指比較窄的那一個邊所型成的軸, 往右較大,values[1] 代表手機的 Y 軸所承受的加速度減去重力於 Y 軸所帶來的加速度, 以上圖為例, 手機的 Y 軸是指比較長的那一個邊所型成的軸, 往上較大,values[2] 代表手機的 Z 軸所承受的加速度減去重力於 Z 軸所帶來的加速度, 手機的 Z 軸代表著向外或向內, 往外較大 當正著直著拿著手機時, 可發現,values[0] 與 values[2] 會接近於 0, 而 Y 軸受到了向下的重力加速度 (-9.81 公尺 / 秒平方 ), 因為手機穩穩的拿在手上, 因此 Y 軸事實上並沒有任何加速度 (0 公尺 / 秒平方 ), 因此 0-(-9.81)=9.81, 讀者可發現 values[1] 的值會接近 9.81 其它感測器的讀值就請讀者自行閱讀說明文件 SensorEvent 物件還有一個成員變數為 accuracy(73 行 ), SENSOR_STATUS_ACCURACY_HIGH( 值為 3) 代表這個回報的值是很精準的, SENSOR_STATUS_UNRELIABLE( 值為 0) 則代表回報的值的精準度是最低的, 請讀者參考說明文件 [1] 此外, 若在精準度發生變化時, 做些處理動做的話, 就要去確實實作 78 行的 onaccuracychanged 方法 現在傾聽者已經設計完成, 我們要幫感測器跟其傾聽者連結在一起, 當感測器註冊了傾聽者後, 程式就會一直去讀取感測器的資料, 因此我們希望只有當程式在前景執行時才讀取感測器的資料, 因此於 onresume 方法 ( 第 24 行 ) 呼叫 SensorManager 類別的 registerlistener 方法去做註冊,registerListener 方法需要三
個參數, 第一個參數傾聽者物件, 第二個參數是感測器物件, 第三個參數是設定回報速率, 亦即多久回報一次, 共有四種速率可選擇 : SENSOR_DELAY_FASTEST SENSOR_DELAY_GAME SENSOR_DELAY_UI 與 SENSOR_DELAY_NORMAL 程式於 onpause 方法 ( 第 31 行 ) 呼叫 unregisterlistener 方法取消註冊, 如此當程式 進入背景或結束時就不會讀取感測器了 下圖是筆者於實機讀取方位感測器的一 個執行畫面 : 14.2 方位感測器的應用 在這一節筆者將示範一個方位感測器的應用, 這個應用程式的功能相當簡單, 就是出現一個箭頭指向亞洲大學行政大樓 請讀者引進光碟中 \ 範例程式 \Chapter14\OrientationApp 這個專案, 裡面有兩個 Java 原始碼檔案 : OrientationApp.java: 這是一個 Activity, 會讀取 GPS 以及方位感測器的資料, 藉以判斷箭頭應該呈現的方向 ArrowView.java: 其繼承了 View 類別, 是一個客製化的 View, 主要是用來顯示箭頭 先來討論 OrientationApp.java, 其內容如下 : 1 public class OrientationApp extends Activity { 2 3 private ArrowView av; 4 private MyLocationListener mll; 5 private MySensorListener msl;
6 private LocationManager lmgr; 7 private SensorManager smgr; 8 private List<Sensor> slist; 9 private float orientation, target; 10 11 @Override 12 public void oncreate(bundle savedinstancestate) { 13 super.oncreate(savedinstancestate); 14 15 setrequestedorientation( 16 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 17 18 av = new ArrowView(this); 19 setcontentview(av); 20 21 lmgr = (LocationManager)getSystemService(LOCATION_SERVICE); 22 mll = new MyLocationListener(); 23 24 smgr = (SensorManager)getSystemService( 25 Context.SENSOR_SERVICE); 26 msl = new MySensorListener(); 27 28 slist = smgr.getsensorlist(sensor.type_orientation); 29 if (slist.size() == 0) { 30 Toast.makeText(this, "No orientation sensor", 31 Toast.LENGTH_SHORT).show(); 32 finish(); 33 } 34 orientation = (float)0.0; 35 target = (float)0.0; 36 } 37 38 @Override 39 protected void onresume() { 40 super.onresume(); 41 lmgr.requestlocationupdates( 42 LocationManager.GPS_PROVIDER, 0, 0, mll); 43 smgr.registerlistener(msl, slist.get(0),
44 SensorManager.SENSOR_DELAY_UI); 45 } 46 47 @Override 48 protected void onpause() { 49 super.onpause(); 50 lmgr.removeupdates(mll); 51 smgr.unregisterlistener(msl, slist.get(0)); 52 } 53 54 private void adjustarrow() { 55 float degree = target-orientation; 56 if (degree < 0) degree = degree + 360.0f; 57 av.setdegree(degree); 58 setcontentview(av); 59 } 60 61 class MySensorListener implements SensorEventListener { 62 public void onsensorchanged (SensorEvent event) { 63 if (event.sensor == slist.get(0)) { 64 orientation = event.values[0]; 65 adjustarrow(); 66 } 67 } 68 public void onaccuracychanged (Sensor sensor, int accuracy) { 69 } 70 } 71 72 class MyLocationListener implements LocationListener { 73 @Override 74 public void onlocationchanged(location location) { 75 if (location == null) 76 return; 77 78 Location dest = new Location(location); 79 dest.setlatitude(24.045857); 80 dest.setlongitude(120.686681); 81 target = location.bearingto(dest);
82 adjustarrow(); 83 } 84 @Override 85 public void onproviderdisabled(string provider) { 86 } 87 @Override 88 public void onproviderenabled(string provider) { 89 } 90 @Override 91 public void onstatuschanged(string provider, int status, 92 Bundle extras) { 93 } 94 } 95 } 這個程式使用到的大部份類別與方法都學過了, 因此筆者只告訴讀者這個程式做了哪幾件事, 讓讀者自行去閱讀細節即可, 這個程式主要做了幾件事 : 設計感測器的傾聽者 (61~70 行 ): 當發現感測值發生變化後, 程式會設定變數 orientation 的值 (values[0]), 藉此來判斷使用者面對的方位, 並呼叫我們自行定義的 adjustarrow 方法來設定箭頭的方向 設計 GPS 的傾聽者 (72~94 行 ): 當發現使用者所在位置改變時, 程式會去計算目標物 ( 亞洲大學行政大樓 ) 是在使用者的哪個方位, 要知道目標物是在使用者的哪個方位可利用 Location 類別 bearingto 方法 [5], 如果目標物在使用者的正北方, 變數 target 的值為 0, 如果目標物在使用者的正東方, 變數 target 的值為 90, 以此類推 接著一樣呼叫自行定義的 adjustarrow 方法來設定箭頭的方向 此處要提醒一下,120.686681 及 24.045857 是亞洲大學行政大樓的經緯度值, 讀者可換成在自己附近的目標物, 以方便程式的測詴 設定箭頭方向 : 寫於 adjustarrow 方法 (54~59 行 ), 我們假設手機正上方為 0 度, 右側為 90 度, 則箭頭應該偏轉的角度只要將 target 減去 orientation 即可算出 算出偏轉角度後呼叫 ArrowView 類別 ( 程式自行定義的, 稍後會解說 ) 的 setdegree 方法將角度傳送給 ArrowView 物件, 並將 ArrowView 顯示於畫面上 於 onresume 方法啟動定位與感測服務 (39~45 行 ) 於 onpause 方法停止定位與感測服務 (48~52 行 ) 在這個 OrientationApp.java 的程式中, 我們沒學過的應該就只有第 15 行 Activity 類別的 setrequestedorientation 方法, 其可用來設定畫面的呈現方式, 是要直著 看 (SCREEN_ORIENTATION_PORTRAIT) 或是橫著看
(SCREEN_ORIENTATION_LANDSCAPE), 如下圖所示, 其它的呈現方式可參考 ActivityInfo 類別的說明 [6] 接下來可以開始來討論 ArrowView.java 了, 其內容如下 : 1 public class ArrowView extends View { 2 3 private final float alength = (float)100.0; 4 private final float arrowd = (float)10.0; 5 private final float arroww = (float)5.0; 6 7 float startx, starty, stopx, stopy, degree; 8 9 public ArrowView(Context context) { 10 super(context); 11 startx = (float)160.0; 12 starty = (float)240.0; 13 degree = (float)0.0; 14 } 15 16 protected void setdegree(float degree) { 17 this.degree = degree; 18 } 19 20 @Override 21 protected void ondraw(canvas canvas) {
22 23 Paint paint = new Paint(); 24 25 float radian = (float)(degree*math.pi/180.0); 26 stopx = startx + (float)(alength*math.sin(radian)); 27 stopy = starty - (float)(alength*math.cos(radian)); 28 canvas.drawcolor(color.white); 29 canvas.drawline(startx, starty, stopx, stopy, paint); 30 31 float v3x, v3y, diffx, diffy, leftax, leftay, rightax, rightay; 32 33 v3x = stopx + ((startx-stopx)*arrowd)/alength; 34 v3y = stopy + ((starty-stopy)*arrowd)/alength; 35 36 diffx = (float)math.abs((arroww*(stopy-starty))/alength); 37 diffy = (float)math.abs((arroww*(stopx-startx))/alength); 38 39 if ((startx-stopx) < 0.0) { 40 leftay = v3y - diffy; 41 rightay = v3y + diffy; 42 } else { 43 leftay = v3y + diffy; 44 rightay = v3y - diffy; 45 } 46 47 if ((starty-stopy) < 0.0) { 48 leftax = v3x + diffx; 49 rightax = v3x - diffx; 50 } else { 51 leftax = v3x - diffx; 52 rightax = v3x + diffx; 53 } 54 canvas.drawline(leftax, leftay, stopx, stopy, paint); 55 canvas.drawline(rightax, rightay, stopx, stopy, paint); 56 } 57 } ArrowView 繼承了 View 類別, 是一個客製化的 View, 我們只要覆寫 ondraw 方
法, 便可呈現我們所設計的 View,onDraw 會傳進一個畫布 (Canvas) 物件, 我們只要在畫布上畫出想要呈現的線條 圖形等即可設計出自己的 View, 這部份請讀者自行閱讀相關類別的說明文件 [7][8], 這裡不做太多的說明, 然而筆者還是附上一張圖, 讓讀者能夠更容易瞭解箭頭是如何畫出來的 最後一件要提醒讀者的事是, 別忘了於 AndroidManifest.xml 中聲明這個程式需 要得到精確位置資訊, 亦即要讀取 GPS 資訊 下面是執行畫面, 然而要測詴這 個程式是否正確, 還是要走出戶外 : 14.3 加速度感測器的應用 在這一節筆者將示範一個加速度感測器的應用, 這個應用程式的功能用來偵測手機是否有劇烈晃動 請讀者引進光碟中 \ 範例程式 \Chapter14\AccelerometerApp 這個專案, 裡面只有 AccelerometerApp 這個 Activity, 其內容如下 : 1 public class AccelerometerApp extends Activity { 2 3 float max;
4 TextView tv; 5 private SensorManager smgr; 6 private List<Sensor> slist; 7 boolean isstarted; 8 9 @Override 10 public void oncreate(bundle savedinstancestate) { 11 super.oncreate(savedinstancestate); 12 13 setrequestedorientation( 14 ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 15 16 setcontentview(r.layout.main); 17 tv = (TextView)findViewById(R.id.tv_max); 18 Button btn_start = (Button)findViewById(R.id.btn_start); 19 btn_start.setonclicklistener(start_l); 20 Button btn_stop = (Button)findViewById(R.id.btn_stop); 21 btn_stop.setonclicklistener(stop_l); 22 23 smgr = (SensorManager)getSystemService( 24 Context.SENSOR_SERVICE); 25 26 slist = smgr.getsensorlist(sensor.type_accelerometer); 27 if (slist.size() == 0) { 28 Toast.makeText(this, "No accelerometer sensor", 29 Toast.LENGTH_SHORT).show(); 30 finish(); 31 } 32 isstarted = false; 33 } 34 35 private final SensorEventListener mlistener = new 36 SensorEventListener() { 37 public void onsensorchanged (SensorEvent event) { 38 if (event.sensor == slist.get(0)) { 39 if (isstarted == false) return; 40 41 float totalforce = (float)0.0;
42 43 totalforce += (float)math.pow( 44 event.values[0]/sensormanager.gravity_earth, 2.0); 45 totalforce += (float)math.pow( 46 event.values[1]/sensormanager.gravity_earth, 2.0); 47 totalforce += (float)math.pow( 48 event.values[2]/sensormanager.gravity_earth, 2.0); 49 50 totalforce = (float)math.sqrt(totalforce); 51 52 if (totalforce > max) 53 max = totalforce; 54 } 55 } 56 public void onaccuracychanged (Sensor sensor, int accuracy) { 57 } 58 }; 59 60 OnClickListener start_l = new OnClickListener() { 61 public void onclick(view v) { 62 if (isstarted == true) return; 63 isstarted = true; 64 max = (float)0.0; 65 smgr.registerlistener(mlistener, slist.get(0), 66 SensorManager.SENSOR_DELAY_UI); 67 tv.settext(""); 68 } 69 }; 70 71 OnClickListener stop_l = new OnClickListener() { 72 public void onclick(view v) { 73 if (isstarted == false) return; 74 isstarted = false; 75 smgr.unregisterlistener(mlistener, slist.get(0)); 76 if (max < 2.5) { 77 tv.settext("fail! Try again!"); 78 } else { 79 tv.settext("max: " + max);
80 } 81 } 82 }; 83 } 這個程式有兩個按鈕 : Start 按鈕: 其傾聽者是實作於 60~69 行, 其主要功能是啟動感測器的讀取, 並將最大值 (max) 的值做初始化 Stop 按鈕: 其傾聽者是實作於 71~82 行, 其主要功能是停止讀取感測器, 並檢查所測得的最大值 (max) 是否有大於 2.5, 若小於 2.5, 會告訴使用者其沒有盡全力晃動手機, 若大於 2.5, 則將測得的最大值告訴使用者 晃動值的計算是寫於感測器的傾聽者內 (35~58 行 ), 其主要是運用畢氏定理算出 總值 ( a 2 + b 2 + c 2 ), 這裡順便跟讀者點出一件事, 當手機歪斜著拿著不動時, 若把三軸的加速度值利用畢氏定理算出會得出接近 9.81( 重力加速度 ) 的數才對 讀者按下 Start 按鈕就可開始進行測詴, 而按下 Stop 按鈕就可觀看結果, 唯一要注意的是不要不小心把手機甩出去, 甚至砸到電視 下面分別是晃動成功 與失敗的執行結果畫面 : 14.4 摘要 本章將介紹了感測器的相關應用, 首先我們學會了如何獲得手機上的感測器清單, 並瞭解如何讀取感測器的感測值 接著我們分別學習了一個方位感測器 (Orientation Sensor) 與一個加速度感測器 (Accelerometer Sensor) 的應用 手機和感測器的結合, 讓手機產生更多的應用, 除了應用於遊戲軟體, 感測器也讓手機上
實作擴增實境變得更容易 14.5 作業 1. 寫一個程式來判斷手機的放置方式 : 平放 水平立放或垂直立放, 分別如下 圖所示 請使用加速度感測器 2. 寫一個程式來判斷手機的放置方式 : 平放 水平立放或垂直立放 這次請改 用方位感測器 3. 改寫 14.2 的程式, 選定兩個不同的目標物, 程式會出現兩個箭頭分別指向 兩個目標物 14.6 參考資料 [1] SensorManager Android Developers, http://developer.android.com/reference/android/hardware/sensormanager.html [2] Sensor Android Developers, http://developer.android.com/reference/android/hardware/sensor.html [3] SensorEventListener Android Developers, http://developer.android.com/reference/android/hardware/sensoreventlistener.html [4] SensorEvent Android Developers, http://developer.android.com/reference/android/hardware/sensorevent.html [5] Location Android Developers, http://developer.android.com/reference/android/location/location.html [6] ActivityInfo Android Developers,
http://developer.android.com/reference/android/content/pm/activityinfo.html [7] Canvas Android Developers, http://developer.android.com/reference/android/graphics/canvas.html [8] Paint Android Developers, http://developer.android.com/reference/android/graphics/paint.html