最近需要做一个计步程序,在网站上研究了一些别人写的程序代码,比较普遍实用的是根据API大小,使用Google内置计步器或加速度传感器进行计步。但是网上源代码的注释很少,经过了一番波折,自己终于有了头绪,并且对计步程序做了一些改进。为了让大家能更快的理解计步原理,现在分享一下自己的一些经验,希望对大家有所帮助
先看一下效果:
(PS:这是我从整个程序中抽出来的一部分,只实现了计步功能,一些其他的控件我已经去掉了,便于大家理解计步的原理。想要美化界面的同胞们可以自行动手哈)
一、程序设计的整体思路:
在主Activity中开启一个服务,在服务中注册一个广播接收者监听屏幕的电源情况保存数据。并且新建子线程,在子线程里开启计步检测,根据不同API版本获得不同传感器管理器和传感器实例并注册监听,如果使用的是Google内置计步器则在重写的onSensorChanged()方法中计算步数,如果使用的是加速度传感器则根据传感器得到的数据,计算波峰波谷阈值等数据,符合要求即为一步(具体条件下面会介绍)。之后更新通知栏和界面。
二、计步使用到的传感器以及对应的计步方法:
在服务中开启计步检测会对API进行一个判断如果API>19则调用CountSensor,因为android4.4以后可以使用Google内置计步器。对于API<=19的手机可以使用加速度传感器。两种传感器实现计步的方法:
<1>Google内置计步器:这个用起来就非常方便了注册监听后直接重写onSensorChanged方法,每检测到人走一步就会调用这个方法。为了使当天走的步数尽可能准确,减少关闭程序导致无法计步造成的影响。我在Google内置计步器事件中可以得到一个值(event.value[0]),该值记录的是这个月目前所走的步数,我首先会记录一天中开始时候这个值的大小,然后计算出今天走的步数公式:今天走的步数=传感器当前统计的步数-之前统计的步数
(PS:如果想简单实现计步可以直接在onSensorChanged()方法中将步数+1就可以了)
<2>加速度传感器:使用这个传感器时会检测传感器的变化,得到传感器三轴的值(x,y,z)然后计算他们的平均值,这样做的目的是为了平衡在某一个方向数值过大造成的数据误差,然后将该值与上一时间点的值进行比较,判断是否为波峰或波谷,如果是就相应的保存下来。如果检测到了波峰,并且符合时间差以及阈值的条件,则判定位1步,如果符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中。同时为防止微小震动对计步的影响,我们将计步分为3个状态——准备计时、计时中、计步中。所谓“计时中”是在3.5秒内每隔0.7秒对步数进行一次判断,看步数是否仍然在增长,如果不在增长说明之前是无效的震动并没有走路,得到的步数不计入总步数中;反之则将这3.5秒内的步数加入总步数中。之后进入“计步中”状态进行持续计步,并且每隔2秒去判断一次当前步数和2秒前的步数是否相同,如果相同则说明步数不在增长,计步结束。为了更直观的理解,附上一张图(原谅我的盗图)
三、代码说明:
1.先来看一下MainActivity:
在onCreate方法中初始化Handler,onStart方法中通过setupService方法开启一个服务StepService,这样可以使程序即使退到后台再到前台时也能开启服务@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text_step = (TextView) findViewById(R.id.main_text_step);
delayHandler = new Handler(this);
}
@Override
public void onStart() {
super.onStart();
setupService();
}
/**
* 开启服务
*/
private void setupService() {
Intent intent = new Intent(this, StepService.class);
bindService(intent, conn, Context.BIND_AUTO_CREATE);
startService(intent);
}
开启服务中使用了bind形式、故有ServiceConnection接收回调。在onServiceConnected方法里发送一个消息。
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
messenger = new Messenger(service);
Message msg = Message.obtain(null, Constant.MSG_FROM_CLIENT);
msg.replyTo = mGetReplyMessenger;
messenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {}
};
在handleMessage中接收从服务端回调的步数,并显示在界面上:
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case Constant.MSG_FROM_SERVER:
//更新步数
text_step.setText(msg.getData().getInt("step") + "");
delayHandler.sendEmptyMessageDelayed(Constant.REQUEST_SERVER, TIME_INTERVAL);
break;
case Constant.REQUEST_SERVER:
try {
Message msgl = Message.obtain(null, Constant.MSG_FROM_CLIENT);
msgl.replyTo = mGetReplyMessenger;
messenger.send(msgl);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
return false;
}
2.接下来分析开启的StepService:
同理,在StepService中也有一个Handler,负责与MainActivity进行通讯private static class MessengerHandler extends Handler {
@Override
public void handleMessage(Message msg){
switch (msg.what){
case Constant.MSG_FROM_CLIENT:
try{
Messenger messenger=msg.replyTo;
Message replyMsg=Message.obtain(null,Constant.MSG_FROM_SERVER);
Bundle bundle=new Bundle();
//将现在的步数以消息的形式进行发送
bundle.putInt("step",StepDetector.CURRENT_STEP);
replyMsg.setData(bundle);
messenger.send(replyMsg); //发送要返回的消息
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
StepService中的onCreate方法注册关屏、开屏等广播。开启一个线程,执行计步逻辑。同时开启一个计时器,每30s往数据库中写入一次数据。
@Override
public void onCreate(){
super.onCreate();
initBroadcastReceiver();
new Thread(new Runnable() {
@Override
public void run() {
//启动步数监测器
startStepDetector();
}
}).start();
startTimeCount();
}
/**
* 初始化广播
*/
private void initBroadcastReceiver(){
//定义意图过滤器
final IntentFilter filter=new IntentFilter();
//屏幕灭屏广播
filter.addAction(Intent.ACTION_SCREEN_OFF);
//日期修改
filter.addAction(Intent.ACTION_TIME_CHANGED);
//关闭广播
filter.addAction(Intent.ACTION_SHUTDOWN);
//屏幕高亮广播
filter.addAction(Intent.ACTION_SCREEN_ON);
//屏幕解锁广播
filter.addAction(Intent.ACTION_USER_PRESENT);
//当长按电源键弹出“关机”对话或者锁屏时系统会发出这个广播
//example:有时候会用到系统对话框,权限可能很高,会覆盖在锁屏界面或者“关机”对话框之上,
//所以监听这个广播,当收到时就隐藏自己的对话,如点击pad右下角部分弹出的对话框
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
mBatInfoReceiver=new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action=intent.getAction();
if(Intent.ACTION_SCREEN_ON.equals(action)){
Log.v(TAG,"screen on");
}else if(Intent.ACTION_SCREEN_OFF.equals(action)){
Log.v(TAG,"screen off");
save();
//改为60秒一存储
duration=60000;
}else if(Intent.ACTION_USER_PRESENT.equals(action)){
Log.v(TAG,"screen unlock");
save();
//改为30秒一存储
duration=30000;
}else if(Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())){
Log.v(TAG,"receive Intent.ACTION_CLOSE_SYSTEM_DIALOGS 出现系统对话框");
//保存一次
save();
}else if(Intent.ACTION_SHUTDOWN.equals(intent.getAction())){
Log.v(TAG,"receive ACTION_SHUTDOWN");
save();
}else if(Intent.ACTION_TIME_CHANGED.equals(intent.getAction())){
Log.v(TAG,"receive ACTION_TIME_CHANGED");
initTodayData();
}
}
};
registerReceiver(mBatInfoReceiver,filter);
}
在onStartComand中,从数据库中初始化今日步数,并更新通知栏
@Override
public int onStartCommand(Intent intent, int flags, int startId){
initTodayData();
updateNotification("今日步数:"+StepDetector.CURRENT_STEP+" 步");
return START_STICKY;
}
在initTodayData()中,我们先得到了今天的日期,然后根据日期查询数据库中的数据,如果数据库中没有当天的数据则步数为0,如果有数据则取出数据库中的数据赋值给用于记录步数的StepDetector.CURRENT_STEP变量。值得注意的是这里有一个isNewDay的boolean型变量,这里我们先不管它,后面会介绍
private void initTodayData(){
CURRENTDATE=getTodayDate();
DbUtils.createDb(this,Constant.DB_NAME);
//获取当天的数据
List<StepData> list=DbUtils.getQueryByWhere(StepData.class,"today",new String[]{CURRENTDATE});
if(list.size()==0||list.isEmpty()){
//如果获取当天数据为空,则步数为0
StepDetector.CURRENT_STEP=0;
isNewDay=true; //用于判断是否存储之前计步器统计的步数,后面会用到
}else if(list.size()==1){
isNewDay=false;
//如果数据库中存在当天的数据那么获取数据库中的步数
StepDetector.CURRENT_STEP=Integer.parseInt(list.get(0).getStep());
}else{
Log.e(TAG, "出错了!");
}
}
根据不同的API,选择开启Google内置计步器或者加速度传感器
private void startStepDetector(){
if(sensorManager!=null&& stepDetector !=null){
sensorManager.unregisterListener(stepDetector);
sensorManager=null;
stepDetector =null;
}
//得到休眠锁,目的是为了当手机黑屏后仍然保持CPU运行,使得服务能持续运行
getLock(this);
sensorManager=(SensorManager)this.getSystemService(SENSOR_SERVICE);
//android4.4以后可以使用Google内置计步器
int VERSION_CODES = Build.VERSION.SDK_INT;
if(VERSION_CODES>=19){
addCountStepListener();
}else{
addBasePedoListener();
}
}
API版本大于19执行addCountStepListener方法,首先先拿到两种传感器,TYPE_STEP_COUNTER 计步传感器,用于记录激活后的步伐数。TYPE_STEP_DETECTOR 步行检测传感器,用户每走一步就触发一次事件。根据拿到的情况来注册监听,sensorManager.registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_UI);查资料后得知这里第一个参数是Listener,第二个参数是所得传感器类型,第三个参数值获取传感器信息的频率 。如果两种传感器都没拿到,就还是执行addBasePedoListener方法。在addBasePedoListener方法里获得加速度传感器,注册监听,然后调stepDetector里的回调接口更新通知栏。具体代码片段如下:
private void addCountStepListener(){
Sensor detectorSensor=sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
Sensor countSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
if(countSensor!=null){
stepSensor = 0;
Log.v(TAG, "countSensor");
sensorManager.registerListener(StepService.this,countSensor,SensorManager.SENSOR_DELAY_UI);
}else if(detectorSensor!=null){
stepSensor = 1;
Log.v("base", "detector");
sensorManager.registerListener(StepService.this,detectorSensor,SensorManager.SENSOR_DELAY_UI);
}else{
stepSensor = 2;
Log.e(TAG,"Count sensor not available!");
addBasePedoListener();
}
}
在addBasePedoListener()中使用了加速度传感器。该方法中会调用StepDetector这个类,它可以通过检测传感器的值通过一定的算法判断是否为1步,具体算法后面会揭晓。
private void addBasePedoListener(){
//只有在使用加速传感器的时候才会调用StepDetector这个类
stepDetector =new StepDetector(this);
//获得传感器类型,这里获得的类型是加速度传感器
//此方法用来注册,只有注册过才会生效,参数:SensorEventListener的实例,Sensor的实例,更新速率
Sensor sensor=sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
sensorManager.registerListener(stepDetector,sensor,SensorManager.SENSOR_DELAY_UI);
stepDetector.setOnSensorChangeListener(new StepDetector.OnSensorChangeListener() {
@Override
public void onChange() {
updateNotification("今日步数:"+StepDetector.CURRENT_STEP+" 步");
}
});
}
无论是使用了Google内置计步器还是使用了加速度传感器,当传感器发生变化时都会调用onSensorChanged()方法。我们在这里根据stepSensor(计步传感器类型)进行一下判断,如果是Google内置计步器,该计步器可以返回一个值(int)event.value[0],这个值统计了我们之前走过的步数,目前的猜测该值应该是以一个月为周期进行统计的(因为上来该值就是上万步,我是在月初测的,经过和QQ里面的计步进行比较发现该值和我这个月走过的总步数相当)。这时候我们就需要用到上面提到的boolean型变量isNewDay了,如果该值为true说明是新的一天,我们记录下内置计步器所统计的步数。将该值存入数据库中,如果该值为false那么说明数据库中已经存储了今天的数据,我们可以从数据库中读取当天开始时候存储的(int)event.value[0]值。那么今天所走的步数=计步器当前统计的步数-之前统计的步数。这样即使程序中途没有运行,也能够准确的记录当天所走的步数。如果是加速度传感器,则不对它进行操作,因为计步的方法会在StepDetector类中实现。之后我们需要更新状态栏信息
@Override
public void onSensorChanged(SensorEvent event) {
if(stepSensor == 0){ //使用计步传感器
if(isNewDay) {
//用于判断是否为新的一天,如果是那么记录下计步传感器统计步数中的数据
// 今天走的步数=传感器当前统计的步数-之前统计的步数
previousStep = (int) event.values[0]; //得到传感器统计的步数
isNewDay = false;
save();
//为防止在previousStep赋值之前数据库就进行了保存,我们将数据库中的信息更新一下
List<StepData> list=DbUtils.getQueryByWhere(StepData.class,"today",new String[]{CURRENTDATE});
//修改数据
StepData data=list.get(0);
data.setPreviousStep(previousStep+"");
DbUtils.update(data);
}else {
//取出之前的数据
List<StepData> list = DbUtils.getQueryByWhere(StepData.class, "today", new String[]{CURRENTDATE});
StepData data=list.get(0);
this.previousStep = Integer.valueOf(data.getPreviousStep());
}
StepDetector.CURRENT_STEP=(int)event.values[0]-previousStep;
}else if(stepSensor == 1){
StepDetector.CURRENT_STEP++;
}
//更新状态栏信息
updateNotification("今日步数:" + StepDetector.CURRENT_STEP + " 步");
}
3.最后我们介绍当使用加速度传感器时需要用到的StepDetector类。
在该类的onSensorChanged()方法中,我们先得到传感器事件,获得加速度传感器并且算出加速度传感器的x、y、z三轴的平均数值(这是为了平衡在某一个方向数值过大造成的数据误差),之后交给DetectorNewStep方法处理
@Override
public void onSensorChanged(SensorEvent event){
Sensor sensor=event.sensor;
synchronized (this){
//获取加速度传感器
if(sensor.getType()==sensor.TYPE_ACCELEROMETER){
calc_step(event);
}
}
}
synchronized private void calc_step(SensorEvent event){
average=(float)Math.sqrt(Math.pow(event.values[0],2)
+Math.pow(event.values[1],2)+Math.pow(event.values[2],2));
detectorNewStep(average);
}
在detectorNewStep方法中,首先判断上次传感器三轴的平均值值gravityOld是否为0,如果为零说明这是第一次进行探测将当前的值赋给gravityOld,如果不为零我们通过当前的值values和上次传感器的值gravityOld值通过DetectorPeak进行判断,检测是否为波峰(具体算法下面会给出),当检测到了波峰后记录这次的时间和上次的时间,如果两次的时间差在200到2000毫秒之间并且波峰与波谷的差大于阈值时就判定为一步更新这次波峰的时间和界面;而如果符合时间差条件,但波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中。最后将传入的值赋给grayityOld
/**
* 监测新的步数
*
* 1.传入sersor中的数据
* 2.如果检测到了波峰,并且符合时间差以及阈值的条件,则判定位1步
* 3.符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中
* @param values 加速传感器三轴的平均值
*/
private void detectorNewStep(float values) {
if(gravityOld==0){
gravityOld=values;
}else{
if(DetectorPeak(values,gravityOld)){
timeOfLastPeak=timeOfThisPeak;
timeOfNow=System.currentTimeMillis();
if(timeOfNow-timeOfLastPeak>=200&&(peakOfWave-valleyOfWave>=ThreadValue)
&&(timeOfNow-timeOfLastPeak)<=2000){
timeOfThisPeak=timeOfNow;
//更新界面的处理,不涉及算法
preStep();
}
if(timeOfNow-timeOfLastPeak>=200
&&(peakOfWave-valleyOfWave>=initialValue)){
timeOfThisPeak=timeOfNow;
ThreadValue=Peak_Valley_Thread(peakOfWave-valleyOfWave);
}
}
}
gravityOld=values;
}
下面是检测波峰的方法:先记录下lastStatus即上一点的状态,上升还是下降,然后比较新值和旧值如果newValue >= oldValue就设置和更新 isDirectionUp、continueUpCount 否则就将contineUpCount赋给continueUpFormerCount(上一点的持续上升的次数,为了记录波峰的上升次数)。最后判断当满足波峰判断的四个条件(见代码注释)的话,这个值就是波峰值返回true。如果上一次状态为下降,本次状态为上升则这个值为波谷值并返回false。
/**
* 监测波峰
* 以下四个条件判断为波峰
* 1.目前点为下降的趋势:isDirectionUp为false
* 2.之前的点为上升的趋势:lastStatus为true
* 3.到波峰为止,持续上升大于等于2次
* 4.波峰值大于1.2g,小于2g
* 记录波谷值
* 1.观察波形图,可以发现在出现步子的地方,波谷的下一个就是波峰,有比较明显的特征以及差值
* 2.所以要记录每次的波谷值,为了和下次的波峰作对比
* @param newValue
* @param oldValue
* @return
*/
public boolean DetectorPeak(float newValue,float oldValue){
lastStatus=isDirectionUp;
if(newValue>=oldValue){
isDirectionUp=true;
continueUpCount++;
}else{
continueUpFormerCount=continueUpCount;
continueUpCount=0;
isDirectionUp=false;
}
if(!isDirectionUp&&lastStatus&&(continueUpFormerCount>=2&&(oldValue>=minValue&&oldValue<maxValue))){
//满足上面波峰的四个条件,此时为波峰状态
peakOfWave=oldValue;
return true;
}else if(!lastStatus&&isDirectionUp){
//满足波谷条件,此时为波谷状态
valleyOfWave=oldValue;
return false;
}else{
return false;
}
}
往后看一下preStep方法,这个方法通过变量CountTimeState,将计步分为三种模式
CountTimeState=0 代表还未开启计步器
CountTimeState=1 代表预处理模式,也就是说TEMP_STEP步数如果在规定的时间内一直在增加,知道这个模式结束,那么TEMP_STEP值有效,反之,无效舍弃,目的是为了过滤掉一些手机晃动带来的影响。
CountTimeState=2 代表正常计步模式
当模式为0时会开启一个计时器并将CountTimeState标记为1。这个计时器会在3.5秒内每隔0.7秒检测一次步数是否在增加,如果3.5秒内步数一直增加则将CountTimeState标记为2,进入正常计步模式并将TEMP_STEP值加入到总步数中,同时开启一个Timer每隔两秒进行一次检测,看步数是否仍在增长,如果步数不再增长则说明人已经停止走路,将CountTimerState改为0。如果在开始的3.5秒之内步数不再增长则认为是手机晃动造成的,清除TEMP_STEP的值并修改CountTimeState为0。
private void preStep(){
if(CountTimeState==0){
//开启计时器(倒计时3.5秒,倒计时时间间隔为0.7秒) 是在3.5秒内每0.7面去监测一次。
time=new TimeCount(duration,700);
time.start();
CountTimeState=1; //计时中
Log.v(TAG,"开启计时器");
}else if(CountTimeState==1){
TEMP_STEP++; //如果传感器测得的数据满足走一步的条件则步数加1
Log.v(TAG,"计步中 TEMP_STEP:"+TEMP_STEP);
}else if(CountTimeState==2){
CURRENT_STEP++;
if(onSensorChangeListener!=null){
//在这里调用onChange() 因此在StepService中会不断更新状态栏的步数
onSensorChangeListener.onChange();
}
}
}
动态生成阈值,阈值是为了跟波峰与波谷的差值进行比较,进而判断是否为1步。
/**
* 阈值的计算
* 1.通过波峰波谷的差值计算阈值
* 2.记录4个值,存入tempValue[]数组中
* 3.在将数组传入函数averageValue中计算阈值
* @param value
* @return
*/
public float Peak_Valley_Thread(float value){
float tempThread=ThreadValue;
if(tempCount<valueNum){
tempValue[tempCount]=value;
tempCount++;
}else{
//此时tempCount=valueNum=5
tempThread=averageValue(tempValue,valueNum);
for(int i=1;i<valueNum;i++){
tempValue[i-1]=tempValue[i];
}
tempValue[valueNum-1]=value;
}
return tempThread;
}
接下来看一下将阈值进行梯度化,取4组数值,进行梯度化。这些数值是通过大量测试试出来的
/**
* 梯度化阈值
* 1.计算数组的均值
* 2.通过均值将阈值梯度化在一个范围里
*/
public float averageValue(float value[],int n){
float ave=0;
for(int i=0;i<n;i++){
ave+=value[i];
}
ave=ave/valueNum; //计算数组均值
if(ave>=8){
Log.v(TAG,"超过8");
ave=(float)4.3;
}else if(ave>=7&&ave<8){
Log.v(TAG,"7-8");
ave=(float)3.3;
}else if(ave>=4&&ave<7){
Log.v(TAG,"4-7");
ave=(float)2.3;
}else if(ave>=3&&ave<4){
Log.v(TAG,"3-4");
ave=(float)2.0;
}else{
Log.v(TAG,"else (ave<3)");
ave=(float)1.7;
}
return ave;
}
程序的重要代码到这里基本就介绍完毕了。如果需要源码可以点击这里下载。感谢大家的观看,希望能够对你有所帮助!