背景
市面上出现了越来越多的计步设备,也有越来越多的app将计步器这块添加作为功能模块.IOS平台,苹果公司已经开放了提供计步数据的api,而android平台作为一个开发的平台,只提供了对应的传感器,具体怎么实现计步,实现方式很多,有传感器、定位等等,然而市面上android app计步并不是很精确.
目的
启动计步开源项目、致力于打造稳定误差小的android平台计步器。
主题
接下来笔者从传感器角度入手做一个android平台的计步器.首先我们需要做的优先任务就是完成计步核心代码编写.参考网上通用的传感器计步算法,通过梯度化阈值、阈值的计算、 检测波峰等算法推算出人是否在行走,然后对两次行走步伐之间的时间差值正常范围应该为200ms-2000ms的判断,进行矫正计步精度.
计算梯度化阈值核心代码如下:
/**
* 梯度化阈值
* 计算数组的均值
* 通过均值将阈值梯度化在一个范围里
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param value
* @param n
* @return
*/
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 = (float) 1.7;
}
return ave;
}
阈值的计算核心代码如下:
/**
* 阈值的计算
* 通过波峰波谷的差值计算阈值
* 记录4个值,存入tempValue[]数组中
* 在将数组传入函数averageValue中计算阈值
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param value
* @return
*/
public float Peak_Valley_Thread(float value) {
float tempThread = ThreadValue;
if (tempCount < valueNum) {
tempValue[tempCount] = value;
tempCount++;
} else {
tempThread = averageValue(tempValue, valueNum);
for (int i = 1; i < valueNum; i++) {
tempValue[i - 1] = tempValue[i];
}
tempValue[valueNum - 1] = value;
}
return tempThread;
}
检测波峰核心代码如下:
/**
* 检测波峰
* 以下四个条件判断为波峰:
* 目前点为下降的趋势:isDirectionUp为false
* 之前的点为上升的趋势:lastStatus为true
* 到波峰为止,持续上升大于等于2次
* 波峰值大于1.2g,小于2g
* 记录波谷值
* 观察波形图,可以发现在出现步子的地方,波谷的下一个就是波峰,有比较明显的特征以及差值
* 所以要记录每次的波谷值,为了和下次的波峰做对比
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @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;
}
Log.v(TAG, "oldValue:" + oldValue);
if (!isDirectionUp && lastStatus
&& (continueUpFormerCount >= 2 && (oldValue >= minValue && oldValue < maxValue))) {
peakOfWave = oldValue;
return true;
} else if (!lastStatus && isDirectionUp) {
valleyOfWave = oldValue;
return false;
} else {
return false;
}
}
根据以上算法得到计步,在此我对计步做了相关优化,连续运动一段时间才开始计步,屏蔽细微移动或者驾车时震动所带来的干扰.停止运动一段时间后,需要连续运动一段时间才会计步.至此就完成计步核心服务类代码,因此就能从这个核心服务类中拿到我们所需要的计步数据.
接下来,我们需要对计步数据做相应处理,开启一个计步服务用于拿取计步数据、更新ui以及缓存数据.为了避免影响计步app性能,我们将开启一个新的进程用于计步服务,这样我们就需要进行进程间的通信,这里笔者是采用Messenger进行进程通信.
计步服务代码如下:
/**
* @className: StepService
* @classDescription: 计步服务
* @author: leibing
* @createTime: 2016/08/31
*/
@TargetApi(Build.VERSION_CODES.CUPCAKE)
public class StepService extends Service implements SensorEventListener {
// TAG
private final String TAG = "StepService";
// 默认int错误码
public static final int INT_ERROR = -12;
// 停止广播动作
public static final String ACTION_STOP_SERVICE = "action_stop_service";
// step key
public final static String STEP_KEY = "step_key";
// 传感器管理
private SensorManager sensorManager;
// 计步核心类
private StepDcretor stepDetector;
// 自定义Handler
private MsgHandler msgHandler = new MsgHandler();
// Messenger 用于跨进程通信
private Messenger messenger = new Messenger(msgHandler);
// 计步需要缓存的数据
private StepModel mStepModel;
// 计步服务广播
private BroadcastReceiver stepServiceReceiver;
// 是否手动停止服务
private boolean isNeedStopService = false;
/**
* @className: MsgHandler
* @classDescription: 用于更新客户端UI
* @author: leibing
* @createTime: 2016/08/31
*/
class MsgHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Constant.MSG_FROM_CLIENT:
try {
// 缓存数据
cacheStepData(StepService.this,StepDcretor.CURRENT_STEP + "");
// 更新通知栏
updateNotification(msg.getData());
// 回复消息给Client
Messenger messenger = msg.replyTo;
Message replyMsg = Message.obtain(null, Constant.MSG_FROM_SERVER);
Bundle bundle = new Bundle();
bundle.putInt(STEP_KEY, StepDcretor.CURRENT_STEP);
replyMsg.setData(bundle);
messenger.send(replyMsg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
/**
* 更新通知栏
* @author leibing
* @createTime 2016/09/02
* @lastModify 2016/09/02
* @param bundle 数据
* @return
*/
private void updateNotification(Bundle bundle) {
if (bundle == null) {
NotificationUtils.getInstance(StepService.this).
updateNotification("今日行走" + StepDcretor.CURRENT_STEP + "步");
}else {
// 内容
String content = (String) bundle.getSerializable(Constant.CONTENT_KEY);
// ticker
String ticker = (String) bundle.getSerializable(Constant.TICKER_KEY);
// 标题
String contentTile = (String) bundle.getSerializable(Constant.CONTENTTITLE_KEY);
// 需要跳转的Activity
Class pendingClass = (Class) bundle.getSerializable(Constant.PENDINGCLASS_KEY);
// 是否不可取消
boolean isOngoing = true;
if (bundle.getSerializable(Constant.ISONGOING_KEY) != null){
isOngoing = (boolean) bundle.getSerializable(Constant.ISONGOING_KEY);
}
// 头像
int icon = INT_ERROR;
if (bundle.getSerializable(Constant.ICON_KEY) != null){
icon = (int) bundle.getSerializable(Constant.ICON_KEY);
}
// id
int notifyId = INT_ERROR;
if (bundle.getSerializable(Constant.NOTIFYID_KEY) != null){
notifyId = (int) bundle.getSerializable(Constant.NOTIFYID_KEY);
}
if (StringUtil.isEmpty(content)
|| StringUtil.isEmpty(ticker)
|| StringUtil.isEmpty(contentTile)){
NotificationUtils.getInstance(StepService.this).
updateNotification("今日行走" + StepDcretor.CURRENT_STEP + "步");
}else {
NotificationUtils.getInstance(StepService.this).
updateNotification(content + StepDcretor.CURRENT_STEP + "步",
ticker,
contentTile,
StepService.this,
pendingClass,
isOngoing,
notifyId,
icon);
}
}
}
/**
* 启动服务为前台服务( 让该service前台运行,避免手机休眠时系统自动杀掉该服务)
* @author leibing
* @createTime 2016/09/07
* @lastModify 2016/09/07
* @param
* @return
*/
public void startForeground(){
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
// 设置头像
builder.setSmallIcon(R.mipmap.ic_launcher);
// 设置标题
builder.setContentTitle("foreground service");
// 设置内容
builder.setContentText("try to avoid this service be killed!");
// 创建notification
Notification notification = builder.build();
//如果 id 为 0 ,那么状态栏的 notification 将不会显示。
startForeground(0, notification);
}
@Override
public void onCreate() {
super.onCreate();
// 初始化计步服务广播
initStepServiceReceiver();
// 启动计步
startStep();
// 启动服务为前台服务( 让该service前台运行,避免手机休眠时系统自动杀掉该服务)
startForeground();
Log.v(TAG,"onCreate");
}
/**
* 初始化计步服务广播
* @author leibing
* @createTime 2016/09/01
* @lastModify 2016/09/01
* @param
* @return
*/
private void initStepServiceReceiver() {
final IntentFilter filter = new IntentFilter();
// 添加停止当前服务广播动作
filter.addAction(ACTION_STOP_SERVICE);
// 实例化广播接收器
stepServiceReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_STOP_SERVICE.equals(action)){
Log.v(TAG,"停止服务");
// 停止服务
isNeedStopService = true;
StepService.this.stopSelf();
}
}
};
// 注册计步服务广播
registerReceiver(stepServiceReceiver, filter);
}
/**
* 启动计步
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param
* @return
*/
private void startStep() {
// 启动计步器
startStepDetector();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(TAG, "onStartCommand");
return START_STICKY;
}
/**
* 缓存计步数据
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param context 上下文
* @param stepCount 计步数
* @return
*/
private void cacheStepData(Context context, String stepCount){
mStepModel = new StepModel();
mStepModel.setDate(DateUtils.simpleDateFormat(new Date()));
mStepModel.setStep(stepCount);
DataCache.getInstance().addStepCache(context, mStepModel);
}
@Override
public IBinder onBind(Intent intent) {
Log.v(TAG, "onBind");
return messenger.getBinder();
}
/**
* 启动计步器
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param
* @return
*/
private void startStepDetector() {
if (sensorManager != null && stepDetector != null) {
sensorManager.unregisterListener(stepDetector);
sensorManager = null;
stepDetector = null;
}
// 初始化计步(拿缓存更新计步数)
DataCache.getInstance().getTodayCache(this, new DataCache.DataCacheListener() {
@Override
public void readListCache(StepModel stepModel) {
if (stepModel != null){
StepDcretor.CURRENT_STEP = Integer.parseInt(stepModel.getStep());
}
}
});
sensorManager = (SensorManager) this
.getSystemService(SENSOR_SERVICE);
// 添加自定义
addBasePedoListener();
// 添加传感器监听
addCountStepListener();
}
/**
* 停止计步器
* @author leibing
* @createTime 2016/09/01
* @lastModify 2016/09/01
* @param
* @return
*/
public void stopStepDetector(){
if (sensorManager != null && stepDetector != null) {
sensorManager.unregisterListener(stepDetector);
sensorManager = null;
stepDetector = null;
}
}
/**
* 添加传感器监听(步行检测传感器、计步传感器)
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param
* @return
*/
private void addCountStepListener() {
// 步行检测传感器,用户每走一步就触发一次事件
Sensor detectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
// 计步传感器
Sensor countSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
if (detectorSensor != null) {
sensorManager.registerListener(StepService.this, detectorSensor, SensorManager.SENSOR_DELAY_UI);
} else if (countSensor != null) {
sensorManager.registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_UI);
} else {
Log.v(TAG, "Count sensor not available!");
}
}
/**
*添加传感器监听(加速度传感器)
* @author leibing
* @createTime 2016/08/31
* @lastModify 2016/08/31
* @param
* @return
*/
private void addBasePedoListener() {
stepDetector = new StepDcretor();
// 获得传感器的类型,这里获得的类型是加速度传感器
// 此方法用来注册,只有注册过才会生效,参数:SensorEventListener的实例,Sensor的实例,更新速率
Sensor sensor = sensorManager
.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
// sensorManager.unregisterListener(stepDetector);
sensorManager.registerListener(stepDetector, sensor,
SensorManager.SENSOR_DELAY_UI);
stepDetector
.setOnSensorChangeListener(new StepDcretor.OnSensorChangeListener() {
@Override
public void onChange() {
}
});
}
@Override
public void onSensorChanged(SensorEvent event) {
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
@Override
public void onDestroy() {
// 取消前台进程
stopForeground(true);
// 解注册计步服务广播
unregisterReceiver(stepServiceReceiver);
// 停止计步器
stopStepDetector();
// 非手动停止服务,则自动重启服务
if (!isNeedStopService){
Intent intent = new Intent(this, StepService.class);
startService(intent);
}else {
isNeedStopService = false;
}
Log.v(TAG,"onDestroy");
super.onDestroy();
}
@Override
public boolean onUnbind(Intent intent) {
Log.v(TAG,"onUnbind");
return super.onUnbind(intent);
}
}
创建计步服务的时候,笔者将计步服务置为前台服务,减少计步服务进程被杀几率。有童鞋会问,为何不开启静态广播去定期唤醒计步服务?其实这样做,并不能做到适配,android 6.0以后已经去掉了相关的系统静态广播.真正能做到百分百进程保活,只能靠手机厂家白名单了,其他的都是浮云,顶多只是降低被杀的几率而已.对于进程保活这块就不纠结了.
本项目已开源,项目地址:JkStepSensor.
如有疑问,请联系!
- QQ:872721111
- email:[email protected]
- github:leibing@github