1,概述
现在人们越来越注重健康,相应的健康类app也是种类繁多,使用的最多的就是减肥健身类和计步跑步类的应用。而现在安卓上的计步无非就是利用手机自带的传感器,获取实时返回的数据后,再利用各自的算法过滤掉无效的步数,通过应用开启的服务保持后台持续进行,监测一整天的步行。
但因为市面上的安卓手机千差万别,不同的厂商定制出来的系统又是不同的,所以传感器的处理也是有所差别的,我们通过长时间收集用户反馈不断进行优化,本文将针对我们应用自带的计步功能实现和遇到的困难和解决方法一一详解,希望对读者有所帮助,也欢迎读者指出缺点互相改进。
2,实现
2.1 计步service
1、进到计步页的时候,我们需要启动一个service来保持后台计步,所以首先定义一个自定义的Service。(一开始设计的时候没有考虑到后台进程这些问题,所以启动service的方法是放在计步页的,后来证实不可取,后面的相关问题会谈到这个问题)
class StepService extends Service{}
开始计步服务
private void startStepService() {
Intent intent = new Intent(PedometerActivity.this, StepService.class);
Bundle bundle = new Bundle(); //intent带参数需要Bundle
bundle.putInt("op", 1);
intent.putExtras(bundle);
startService(intent);
}
上面启动service传递的参数是为了控制开始暂停的,下面会讲到。
2、绑定service,为了能够在计步页实时显示计步服务中拿回的步数,我们还需要在计步页绑定一下service,只需要在计步页绑定,退出计步页时解除绑定。
/**
* 这里绑定是为了计步页面实时显示service传回的步数
*/
private void bindStepService() {
bindService(new Intent(PedometerActivity.this, StepService.class), mConnection,
Context.BIND_AUTO_CREATE);
}
private StepService mService;
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
mService = ((StepService.StepBinder) service).getService();
mService.registerCallback(mCallback);
mService.reloadSettings();
}
public void onServiceDisconnected(ComponentName className) {
mService = null;
}
};
以下是service的回调
private StepService.ICallback mCallback = new StepService.ICallback() {
public void stepsChanged(int value) {
mHandler.sendMessage(mHandler.obtainMessage(STEPS_MSG, value, 0));
}
public void paceChanged(int value) {
mHandler.sendMessage(mHandler.obtainMessage(PACE_MSG, value, 0));
}
public void distanceChanged(float value) {
mHandler.sendMessage(mHandler.obtainMessage(DISTANCE_MSG, (int) (value * 1000), 0));
}
public void speedChanged(float value) {
mHandler.sendMessage(mHandler.obtainMessage(SPEED_MSG, (int) (value * 1000), 0));
}
public void caloriesChanged(float value) {
mHandler.sendMessage(mHandler.obtainMessage(CALORIES_MSG, (int) (value), 0));
}
@Override
public void timeChanged(String value) {
Message msg = new Message();
msg.what = TIME_MSG;
msg.obj = value;
mHandler.sendMessage(msg);
}
};
2.2 service中相关监听
3、 在计步service的onCreate()方法中初始化步数监听。
首先定义一个获取步数的接口:
public interface StepListener {
void onStep();
void passValue();
}
然后需要定义一些计步相关的监听类
StepDisplayer mStepDisplayer; //步数
DistanceNotifier mDistanceNotifier; //距离
SpeedNotifier mSpeedNotifier; //速度
CaloriesNotifier mCaloriesNotifier; //热量
以获取步数的监听类为例:
/** 将计算的步数传递给activity显示 */
public class StepDisplayer implements StepListener{
public static int mCount = 0;
public StepDisplayer(PedometerSettings settings){
notifyListener();
}
public void setSteps(int steps){
mCount = steps;
notifyListener();
}
@Override
public void onStep() {
mCount ++;
notifyListener();
}
@Override
public void passValue() {
}
public void reloadSettings(){
notifyListener();
}
public interface Listener{
void stepsChanged(int value);
void passValue();
}
private ArrayList<Listener> mListeners = new ArrayList<Listener>();
public void addListener(Listener l){
mListeners.add(l);
}
public void notifyListener(){
for(Listener listener:mListeners){
listener.stepsChanged(mCount);
}
}
}
然后需要设置相关的监听:
mStepDisplayer.addListener(mStepListener);
mStepDetector.addStepListener(mStepDisplayer);
(mStepDetector为StepDetector声明的对象,StepDetector类实现SensorEventListener接口)
所以直接在步数通知的回调中,可以显示处理拿回步数。
private StepDisplayer.Listener mStepListener = new StepDisplayer.Listener() {
public void stepsChanged(int value) {
mSteps = value;
//此处拿到每次的步数,做相应的处理
}
}
2.3 传感器的处理
service中设置相关的通知,方便实时拿回步数,但还没讲到传感器的处理,所以下面是介绍传感器获取步数的操作。
2.3.1 onStart()的不同处理
service的onStart()里接收不同的参数进行不同的处理。
@Override
public void onStart(Intent intent, int startId) {
if (intent != null) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
int op = bundle.getInt("op");
switch(op){
case 1: //开始
registerDetector();
break;
case 2: //暂停
unregisterDetector();
mNM.cancel(R.string.app_name);
break;
}
}
}
}
2.3.2 定义传感器相关类
SensorEventListener类是手机传感器的监听类,SensorManager类为传感器管理类;
2.3.3 注册传感器
mSensor = mSensorManager.getDefaultSensor(sensorType);
mSensorManager.registerListener(mStepDetector,
mSensor,
SensorManager.SENSOR_DELAY_NORMAL);
这里的mSensor为Sensor类。上面第三个参数为采样率:最快、游戏、普通、用户界面。当应用程序请求特定的采样率时,其实只是对传感器子系统的一个建议,不保证特定的采样率可用。
最快: SensorManager.SENSOR_DELAY_FASTEST
最低延迟,一般不是特别敏感的处理不推荐使用,该种模式可能造成手机电力大量消耗,由于传递的为原始数据,算法不处理好将会影响游戏逻辑和UI的性能。
游戏: SensorManager.SENSOR_DELAY_GAME
游戏延迟,一般绝大多数的实时性较高的游戏都使用该级别。
普通: SensorManager.SENSOR_DELAY_NORMAL
标准延迟,对于一般的益智类或EASY级别的游戏可以使用,但过低的采样率可能对一些赛车类游戏有跳帧现象。
用户界面: SensorManager.SENSOR_DELAY_UI
一般对于屏幕方向自动旋转使用,相对节省电能和逻辑处理,一般游戏开发中我们不使用。
2.3.4 两种传感器处理
4.4以上)自带计步传感器,可以直接在回调里拿回步数直接显示,不需要考虑步数是不是有效的,传感器本身已经帮你处理了。
Sensor.TYPE_ACCELEROMETER;//默认采用加速度传感器来处理,需要算法过滤无效步数
Sensor.TYPE_STEP_DETECTOR;//采用手机4.4以上自带计步处理器,不需要算法过滤无效步数
所以需要判断手机是不是自带内置计步器,相关代码如下:
/**
* 判断手机是否支持4.4以上自带计步处理器
*
* @return
*/
public void judgeIsSupportStepDetector() {
int sensorTypeC = Sensor.TYPE_STEP_COUNTER;
PackageManager pm = getPackageManager();
boolean flag = pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_STEP_DETECTOR);
if (flag) {
SensorManager mSensorManager = (SensorManager) this
.getSystemService(Context.SENSOR_SERVICE);
if (mSensorManager.getDefaultSensor(sensorTypeC) != null) {
//支持内置计步器
}
}
}
2.3.5 传感器的回调
的onSensorChange()方法里,根据注册传感器时的类型,分别判断是哪种传感器回调的数据。
方法里通过SensorEvent 获取类型和相关参数:
如果event.sensor.getType() == Sensor.TYPE_STEP_DETECTOR 则表示设置了内置计步器模式,判断event.values[0] == 1.0的话,调用我们前面定义的接口stepListener.onStep()本地步数直接加1;
如果event.sensor.getType() == Sensor.TYPE_ACCELEROMETER,表示采用加速度传感器,我们就不能像内置计步器那样直接本地步数加1,而是要通过自己的算法来过滤无效的步数。下面会介绍下过滤的算法的实现原理。
2.3.6 计步过滤算法实现原理(只是过滤传感器无效的振幅)
(只针对普通模式,也就是加速度传感器)
普通模式下,传感器会不断传回不同的值,这个时候我们需要设置相关条件去过滤掉这些无效的振幅,得出我们最后需要的值,也就是有效的那一步。
第一个条件:时间间隔不能太短。
定义一个最后的有效步数的时间a,默认是0毫秒,需满足最后 (1)(2)(3) 所有条件得出一个有效步数后才把当前时间赋给a,在每次传感器回调中获取当前时间b,第一个判断成立的条件是a-b > 90,即两次振幅的间隔必须大于90毫秒。
因为设置了灵敏度调节,所以这个时间90是可设置的,设置越小则灵敏度越高。
第二个条件:振幅速度不能太小。
speed :
long diff = now - mLastTime; (now 为当前时间,mLastTime为条件1的a)
float speed = Math.abs(event.values[SensorManager.DATA_X] + event.values[SensorManager.DATA_Y] + event.values[SensorManager.DATA_Z] - mLastX - mLastY - mLastZ) / diff * 10000;
,mLastY ,mLastZ 分别为x轴,y轴,z轴的值。
以下赋值只需要在第一个判断成立时:
mLastTime = now;
mLastX = event.values[SensorManager.DATA_X];
mLastY = event.values[SensorManager.DATA_Y];
mLastZ = event.values[SensorManager.DATA_Z];
默认速度speed 大于 60为条件2成立,因为设置了灵敏度调节,所以这个60是变化的,设置越小则灵敏度越高。
补充一点,介绍android 的坐标系是如何定义x, y ,z 轴的。
x轴的方向是沿着屏幕的水平方向从左向右,如果手机不是正方形的话,较短的边需要水平放置,较长的边需要垂直放置。
Y轴的方向是从屏幕的左下角开始沿着屏幕的的垂直方向指向屏幕的顶端。
将手机放在桌子上,z轴的方向是从手机指向天空。)
第三个条件:
同时满足 ((++mShakeCount >= 8) 且(now - mLastShake > 520) ,
这里mShakeCount 为有效震动次数,必须大于8才开始计算步数,什么情况下归零呢?
在条件一前面加多一个处理,
if ((now - mLastForce) > 2000) {
mShakeCount = 0;
}
这里的mLastForce为条件二成立时赋予当前时间,如果两次震动间隔大于2000毫秒,则上面的mShakeCount 归零。
mLastShake为上一次获取到有效步数时记录的时间,两次震动的最小时间间隔低于520毫秒则不算有效步伐。
根据上述三个条件同时依次成立,得出来的视为有效步数。
(请参考源代码)
到这里,大概计步的实现流程就结束了。但实际情况下,遇到的问题却是千奇百怪,单单是写这样的流程是无法满足我们的计步应用在后台长时间运行的。所以我们总结了用户的反馈后,整理出了以下的问题和相应的解决方法。
3, 遇到的问题及相应的解决办法
1、计步不准。
在以前的版本中,我们只是利用速度传感器去处理计步,而忽略了不同手机厂商速度传感器的敏感度时不同的,所以导致在相同的过滤算法下,不同手机计步会有很大偏差。
解决方法:
(1) 增加调节灵敏度调节入口,只针对速度传感器类型的,通过设置速度和两次有效步数时间间隔来提高或降低敏感度。
(2) 安卓4.4新增了一个新的传感器类型Sensor.TYPE_STEP_DETECTOR计步传感器,经测试发现注册这个类型的传感器,步数最为准确。后来发现不只是4.4的系统,4.4以上的也支持,但不是都支持。
(3) 增加普通模式和内置计步器模式,分别为速度传感器和计步传感器模式供用户自主选择。
2、锁屏不计步。
遇到过很多用户反馈锁屏后计步完全停滞了,检查发现并不是计步的服务停止了,而是传感器根本没响应,但屏幕亮了之后又能正常计步。有些手机厂商为了省电,会在锁屏下把一些传感器关闭或者敏感度降的极低。当然这是一两年前发现的问题,现在大部分手机应该不会有这个问题。
解决方法:
增加强制计步模式。也就是监听用户锁屏后,强制唤醒屏幕,但是屏幕亮度为最低,保证唤醒传感器的同时,不至于太耗电。这里不赘述强制唤醒屏幕的方法。
3、计步了一段时间后不计步了,要进入计步页才能恢复。
我们原来只是在计步页开启和绑定计步服务,而且并没有判断计步服务是否正常,所以导致应用的进程因为各种情况结束掉后,必须进入计步页才能恢复。
解决方法:
(1) 判断计步服务是否正常运行
代码如下:
/**
* 判断某个服务是否正在运行的方法
*
* @param mContext
* @param setpPakcageName 是包名+服务的类名(例net.loonggg.testbackstage.TestService)
* @return true代表正在运行,false代表服务没有正在运行
*/
public static boolean isStepServiceWork(Context mContext) {
boolean isWork = false;
ActivityManager myAM = (ActivityManager) mContext
.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningServiceInfo> myList = myAM.getRunningServices(40);
if (myList.size() <= 0) {
return false;
}
for (int i = 0; i < myList.size(); i++) {
String mName = myList.get(i).service.getClassName().toString();
if (mName.equals(setpPakcageName)) {
isWork = true;
break;
}
}
return isWork;
}
(2) 父类MainActivity类的onResume()判断计步服务是否正常,如果已经关闭了则重新启动服务,不需要进入计步页才启动;
(3) 推送消息的监听里判断计步服务,同上;
(4) 增加锁屏和解锁屏幕的广播,在广播里判断计步服务是否正常;
(5) 点击手机硬件menu键或home键,弹出正在运行的应用列表,找到我们的应用,下拉后松开,锁住应用,就能防止应用进程被系统杀掉。(最可靠)
4、计步过于灵敏,不走动摇晃手机都会计步。
10步后才会开始更新,如果走了几步又停下来一定时间之后,发现又得重新走大概10步才会更新步数。
解决方法:
新增一个稳定步数范围的算法,原理如下:
(1) 定义一个有效步数累计的对象startUsefulStep
(2) 每获取一个有效步数时记录下时间,两步间隔大于于5秒的话,重新开始过滤有效步数;
(3) 每步间隔小于5秒,startUsefulStep累加,连续走了10步;
/**
* 是否开始有效步数的计步
*/
public static boolean isStartUsefulStep(int step) {
boolean isStartPed = false;//是否正式开始计步
SharedPreferences mSetting = null;
PedometerSettings mPedometerSettings = null;
if (mSetting == null || mPedometerSettings == null) {
mSetting =
PreferenceManager.getDefaultSharedPreferences(ApplicationEx.getInstance());
mPedometerSettings = new PedometerSettings(mSetting, ApplicationEx.getInstance());
}
startUsefulStep += step;
//两步的间隔大于5秒,重新开始过滤无效步数
if ((new Date().getTime() - mPedometerSettings.getLastPedTime()) > 5000) {
startUsefulStep = 0;
}
mPedometerSettings.saveLastPedTime(new Date().getTime());
//每步间隔为5秒内,连续走了10步,视为正式开始计步
if (startUsefulStep >= 10) {
isStartPed = true;
}
return isStartPed;
}
这样的话,当执行完这个方法后结果为true时,要先在原步数上加10步再开始累加。
5、应用因为崩溃或其他原因导致程序退出,下次进来时步数不见了
9的倍数(这个数字自己定义,但是不能太大,最好不要超过50;也不要每一步就保存;最好也不要使用10的倍数,不然看起来每次恢复的步数都是10的倍数,会有点假。),是的话保存一次步数到文件,下次进来的时候判断一下文件里保存的步数。
4,未能解决的问题
1、进程被系统干掉。例如oppo,自带的安全中心-省电管理中,如果未将应用加入到可信赖的列表或者是关闭省电模式,锁屏后应用进程容易被杀掉;我们告知用户的操作方法是点击硬件menu键或home键,弹出正在运行的应用列表,找到我们的应用,下拉一次后松开锁住应用,就能防止应用进程被杀掉。
2、计步算法不够准确,不同手机上的结果还是有较大偏差;
3、当前的算法不支持智能识别跑步
5,总结
计步本身其实没什么太多的技术难点,复杂的是过滤有效振幅的算法(当前我们的算法还是比较弱智的水平),能够较智能(我不认为现在计步比较好的像乐动力,动动的算法就是百分百智能的)识别不同的场景。而且不同手机的传感器硬件也是不同的,所以可能相同的算法处理出来的逻辑又会有所不同。所以这个还需要不断的优化,但也从中更加认识安卓平台碎片化的严重性,也能更好的知道如何应对同设备带来的不同问题,从中也能了解到不同用户的使用习惯,更好的改进计步的功能。