效果最好的在文章最后給出,不想看分析的直接取最后的代碼即可擒权。
Android端的搖一搖功能現(xiàn)在使用十分廣泛垮刹,從最開始微信憑借搖一搖的功能大火,到現(xiàn)在很多的APP都具備搖一搖的功能培他。因此搖一搖的開發(fā)也變得十分廣泛,網(wǎng)上隨手一查也能查到很多關(guān)于搖一搖的開發(fā)代碼遗座。搖一搖幾乎都是根據(jù)Android自帶傳感的加速度傳感器來實(shí)現(xiàn)的靶壮,檢測到手機(jī)的加速度,然后做出相應(yīng)的邏輯判斷即可完成搖一搖的判定员萍。
但是每個(gè)手機(jī)加速度傳感器之間是有一定差距的腾降,加上手機(jī)性能和手機(jī)的軟件之間的不同,所以導(dǎo)致相同的代碼在不同的手機(jī)上的體驗(yàn)有一定的差距碎绎。我們應(yīng)當(dāng)在不增加太多設(shè)計(jì)復(fù)雜的的基礎(chǔ)上螃壤,爭取讓不同手機(jī)之間的體驗(yàn)達(dá)到一個(gè)比較接近的狀態(tài)。
我體驗(yàn)了微信的搖一搖功能筋帖,微信的搖一搖現(xiàn)在比較容易觸發(fā)奸晴,幾乎只需要搖一次就可以觸發(fā),但是由于微信本身只有進(jìn)入搖一搖界面才會(huì)開啟搖一搖的功能日麸,因此用戶進(jìn)入該界面就是想要搖一搖的寄啼,因此將搖一搖設(shè)置的比較敏感也是符合產(chǎn)品使用場景的。但是有些應(yīng)用具備全局的搖一搖功能代箭,那么此時(shí)就不應(yīng)該設(shè)計(jì)的太容易觸發(fā)墩划,這樣就會(huì)變成知乎那樣,被各種吐槽了嗡综。
如果單純通過增加加速度閾值來增加觸發(fā)難度乙帮,你會(huì)發(fā)現(xiàn)當(dāng)用戶真正想要搖一搖的時(shí)候會(huì)十分困難,可能就直接導(dǎo)致用戶不使用搖一搖功能极景,這樣意味著開發(fā)的這個(gè)功能完全失去了意義察净。這就需要我們從別的方向來解決這個(gè)問題了。
搖一搖的實(shí)現(xiàn)代碼
說了這么多盼樟,現(xiàn)在我們正式進(jìn)入搖一搖開發(fā)的實(shí)現(xiàn)氢卡。
上文也說道,現(xiàn)在的搖一搖都是通過Android的加速度傳感器來實(shí)現(xiàn)的晨缴。通過檢測加速译秦,來判斷用戶是否在進(jìn)行搖一搖操作。要獲取加速度傳感器的數(shù)據(jù)可以通過SensorManager的類來實(shí)現(xiàn)。
具體使用方法也比較簡單诀浪,就不細(xì)說直接給出一段簡單的代碼:
//獲取系統(tǒng)的SensorManager
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
// TODO: 添加自己的傳感器數(shù)據(jù)處理代碼;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};
//注冊傳感器監(jiān)聽事件
mSensorManager.registerListener(listener,
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_UI);
//注銷傳感器監(jiān)聽
mSensorManager.unregisterListener(listener);
需要注意的是registerListener(listener,
? mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
? SensorManager.SENSOR_DELAY_UI);當(dāng)中的SENSOR_DELAY_UI參數(shù)延都,這個(gè)參數(shù)表示傳感器數(shù)據(jù)變化通知的頻率雷猪,如果過快會(huì)造成性能和電量的消耗。官方提供了四個(gè)標(biāo)準(zhǔn)的參數(shù)
SENSOR_DELAY_FASTEST get sensor data as fast as possible
SENSOR_DELAY_GAME rate suitable for games
SENSOR_DELAY_NORMAL rate (default) suitable for screen orientation changes
SENSOR_DELAY_UI rate suitable for the user interface
不過這些不是十分準(zhǔn)確晰房,大多數(shù)時(shí)候還是需要我們在onSensorChanged(SensorEvent event)方法中通過時(shí)間做過濾才靠譜求摇,搖一搖功能使用SENSOR_DELAY_UI就行。當(dāng)然你也可以自己設(shè)定傳感器監(jiān)聽事件觸發(fā)頻率殊者,Android 2.3以上支持与境。
onSensorChanged(SensorEvent event)的處理
下面進(jìn)入本文最為關(guān)鍵的部分,onSensorChanged(SensorEvent event)的處理猖吴。從上面可以看到摔刁,不同傳感器的Listener都是同一個(gè)接口,包括兩個(gè)方法:onSensorChanged(SensorEvent event)海蔽,和onAccuracyChanged(Sensor sensor, int accuracy)共屈,我們只有大多數(shù)時(shí)候只用關(guān)心SensorEvent的處理即可。
/**
* This class represents a {@link android.hardware.Sensor Sensor} event and
* holds information such as the sensor's type, the time-stamp, accuracy and of
* course the sensor's {@link SensorEvent#values data}.
*/
public class SensorEvent {
public final float[] values;
/**
* The sensor that generated this event. See
* {@link android.hardware.SensorManager SensorManager} for details.
*/
public Sensor sensor;
/**
* The accuracy of this event. See {@link android.hardware.SensorManager
* SensorManager} for details.
*/
public int accuracy;
/**
* The time in nanosecond at which the event happened
*/
public long timestamp;
SensorEvent(int valueSize) {
values = new float[valueSize];
}
}
SensorEvent對象十分簡單党窜,不同的傳感器的事件都一樣拗引,只是value數(shù)組不一樣而已,因此我們主要在Listener當(dāng)中處理傳感器的數(shù)據(jù)即可幌衣。
加速度傳感器的返回?cái)?shù)據(jù)為X軸矾削、Y軸和Z軸方向的加速度。現(xiàn)在網(wǎng)上大多數(shù)搖一搖的代碼都是直接判斷3個(gè)方向的加速度是否達(dá)到某一個(gè)閾值豁护,如果達(dá)到那么觸發(fā)搖一搖事件哼凯。這樣的處理會(huì)就會(huì)出現(xiàn)之前討論的問題,要么很容易觸發(fā)楚里、要么太難觸發(fā)挡逼,導(dǎo)致功能白做。因此有必要對該方法進(jìn)行更多腻豌,更細(xì)致的處理家坎。
搖動(dòng)事件拆解分析
簡單的一次搖動(dòng)手機(jī)的事件可以分解為:
- 向某個(gè)方向加速運(yùn)動(dòng)然后速度達(dá)到最大
- 減速到速度為零隨后開始反向加速運(yùn)動(dòng)
- 反向加速到最大之后,再減速到速度為零
一次簡單的搖動(dòng)可以粗略的分解上面三個(gè)步驟吝梅,其中加速度最開始為正向(此時(shí)為正向加速過程)虱疏,隨后加速度反向(對應(yīng)減速和方向加速的過程),之后再次反向(對應(yīng)反向運(yùn)動(dòng)減速到正向加速的過程)苏携∽龅桑可以看出來搖動(dòng)手機(jī)的時(shí)候加速度不斷在變化方向。變化的頻率和我們晃動(dòng)的頻率正相關(guān),同時(shí)兩次加速方向應(yīng)該反向装蓬。但是根據(jù)參數(shù)我們可以看出來著拭,系統(tǒng)將加速度分解到三個(gè)方向,這樣描述加速度就包含了方向信息在里面牍帚。因此對應(yīng)兩次加速度的夾角如果接近180度儡遮,那么說明加速度方向進(jìn)行了一次轉(zhuǎn)向,所以可以對應(yīng)一次晃動(dòng)暗赶。如果一切都如預(yù)想中一樣鄙币,那么最為合理的代碼應(yīng)當(dāng)是判斷加速度反向,那么記錄一次反向蹂随,在一定時(shí)間內(nèi)完成設(shè)定次數(shù)的加速度反向十嘿,那么判定為一次搖動(dòng)。判定反向用到了空間中兩向量之間夾角的計(jì)算公式岳锁,最終的代碼如下:
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.timestamp - mLastTimestamp < MIN_TIME_BETWEEN_SAMPLES_NS) {
return;
}
float ax = sensorEvent.values[0];
float ay = sensorEvent.values[1];
float az = sensorEvent.values[2] - SensorManager.GRAVITY_EARTH;
Log.e(TAG, "ACCELEROMETER: " + ax +"+++"+ ay+"+++"+az);
if (Math.sqrt(ax*ax + ay*ay + az*az) > SENSOR_VALUE ){
// Log.e(TAG, "ACCELEROMETER: " + ax +"+++"+ ay+"+++"+az);
if (lastAz == 0 && lastAx == 0 && lastAy == 0){
lastAy += ay;
lastAz += az;
lastAx += ax;
return;
}
float product = ax*lastAx + ay*lastAy + az*lastAz;
float length = (float) (Math.sqrt(ax*ax + ay*ay + az*az) * Math.sqrt(lastAx*lastAx + lastAy*lastAy + lastAz*lastAz));
Log.e(TAG, "cos: "+ product/length);
if(product/length < -0.9){//cosA == -1時(shí)表示反向
Log.e(TAG, "cos: "+ product/length);
Log.e(TAG, "ACCELEROMETER: " + Math.sqrt(ax*ax + ay*ay + az*az));
Log.e(TAG, "ACCELEROMETER: " + ax +"+++"+ ay+"+++"+az);
lastAz = az;
lastAy = ay;
lastAx = ax;
recordShake(sensorEvent.timestamp);
}
}
if (sensorEvent.timestamp - lastShakeTimestamp > SHAKING_TIME_WINDOW){
reset();
}
}
問題
理想很豐滿绩衷,現(xiàn)實(shí)很骨感。由于手機(jī)的加速度傳感器默認(rèn)手機(jī)是水平放置在桌面的激率,此時(shí)Z軸減去重力加速度基本為零唇聘。但是用戶使用手機(jī)的時(shí)候手機(jī)的方位往往不是水平的,此時(shí)就會(huì)造成上述方法的完全失效柱搜。如果再考慮什么手機(jī)的擺放方位迟郎,那么問題將會(huì)變得十分復(fù)雜,顯得很沒必要聪蘸。因此大多數(shù)廠商采用了一種取巧的方式來實(shí)現(xiàn)宪肖,只要加速度傳感器的三個(gè)方向中有任意一個(gè)方向反向,那么直接判定為一次加速度反向健爬,在一段時(shí)間內(nèi)加速度反向達(dá)到一定次數(shù)之后就判定為用戶正在搖動(dòng)手機(jī)控乾。更改后的代碼如下:
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.timestamp - mLastTimestamp < MIN_TIME_BETWEEN_SAMPLES_NS) {
return;
}
float ax = sensorEvent.values[0];
float ay = sensorEvent.values[1];
float az = sensorEvent.values[2] - SensorManager.GRAVITY_EARTH;
mLastTimestamp = sensorEvent.timestamp;
if (Math.abs(ax) > REQUIRED_FORCE && ax * lastAX <= 0) {
recordShake(sensorEvent.timestamp);
lastAX = ax;
} else if (Math.abs(ay) > REQUIRED_FORCE && ay * lastAY <= 0) {
recordShake(sensorEvent.timestamp);
lastAY = ay;
} else if (Math.abs(az) > REQUIRED_FORCE && az * lastAZ <= 0) {
recordShake(sensorEvent.timestamp);
lastAZ = az;
}
maybeShake(sensorEvent.timestamp);
}