效果最好的在文章最后给出,不想看分析的直接取最后的代码即可。
Android端的摇一摇功能现在使用十分广泛,从最开始微信凭借摇一摇的功能大火,到现在很多的APP都具备摇一摇的功能。因此摇一摇的开发也变得十分广泛,网上随手一查也能查到很多关于摇一摇的开发代码。摇一摇几乎都是根据Android自带传感的加速度传感器来实现的,检测到手机的加速度,然后做出相应的逻辑判断即可完成摇一摇的判定。
但是每个手机加速度传感器之间是有一定差距的,加上手机性能和手机的软件之间的不同,所以导致相同的代码在不同的手机上的体验有一定的差距。我们应当在不增加太多设计复杂的的基础上,争取让不同手机之间的体验达到一个比较接近的状态。
我体验了微信的摇一摇功能,微信的摇一摇现在比较容易触发,几乎只需要摇一次就可以触发,但是由于微信本身只有进入摇一摇界面才会开启摇一摇的功能,因此用户进入该界面就是想要摇一摇的,因此将摇一摇设置的比较敏感也是符合产品使用场景的。但是有些应用具备全局的摇一摇功能,那么此时就不应该设计的太容易触发,这样就会变成知乎那样,被各种吐槽了。
知乎摇一摇吐槽
如果单纯通过增加加速度阈值来增加触发难度,你会发现当用户真正想要摇一摇的时候会十分困难,可能就直接导致用户不使用摇一摇功能,这样意味着开发的这个功能完全失去了意义。这就需要我们从别的方向来解决这个问题了。
摇一摇的实现代码
说了这么多,现在我们正式进入摇一摇开发的实现。
上文也说道,现在的摇一摇都是通过Android的加速度传感器来实现的。通过检测加速,来判断用户是否在进行摇一摇操作。要获取加速度传感器的数据可以通过SensorManager的类来实现。
具体使用方法也比较简单,就不细说直接给出一段简单的代码:
//获取系统的SensorManager
mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
SensorEventListener listener = new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
// TODO: 添加自己的传感器数据处理代码;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};
//注册传感器监听事件
mSensorManager.registerListener(listener,
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_UI);
//注销传感器监听
mSensorManager.unregisterListener(listener);
需要注意的是registerListener(listener,
mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
SensorManager.SENSOR_DELAY_UI);当中的SENSOR_DELAY_UI参数,这个参数表示传感器数据变化通知的频率,如果过快会造成性能和电量的消耗。官方提供了四个标准的参数
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
不过这些不是十分准确,大多数时候还是需要我们在onSensorChanged(SensorEvent event)方法中通过时间做过滤才靠谱,摇一摇功能使用SENSOR_DELAY_UI就行。当然你也可以自己设定传感器监听事件触发频率,Android 2.3以上支持。
onSensorChanged(SensorEvent event)的处理
下面进入本文最为关键的部分,onSensorChanged(SensorEvent event)的处理。从上面可以看到,不同传感器的Listener都是同一个接口,包括两个方法:onSensorChanged(SensorEvent event),和onAccuracyChanged(Sensor sensor, int accuracy),我们只有大多数时候只用关心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数组不一样而已,因此我们主要在Listener当中处理传感器的数据即可。
加速度传感器的返回数据为X轴、Y轴和Z轴方向的加速度。现在网上大多数摇一摇的代码都是直接判断3个方向的加速度是否达到某一个阈值,如果达到那么触发摇一摇事件。这样的处理会就会出现之前讨论的问题,要么很容易触发、要么太难触发,导致功能白做。因此有必要对该方法进行更多,更细致的处理。
摇动事件拆解分析
简单的一次摇动手机的事件可以分解为:
- 向某个方向加速运动然后速度达到最大
- 减速到速度为零随后开始反向加速运动
- 反向加速到最大之后,再减速到速度为零
一次简单的摇动可以粗略的分解上面三个步骤,其中加速度最开始为正向(此时为正向加速过程),随后加速度反向(对应减速和方向加速的过程),之后再次反向(对应反向运动减速到正向加速的过程)。可以看出来摇动手机的时候加速度不断在变化方向。变化的频率和我们晃动的频率正相关,同时两次加速方向应该反向。但是根据参数我们可以看出来,系统将加速度分解到三个方向,这样描述加速度就包含了方向信息在里面。因此对应两次加速度的夹角如果接近180度,那么说明加速度方向进行了一次转向,所以可以对应一次晃动。如果一切都如预想中一样,那么最为合理的代码应当是判断加速度反向,那么记录一次反向,在一定时间内完成设定次数的加速度反向,那么判定为一次摇动。判定反向用到了空间中两向量之间夹角的计算公式,最终的代码如下:
@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时表示反向
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();
}
}
问题
理想很丰满,现实很骨感。由于手机的加速度传感器默认手机是水平放置在桌面的,此时Z轴减去重力加速度基本为零。但是用户使用手机的时候手机的方位往往不是水平的,此时就会造成上述方法的完全失效。如果再考虑什么手机的摆放方位,那么问题将会变得十分复杂,显得很没必要。因此大多数厂商采用了一种取巧的方式来实现,只要加速度传感器的三个方向中有任意一个方向反向,那么直接判定为一次加速度反向,在一段时间内加速度反向达到一定次数之后就判定为用户正在摇动手机。更改后的代码如下:
@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);
}