最近需要做一个计步程序,在网站上研究了一些别人写的程序代码,比较普遍实用的是根据API大小,使用Google内置计步器或加速度传感器进行计步。但是网上源代码的注释很少,经过了一番波折,自己终于有了头绪,并且对计步程序做了一些改进。为了让大家能更快的理解计步原理,现在分享一下自己的一些经验,希望对大家有所帮助
先看一下效果:
(PS:这是我从整个程序中抽出来的一部分,只实现了计步功能,一些其他的控件我已经去掉了,便于大家理解计步的原理。想要美化界面的同胞们可以自行动手哈)
在主Activity中开启一个服务,在服务中注册一个广播接收者监听屏幕的电源情况保存数据。并且新建子线程,在子线程里开启计步检测,根据不同API版本获得不同传感器管理器和传感器实例并注册监听,如果使用的是Google内置计步器则在重写的onSensorChanged()方法中计算步数,如果使用的是加速度传感器则根据传感器得到的数据,计算波峰波谷阈值等数据,符合要求即为一步(具体条件下面会介绍)。之后更新通知栏和界面。
在服务中开启计步检测会对API进行一个判断如果API>19则调用CountSensor,因为android4.4以后可以使用Google内置计步器。对于API<=19的手机可以使用加速度传感器。两种传感器实现计步的方法:
<1> Google内置计步器: 这个用起来就非常方便了注册监听后直接重写onSensorChanged方法,每检测到人走一步就会调用这个方法。为了使当天走的步数尽可能准确,减少关闭程序导致无法计步造成的影响。我在Google内置计步器事件中可以得到一个值(event.value[0]),该值记录的是这个月目前所走的步数,我首先会记录一天中开始时候这个值的大小,然后计算出今天走的步数<2>加速度传感器:使用这个传感器时会检测传感器的变化,得到传感器三轴的值(x,y,z)然后计算他们的平均值,这样做的目的是为了平衡在某一个方向数值过大造成的数据误差,然后将该值与上一时间点的值进行比较,判断是否为波峰或波谷,如果是就相应的保存下来。如果检测到了波峰,并且符合时间差以及阈值的条件,则判定位1步,如果符合时间差条件,波峰波谷差值大于initialValue,则将该差值纳入阈值的计算中。同时为防止微小震动对计步的影响,我们将计步分为3个状态——准备计时、计时中、计步中。所谓“计时中”是在3.5秒内每隔0.7秒对步数进行一次判断,看步数是否仍然在增长,如果不在增长说明之前是无效的震动并没有走路,得到的步数不计入总步数中;反之则将这3.5秒内的步数加入总步数中。之后进入“计步中”状态进行持续计步,并且每隔2秒去判断一次当前步数和2秒前的步数是否相同,如果相同则说明步数不在增长,计步结束。为了更直观的理解,附上一张图(原谅我的盗图)
@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) {}
};
@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;
}
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;
}
private void initTodayData(){
CURRENTDATE=getTodayDate();
DbUtils.createDb(this,Constant.DB_NAME);
//获取当天的数据
List 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, "出错了!");
}
}
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();
}
}
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();
}
}
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+" 步");
}
});
}
@Override
public void onSensorChanged(SensorEvent event) {
if(stepSensor == 0){ //使用计步传感器
if(isNewDay) {
//用于判断是否为新的一天,如果是那么记录下计步传感器统计步数中的数据
// 今天走的步数=传感器当前统计的步数-之前统计的步数
previousStep = (int) event.values[0]; //得到传感器统计的步数
isNewDay = false;
save();
//为防止在previousStep赋值之前数据库就进行了保存,我们将数据库中的信息更新一下
List list=DbUtils.getQueryByWhere(StepData.class,"today",new String[]{CURRENTDATE});
//修改数据
StepData data=list.get(0);
data.setPreviousStep(previousStep+"");
DbUtils.update(data);
}else {
//取出之前的数据
List 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 + " 步");
}
@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);
}
/**
* 监测新的步数
*
* 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
当模式为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
/**
* 梯度化阈值
* 1.计算数组的均值
* 2.通过均值将阈值梯度化在一个范围里
*/
public float averageValue(float value[],int n){
float ave=0;
for(int i=0;i=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;
}