編輯:關於android開發
作者:鄭童宇
GitHub:https://github.com/CrazyZty
現在市面上有不少Android手機支持敲擊屏幕解鎖,敲擊屏幕解鎖是一項很實用的功能,但一來只支持敲擊屏幕,二來只能用於解鎖或鎖屏,再者我們應用層的開發者切不進去,完全無法玩起來。開發者,開發者,我們既然身為開發者何不搞點大新聞,那麼這次我來教教各位如何用代碼來實現手機的敲擊識別,聽起來是不是很有趣,有些躍躍欲試呢。事實上在ios上已經有實現這個功能的應用:Knock,一款敲擊來解鎖Mac電腦的應用,售價4.99美元,約為33人民幣。有時候真想去做ios開發,可以開心的為自己的應用定價,愉悅的掙外快。言歸正傳,既然ios可以實現,那我們Android自然不能落伍,現在我就帶領大家來用代碼實現手機的敲擊識別吧。
本篇博文以Java為示例語言,以Android為示例平台。
說到敲擊識別,你們會考慮使用什麼來實現呢,傳感器?對,沒錯,作為手機手勢姿態識別的唯一途徑,我們自然需要使用傳感器來實現對敲擊的識別,但Android傳感器種類繁多,我們應該選擇哪一個呢?
在Android2.3的時代,Android系統就已經定義了11個傳感器,到了現在Android6.0的時代,系統定義的傳感器數目已經達到26個,這麼多傳感器我們到底用哪一個呢,事實上我們只需要考慮2.3時代提供的那11個傳感器即可,因為一方面後期加入的傳感器部分如心跳傳感器等需要硬件支持,導致很多手機無法支持此類傳感器,另一方面2.3時代的11個傳感器功能已經相當強大,可以支持絕大多數手勢姿態的識別,那麼現在我來列舉一下上述11個傳感器:
SENSOR_TYPE_ACCELEROMETER 加速度
SENSOR_TYPE_MAGNETIC_FIELD 磁力
SENSOR_TYPE_ORIENTATION 方向
SENSOR_TYPE_GYROSCOPE 陀螺儀
SENSOR_TYPE_LIGHT 光線感應
SENSOR_TYPE_PRESSURE 壓力
SENSOR_TYPE_TEMPERATURE 溫度
SENSOR_TYPE_PROXIMITY 接近
SENSOR_TYPE_GRAVITY 重力
SENSOR_TYPE_LINEAR_ACCELERATION 線性加速度
SENSOR_TYPE_ROTATION_VECTOR 旋轉矢量
關於這11個傳感器的詳細描述,各位可以去http://www.oschina.net/question/163910_28354查看,事實上我一直懷疑LG G3的敲擊解鎖與光線傳感器或接近傳感器有關,因為我用手指懸浮在LG G3的頭部正上方時一直無法敲擊解鎖,移開後恢復正常,而敲擊鎖屏應該只和觸摸屏相關,因為無論我怎麼遮擋傳感器,敲擊鎖屏的功能完全不受影響。
言歸正傳,對這11個傳感器有所了解後,我們需要選擇哪個或哪些傳感器來實現功能呢,我們來模擬一下手機敲擊的情況,將手機平放在桌面上,手指敲擊手機的時候,手指給了手機一個力,同時桌面給予手機一個反作用力,考慮桌面不形變的情況下,手機受力平衡加速度為0,但這時手機的加速度傳感器數據是否會有變化呢,答案是會的,手機加速度傳感器的數據會有一段短暫但明顯的變化,為什麼呢,手機受力平衡加速度為0是因為它是一個整體,但內部構件還是會受到相互之間復雜的力的左右,並非受力的同時就達到受力平衡的,其實換個思路。用一個和手機形狀相似內部光滑的容器,容器裡面放幾個玻璃球,敲擊幾下,容器不會移動,但玻璃球是不是移動了呢。雖然手機內部的構件遠比玻璃球穩定,但也得遵循基本法,老老實實接受力的作用。
上述場景是平放於桌面的場景,實際生活的場景往往更加復雜多樣,但無論處於哪種場景,毫無疑問對手機的敲擊操作都應該導致加速度傳感器傳出數據的明顯變化,那麼我們現在就明白了應該選擇什麼傳感器作為我們敲擊識別的工具了吧,但加速度相關的傳感器有兩個,加速度傳感器和線性加速度傳感器,我們應該選擇哪一個呢,加速度傳感器提供的數據是重力影響下的手機加速度,線性加速傳感器提供的數據是排除重力影響的手機加速度,可以直觀的反映排除重力後手機的受力情況,很合適用以敲擊識別,那我們是否就應該選擇線性加速度傳感器呢,恰恰相反,我們要選擇加速度傳感器,Android提供的線性加速度傳感器基於軟件的,不同平台對於線性加速傳感器的處理未必相同,事實上,在敲擊三星S4,LG G3中一款機型的背面,就出現線性加速度傳感器傳出的數據沒有較大變化的情況,保險起見,我們還是選用基於硬件的加速度傳感器更合適一些。順便吐槽一句,當時看到壓力傳感器的時候,我還以為監測作用於手機的壓力的傳感器,那無疑是很適合用於識別敲擊,後面看到描述才知道是監測壓強的。
如上所說,對手機的敲擊操作會導致加速度傳感器傳出數據的明顯變化,故而本次功能實現中,判斷是否有敲擊操作的方法是檢測手機線性加速度相比正常情況是否有明顯變化。在功能實現過程中為排除重力的影響,需要對加速度傳感器的數據進行處理將其轉化為線性加速度,因為轉化為線性加速度是一個需要校准的過程,所以需要先投入一定數目的數據用於校准以獲得更精確的線性加速度,同時考慮到現實生活存在可能導致誤識別的場景,比如搖動手機會帶給手機一個較長時間且明顯的線性加速度變化,所以提出穩態的概念,將手機處於相對穩定,沒有長時間出現明顯線性加速變化的狀況視為穩態,在穩態的情況下才會進行對敲擊的識別,另外此次敲擊識別考慮到對手機邊框的敲擊使用可能性過低,因此僅考慮識別對手機屏幕或背面的敲擊,這樣在識別的過程中可忽略X,Y軸的數據,僅考慮Z軸的線性加速度。
本次實現的功能是識別對手機屏幕或背面的敲擊操作,功能實現流程: 注冊傳感器,采集數據,投入指定數目的數據校准以獲取較精准的線性加速度,校准結束後判斷當前是否穩態,如果為非穩態,則等待下次數據,如果為穩態,則調用方法判斷是否存在敲擊操作,在進行敲擊識別的同時也將處理得到的線性加速度和最近敲擊次數,穩態狀態顯示到界面上去,
注冊傳感器的方法屬於系統原生的方法,就不過多講解,不過需要注意一點,在注冊加速度傳感器時標識傳感器數據采樣間隔的參數最好使用SENSOR_DELAY_GAME,因為敲擊導致的加速度數據變化很短暫,如果使用SENSOR_DELAY_UI或SENSOR_DELAY_NORMAL往往采集不到敲擊引發的加速度變化,當然如果使用SENSOR_DELAY_FASTEST自然不會有這個問題,但性能消耗會比較大。
注冊傳感器後就可以在回調方法裡等待處理數據, 下面我給出實現代碼,綜合代碼講解實現過程。
1 public void onSensorChanged(SensorEvent sensorEvent) { 2 if (sensorEvent.sensor == null) { 3 return; 4 } 5 6 if (sensorEvent.sensor.getType() == accelerometerSensorType) { 7 float accelerationZ = sensorEvent.values[2]; 8 9 if (accelerationZ > 0) { 10 recognitionKnockRatio = 20; 11 recognitionUniqueRatio = 10; 12 13 smoothSectionMaxRatio = 5f; 14 } else { 15 recognitionKnockRatio = 7.5f; 16 recognitionUniqueRatio = 6; 17 18 smoothSectionMaxRatio = 2.5f; 19 } 20 21 gravityZ = alpha * gravityZ + (1 - alpha) * accelerationZ; 22 23 linearAccelerationZ = accelerationZ - gravityZ; 24 25 if (calibrateLinearAcceleration) { 26 calibrateLinearAccelerationIndex++; 27 28 if (calibrateLinearAccelerationIndex <= calibrateLinearAccelerationSectionNumber) { 29 return; 30 } 31 32 calibrateLinearAcceleration = false; 33 } 34 35 if (sensorDataShowIndex >= sensorDataShowNumber) { 36 sensorDataShowIndex = sensorDataShowNumber - sensorDataShowDurationNumber; 37 38 Iterator<?> it = linearAccelerationZShowList.listIterator(0); 39 for (int i = 0; i < sensorDataShowDurationNumber; i++) { 40 it.next(); 41 it.remove(); 42 } 43 44 MainActivity.UpdateSensorData(linearAccelerationZShowList); 45 } 46 47 linearAccelerationZShowList.add(linearAccelerationZ); 48 49 sensorDataShowIndex++; 50 51 if (!stable) { 52 linearAccelerationZList.add(linearAccelerationZ); 53 54 if (linearAccelerationZList.size() >= stableSectionNumber) { 55 stableRecognition(); 56 57 linearAccelerationZList.clear(); 58 } 59 60 return; 61 } 62 63 knockRecognition(linearAccelerationZ); 64 } 65 }
傳感器數據回調的方法中對加速度傳感器獲取的數據分別進行了處理,首先,根據z軸加速度的正負,為recognitionKnockRatio,recognitionUniqueRatio,smoothSectionMaxRatio三個變量賦予不同的數值,至於為什麼要進行這樣處理,是因為對Android手機實際進行敲擊操作發現,加速度傳感器對正面敲擊操作反饋敏感,對背面敲擊操作反饋相對遲鈍,反饋到數據層面就是,敲擊正面導致的加速度傳感器數據變化相比敲擊背面明顯很多,故而針對敲擊屏幕和敲擊背面要分配不同的數值,然而事實上站在手機的角度,運用現在的數據是完全無法分析敲擊操作導致的加速度明顯變化來源於敲擊正面還是敲擊背面,所以就使用z軸加速度的正負來簡單判斷,畢竟絕大多數情況下z軸加速度為正,那就是手機背面偏向地面,用戶更可能敲擊手機屏幕,而為負就是手機屏幕偏向地面,用戶更可能敲擊手機背面。至於導致敲擊屏幕和敲擊背面加速度傳感器反饋敏感程度不同這種情況的原因不外乎兩個,一是加速度傳感器相比於背面距離屏幕更近,再者就是Android手機外殼的問題了,這一點在LG G3上尤為明顯,LG G3的是有一定弧度的塑料外殼,在背面敲擊引發的傳感器數據變化相比於敲擊屏幕要低很多,而金屬外殼的三星S6,在背面敲擊引發的傳感器數據變化接近於敲擊屏幕。事實上上述三個系數屬於經驗系數,並且對於不同類型手機盡量提供不同的數值,原因可參見剛才所說的LG G3和三星S6,再一次感慨Android手機的多樣性,Android手機種類太多,硬件設計的不同導致在一款手機上適用的系數在另一款手機上可能完全無法適用,要是如iphone一樣只有那幾款機型的話無疑好處理很多。
接著對加速度進行濾波處理以獲取線性加速度,獲取線性加速度的方法參考了Android SensorEvent源碼中建議的方法:
1 * <p> 2 * It should be apparent that in order to measure the real acceleration of 3 * the device, the contribution of the force of gravity must be eliminated. 4 * This can be achieved by applying a <i>high-pass</i> filter. Conversely, a 5 * <i>low-pass</i> filter can be used to isolate the force of gravity. 6 * </p> 7 * 8 * <pre class="prettyprint"> 9 * 10 * public void onSensorChanged(SensorEvent event) 11 * { 12 * // alpha is calculated as t / (t + dT) 13 * // with t, the low-pass filter's time-constant 14 * // and dT, the event delivery rate 15 * 16 * final float alpha = 0.8; 17 * 18 * gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0]; 19 * gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1]; 20 * gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2]; 21 * 22 * linear_acceleration[0] = event.values[0] - gravity[0]; 23 * linear_acceleration[1] = event.values[1] - gravity[1]; 24 * linear_acceleration[2] = event.values[2] - gravity[2]; 25 * } 26 * </pre>
通過高通濾波和低通濾波對加速度進行處理排除重力影響以獲取線性加速度,但在此過程中是需要傳入一定數量的數據進行校准以獲取較精准的線性加速度,在這裡我們設定calibrateLinearAccelerationSectionNumber作為用以校准數據的數據長度,用calibrateLinearAccelerationIndex和calibrateLinearAcceleration來控制何時校准結束。
校准結束後使用linearAccelerationZShowList存儲顯示到應用界面上的傳感器線性加速度,接著如果處於非穩態,則開始穩態識別,判斷當前狀態是否穩態,如果處於穩態狀態則開始敲擊識別。
如上文提到的,用戶如果進行搖動手機之類的操作,是會產生明顯的加速度變化,很有可能導致誤識別的情況,所以在此提出了穩態的概念,即為手機加速度沒有長時間明顯變化的狀態,延伸到現實場景就是用戶沒有對手機進行明顯移動的狀態,嚴格來說,一般用戶在對手機進行明顯移動如搖動手機的同時進行敲擊操作的可能性極低,所以可以將穩態這個概念正式運用到功能實現中。
已經了解穩態這個概念,那我們應該如何定義什麼情況屬於穩態,什麼情況屬於非穩態,下面我給出實現代碼,綜合代碼進行講解。
1 private void stableRecognition() { 2 int exceptionNumber = 0; 3 4 float accelerationZValue; 5 float minAccelerationZValue = Integer.MAX_VALUE; 6 float maxAccelerationZValue = Integer.MIN_VALUE; 7 8 for (int i = stableSectionNumber - 1; i >= 0; i--) { 9 accelerationZValue = linearAccelerationZList.get(i); 10 11 if (Math.abs(accelerationZValue) > maxStableOffset) { 12 exceptionNumber++; 13 } else { 14 if (accelerationZValue > maxAccelerationZValue) { 15 maxAccelerationZValue = accelerationZValue; 16 } else { 17 if (accelerationZValue < minAccelerationZValue) { 18 minAccelerationZValue = accelerationZValue; 19 } 20 } 21 } 22 } 23 24 stable = exceptionNumber <= maxExceptionNumber; 25 26 if (stable) { 27 if (linearAccelerationZStableSection == 0) { 28 linearAccelerationZStableSection = 29 (maxAccelerationZValue - minAccelerationZValue) / 2; 30 } 31 32 if (linearAccelerationZStableSection > maxStableOffset) { 33 linearAccelerationZStableSection = maxStableOffset; 34 } 35 } 36 37 MainActivity.UpdateStable(stable); 38 39 LogFunction.log("stable", "" + stable); 40 LogFunction.log("exceptionNumber", "" + exceptionNumber); 41 LogFunction.log("linearAccelerationZStableSection", "" + linearAccelerationZStableSection); 42 }
在此次功能實現過程中,判斷穩態的方式是采樣50個點,然後計算每個點的絕對值,如果大於最大偏差maxStableOffset就視為異常點,異常點大於最大異常點數目maxExceptionNumber就視為非穩態,反之視為穩態。判斷穩態結束後,如果處於穩態則將剔除異常點數據後的Z軸最大加速度和最小加速度之間差值的一半視為波動區間linearAccelerationZStableSection。maxStableOffset與maxExceptionNumber相同都是經驗系數,是對Android手機實際提供的不同場景下的線性加速度分析得出的。現在存在一個問題,那就是如果原本狀態處於穩態,然後用戶突然對手機進行操作,將手機狀態轉變為非穩態那要如何處理,不要著急,這個問題會在敲擊識別的過程中進行處理的。
現在到了整個功能實現最核心的地方:敲擊識別,如上文所說敲擊會引起加速度傳感器數據的明顯變化,但是我們要如何使用代碼進行檢測敲擊,以及如何排除用戶對手機其他操作引發的誤識別問題,事實上這些問題都會在這裡進行處理,現在我給出實現代碼,綜合代碼進行講解。
1 private void knockRecognition(float linearAccelerationZ) { 2 float linearAccelerationZAbsolute = Math.abs(linearAccelerationZ); 3 4 float linearAccelerationZAbsoluteRadio = 5 linearAccelerationZAbsolute / linearAccelerationZStableSection; 6 7 if (linearAccelerationZAbsoluteRadio > recognitionUniqueRatio) { 8 uniqueLinearAccelerationZList.add(linearAccelerationZ); 9 10 currentForecastNumber = forecastNumber; 11 } else { 12 if (uniqueLinearAccelerationZList.size() > 0) { 13 if (currentForecastNumber > 0) { 14 currentForecastNumber--; 15 } else { 16 handleUniqueLinearAccelerationZ(); 17 } 18 } 19 } 20 21 if (linearAccelerationZAbsoluteRadio < smoothSectionMaxRatio) { 22 float offsetWeight = 0.001f; 23 24 linearAccelerationZStableSection = 25 weightedMean(offsetWeight, linearAccelerationZAbsolute, 26 linearAccelerationZStableSection); 27 } 28 }
knockRecognition就是用來處理線性加速度進而確認是否有敲擊操作的方法,首先對傳入參數線性加速度進行處理,獲取線性加速度絕對值,接著如果線性加速度絕對值與波動區間的比值大於recognitionUniqueRatio,那就認為手機正在受到力的作用,為確定是敲擊操作還是用戶其他操作,先將線性加速度加入到獨特線性加速度列表中, 反之如果小於等於recognitionUniqueRatio,那就認為手機處於相對穩定狀態,在此時如果此時獨特線性加速度列表長度大於0,如果currentForecastNumber大於0,則currentForecastNumber減1,如果currentForecastNumber小於等於0,則開始處理獨特線性加速度列表,而在處理獨特線性加速度列表的過程中正式開始識別是否敲擊,以及當前狀態是否轉變為非穩態。在進行上述操作的同時,如果線性加速度絕對值與波動區間的比值小於smoothSectionMaxRatio則用線性加速度絕對值來平滑波動區間。
在這裡,大家肯定對currentForecastNumber有疑問,這個變量代表什麼含義,為什麼會有這個變量,原因是這樣的,一次敲擊可能導致兩個接近但不連續的獨特線性加速度。如果沒有currentForecastNumber這個變量就會導致現實的一次敲擊可能被識別為兩次敲擊操作。
而如果線性加速度絕對值與波動區間的比值小於smoothOffsetMaxRatio則用線性加速度絕對值來平滑波動區間,是因為一方面手機的狀態可能隨時改變,波動區間應該隨著手機狀態的改變跟著改變,另一方面穩態識別時計算的波動區間可能存在問題,並不能正確的反映當前手機的加速度波動,這個時候就需要根據最新的數據進行學習以平滑波動區間,而為什麼比值要小於smoothSectionMaxRatio是因為比值大於smoothSectionMaxRatio的基本是非正常情況的線性加速度,不適合用於平滑波動區間,而如果現實情況中的線性加速度與波動區間比值基本都超過smoothSectionMaxRatio,那說明現在手機多半處於非穩態狀態了,等待新的穩態識別重置波動區間即可,另外如上文所說,recognitionUniqueRatio,smoothOffsetMaxRatio屬於經驗系數,完全可以自主設定。
1 private void handleUniqueLinearAccelerationZ() { 2 LogFunction.log("linearAccelerationZStableSection", "" + linearAccelerationZStableSection); 3 4 int recognitionKnockNumber = 1; 5 6 int uniqueLinearAccelerationZListLength = uniqueLinearAccelerationZList.size(); 7 8 float accelerationZOffsetAbsolute; 9 float maxAccelerationZOffsetAbsolute = 0; 10 11 for (int i = 0; i < uniqueLinearAccelerationZListLength; i++) { 12 accelerationZOffsetAbsolute = Math.abs(uniqueLinearAccelerationZList.get(i)); 13 14 if (maxAccelerationZOffsetAbsolute < accelerationZOffsetAbsolute) { 15 maxAccelerationZOffsetAbsolute = accelerationZOffsetAbsolute; 16 } 17 18 LogFunction.log("uniqueLinearAccelerationZList index" + i, 19 "" + uniqueLinearAccelerationZList.get(i)); 20 } 21 22 uniqueLinearAccelerationZList.clear(); 23 24 LogFunction.log("uniqueLinearAccelerationZListLength", 25 "" + uniqueLinearAccelerationZListLength); 26 27 if (uniqueLinearAccelerationZListLength > unstableListLength) { 28 stable = false; 29 MainActivity.UpdateStable(stable); 30 return; 31 } 32 33 LogFunction.log("maxAccelerationZOffsetAbsolute / linearAccelerationZStableSection", 34 "" + (maxAccelerationZOffsetAbsolute / linearAccelerationZStableSection)); 35 36 if (maxAccelerationZOffsetAbsolute > 37 linearAccelerationZStableSection * recognitionKnockRatio) { 38 LogFunction.log("recognitionKnockRatio", "" + recognitionKnockRatio); 39 LogFunction.log("recognitionUniqueRatio", "" + recognitionUniqueRatio); 40 41 knockRecognitionSuccess(recognitionKnockNumber); 42 } 43 }
終於到了最後的handleUniqueLinearAccelerationZ方法,顧名思義,就是用來處理獨特線性加速度列表的,在這個方法內,進行了敲擊識別和穩態狀態是否轉變的判定,如果獨特線性加速度列表長度超過非穩態獨特線性加速度列表長度,則認為現在手機狀態此刻狀態轉變為非穩態並結束方法,如果發現加速度偏移數據列表中最大偏移值超過波動區間一定倍數則識別為敲擊。
至此,敲擊識別的流程我們算是走完了。事實上我提供的敲擊識別方法還是存在著誤識別的情況,ios的Knock我使用過,擁有著符合價格的能力,識別率相當的好,不知道他們是通過機器學習還是別的方法歸結了一套他們的識別系數,當然我在此提供的敲擊識別僅僅是一種敲擊識別的方法,我也無法說它成熟,因為並沒有經過真正用戶的考驗,大家完全可以按照自己的思想更換算法甚至更換傳感器來實現自己的敲擊識別,而我在此其實相當於提供一個實現思路。
這是第三篇博客了,第一篇博客屬於試水就選擇了做過的一個比較偏門但並不好處理的一個小模塊:為MP3文件寫入ID3標簽,第二篇博客選擇了一個很嚴謹的實用模塊:音頻合成,前兩個模塊都有一個共同點就是各種規范已經很明確,雖然代碼實現上可能有所不同但實現思路必然相同,而第三篇的博客的敲擊檢測無疑寬松很多,所以我也是第一次寫了實現思路這一小節,因為我也不確定我的實現思路是否完全正確,作為傳感器的實際應用是存在著無數的可能性,我們完全可以按照自己的想法去嘗試,錯了大不了換一個方向罷了。
另外博客或者代碼中如果存在什麼問題,歡迎各位朋友們提出來。
這篇博文就到這裡結束了,本文所有代碼已經托管到https://github.com/CrazyZty/KnockDetect,大家可以自由下載。
Android IPC 之 AIDL(一) IPC是Inter-Process Communication的縮寫,即跨進程通信。Android中跨進程通信有多種方式,如文
Android開發學習之路--Camera之初體驗 顧名思義Camera就是拍照和錄像的功能,像微信裡面,我們想拍照傳一下照片,就可以通過camera來拍照,然後存儲
百度地圖可視化定位效果,可以輸入目的地定位。,可視化目的地登錄百度開發者帳號後下載sdk導入自己的工程中。 代碼如下: 1 package com.lixu.bai
安卓第六天筆記--ListView,安卓--listview安卓第六天筆記--ListView 1.AdapteView AdapteView 繼承ViewGroup它的