Android實現計步器功能,隔天步數清零,查看歷史運動紀錄_附源碼

最近需要用到計步功能,這可難壞我了,IOS端倒好,有自帶的計步功能,讓我驚訝的是連已爬樓層都給做好了,只需要調接口便可獲得數據,我有一句MMP,我很想講。

但是抱怨歸抱怨,功能還是得嘗試的去實現,微信運動,樂動力,都還不錯,尤其是樂動力的計步功能真的非常的強大,在UI方面與用戶交互也做得非常棒,當連續運動十步後開始計步。本想着去找他們實現的算法然後拿來用,但很明顯這是不可能的。後來我搜了很多資料發現,在Android4.4 Kitkat 新增的STEP DETECTOR 以及 STEP COUNTER傳感器。但是!android的這個傳感器雖然可以計步,但是所記錄的步數是從你開機之時開始計算,不斷累加,隔天也不會清零,並且,一旦關機後,傳感器記錄的數據也就清空了!這就很尷尬了,不過既然直接使用傳感器數據不行,那我們就自己動手,將數據按天來保存~接下來進入正題,皮皮猿,我們走起~

先來看下我們需要解決的點有:

1、步數從開機之後不斷累加,關機之後便清零,步數不能隔天清零

2、不能查看歷史數據

這就好辦了。我們只需將當前傳感器記錄的步數以每天爲單位存進數據庫,如果更新的步數爲當天的則去更新數據庫!先來看下我的界面(Demo在文章最後):

      

 

第一二張圖爲界面效果圖,數據均是從數據取出繪製在界面上,第三張圖爲設置前臺進程時所設置的Notification樣式,當然了這個可以去自定義樣式,再此我就不詳細解釋了。

工程的目錄結構如下:

 

其中主要的代碼都在StepService.class 中了,其中註釋也都非常詳細,我就直接放代碼了:

/**
 * Created by fySpring
 * Date : 2017/3/24
 * To do :
 */

public class StepService extends Service implements SensorEventListener {
    public static final String TAG = "StepService";

    //當前日期
    private static String CURRENT_DATE;
    //當前步數
    private int CURRENT_STEP;
    //3秒進行一次存儲
    private static int saveDuration = 3000;
    //傳感器
    private SensorManager sensorManager;
    //數據庫
    private StepDataDao stepDataDao;
    //計步傳感器類型 0-counter 1-detector
    private static int stepSensor = -1;
    //廣播接收
    private BroadcastReceiver mInfoReceiver;
    //自定義簡易計時器
    private TimeCount timeCount;
    //發送消息,用來和Service之間傳遞步數
    private Messenger messenger = new Messenger(new MessengerHandler());
    //是否有當天的記錄
    private boolean hasRecord;
    //未記錄之前的步數
    private int hasStepCount;
    //下次記錄之前的步數
    private int previousStepCount;
    private Notification.Builder builder;

    private NotificationManager notificationManager;
    private Intent nfIntent;


    @Override
    public void onCreate() {
        super.onCreate();
        initBroadcastReceiver();
        new Thread(new Runnable() {
            public void run() {
                getStepDetector();
            }
        }).start();
        startTimeCount();
        initTodayData();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return messenger.getBinder();
    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        /**
         * 此處設將Service爲前臺,不然當APP結束以後很容易被GC給幹掉,這也就是大多數音樂播放器會在狀態欄設置一個
         * 原理大都是相通的
         */
        notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        //獲取一個Notification構造器
        builder = new Notification.Builder(this.getApplicationContext());
        /**
         * 設置點擊通知欄打開的界面,此處需要注意了,如果你的計步界面不在主界面,則需要判斷app是否已經啓動,
         * 再來確定跳轉頁面,這裏面太多坑,(別問我爲什麼知道 - -)
         * 總之有需要的可以和我交流
         */
        nfIntent = new Intent(this, MainActivity.class);
        builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0)) // 設置PendingIntent
                .setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher)) // 設置下拉列表中的圖標(大圖標)
                .setContentTitle("今日步數"+CURRENT_STEP+"步") // 設置下拉列表裏的標題
                .setSmallIcon(R.mipmap.ic_launcher) // 設置狀態欄內的小圖標
                .setContentText("加油,要記得勤加運動"); // 設置上下文內容
        // 獲取構建好的Notification
        Notification stepNotification = builder.build();

        notificationManager.notify(110,stepNotification);
        // 參數一:唯一的通知標識;參數二:通知消息。
        startForeground(110, stepNotification);// 開始前臺服務

        return START_STICKY;
    }

    /**
     * 自定義handler
     */
    private class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constant.MSG_FROM_CLIENT:
                    try {
                        //這裏負責將當前的步數發送出去,可以在界面或者其他地方獲取,我這裏是在MainActivity中獲取來更新界面
                        Messenger messenger = msg.replyTo;
                        Message replyMsg = Message.obtain(null, Constant.MSG_FROM_SERVER);
                        Bundle bundle = new Bundle();
                        bundle.putInt("steps", CURRENT_STEP);
                        replyMsg.setData(bundle);
                        messenger.send(replyMsg);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }


    /**
     * 初始化廣播
     */
    private void initBroadcastReceiver() {
        final IntentFilter filter = new IntentFilter();
        // 屏幕滅屏廣播
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        //關機廣播
        filter.addAction(Intent.ACTION_SHUTDOWN);
        // 屏幕解鎖廣播
        filter.addAction(Intent.ACTION_USER_PRESENT);
        // 當長按電源鍵彈出“關機”對話或者鎖屏時系統會發出這個廣播
        // example:有時候會用到系統對話框,權限可能很高,會覆蓋在鎖屏界面或者“關機”對話框之上,
        // 所以監聽這個廣播,當收到時就隱藏自己的對話,如點擊pad右下角部分彈出的對話框
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        //監聽日期變化
        filter.addAction(Intent.ACTION_DATE_CHANGED);
        filter.addAction(Intent.ACTION_TIME_CHANGED);
        filter.addAction(Intent.ACTION_TIME_TICK);

        mInfoReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                switch (action) {
                    // 屏幕滅屏廣播
                    case Intent.ACTION_SCREEN_OFF:
                        //屏幕熄滅改爲10秒一存儲
                        saveDuration = 10000;
                        break;
                    //關機廣播,保存好當前數據
                    case Intent.ACTION_SHUTDOWN:
                        saveStepData();
                        break;
                    // 屏幕解鎖廣播
                    case Intent.ACTION_USER_PRESENT:
                        saveDuration = 3000;
                        break;
                    // 當長按電源鍵彈出“關機”對話或者鎖屏時系統會發出這個廣播
                    // example:有時候會用到系統對話框,權限可能很高,會覆蓋在鎖屏界面或者“關機”對話框之上,
                    // 所以監聽這個廣播,當收到時就隱藏自己的對話,如點擊pad右下角部分彈出的對話框
                    case Intent.ACTION_CLOSE_SYSTEM_DIALOGS:
                        saveStepData();
                        break;
                    //監聽日期變化
                    case Intent.ACTION_DATE_CHANGED:
                    case Intent.ACTION_TIME_CHANGED:
                    case Intent.ACTION_TIME_TICK:
                        saveStepData();
                        isNewDay();
                        break;
                    default:
                        break;
                }
            }
        };
        //註冊廣播
        registerReceiver(mInfoReceiver, filter);
    }

    /**
     * 初始化當天數據
     */
    private void initTodayData() {
        //獲取當前時間
        CURRENT_DATE = TimeUtil.getCurrentDate();
        //獲取數據庫
        stepDataDao = new StepDataDao(getApplicationContext());
        //獲取當天的數據,用於展示
        StepEntity entity = stepDataDao.getCurDataByDate(CURRENT_DATE);
        //爲空則說明還沒有該天的數據,有則說明已經開始當天的計步了
        if (entity == null) {
            CURRENT_STEP = 0;
        } else {
            CURRENT_STEP = Integer.parseInt(entity.getSteps());
        }
    }


    /**
     * 監聽晚上0點變化初始化數據
     */
    private void isNewDay() {
        String time = "00:00";
        if (time.equals(new SimpleDateFormat("HH:mm").format(new Date())) ||
                !CURRENT_DATE.equals(TimeUtil.getCurrentDate())) {
            initTodayData();
        }
    }


    /**
     * 獲取傳感器實例
     */
    private void getStepDetector() {
        if (sensorManager != null) {
            sensorManager = null;
        }
        // 獲取傳感器管理器的實例
        sensorManager = (SensorManager) this
                .getSystemService(SENSOR_SERVICE);
        //android4.4以後可以使用計步傳感器
        int VERSION_CODES = Build.VERSION.SDK_INT;
        if (VERSION_CODES >= 19) {
            addCountStepListener();
        }
    }


    /**
     * 添加傳感器監聽
     */
    private void addCountStepListener() {
        Sensor countSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
        Sensor detectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_DETECTOR);
        if (countSensor != null) {
            stepSensor = 0;
            sensorManager.registerListener(StepService.this, countSensor, SensorManager.SENSOR_DELAY_NORMAL);
        } else if (detectorSensor != null) {
            stepSensor = 1;
            sensorManager.registerListener(StepService.this, detectorSensor, SensorManager.SENSOR_DELAY_NORMAL);
        }
    }


    /**
     * 由傳感器記錄當前用戶運動步數,注意:該傳感器只在4.4及以後纔有,並且該傳感器記錄的數據是從設備開機以後不斷累加,
     * 只有當用戶關機以後,該數據纔會清空,所以需要做數據保護
     *
     * @param event
     */
    @Override
    public void onSensorChanged(SensorEvent event) {
        if (stepSensor == 0) {
            int tempStep = (int) event.values[0];
            if (!hasRecord) {
                hasRecord = true;
                hasStepCount = tempStep;
            } else {
                int thisStepCount = tempStep - hasStepCount;
                CURRENT_STEP += (thisStepCount - previousStepCount);
                previousStepCount = thisStepCount;
            }
        } else if (stepSensor == 1) {
            if (event.values[0] == 1.0) {
                CURRENT_STEP++;
            }
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {

    }


    /**
     * 開始倒計時,去存儲步數到數據庫中
     */
    private void startTimeCount() {
        timeCount = new TimeCount(saveDuration, 1000);
        timeCount.start();
    }


    private class TimeCount extends CountDownTimer {
        /**
         * @param millisInFuture    The number of millis in the future from the call
         *                          to {@link #start()} until the countdown is done and {@link #onFinish()}
         *                          is called.
         * @param countDownInterval The interval along the way to receive
         *                          {@link #onTick(long)} callbacks.
         */
        public TimeCount(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }

        @Override
        public void onTick(long millisUntilFinished) {

        }

        @Override
        public void onFinish() {
            // 如果計時器正常結束,則每隔三秒存儲步數到數據庫
            timeCount.cancel();
            saveStepData();
            startTimeCount();
        }
    }


    /**
     * 保存當天的數據到數據庫中,並去刷新通知欄
     */
    private void saveStepData() {
        //查詢數據庫中的數據
        StepEntity entity = stepDataDao.getCurDataByDate(CURRENT_DATE);
        //爲空則說明還沒有該天的數據,有則說明已經開始當天的計步了
        if (entity == null) {
            //沒有則新建一條數據
            entity = new StepEntity();
            entity.setCurDate(CURRENT_DATE);
            entity.setSteps(String.valueOf(CURRENT_STEP));

            stepDataDao.addNewData(entity);
        } else {
            //有則更新當前的數據
            entity.setSteps(String.valueOf(CURRENT_STEP));

            stepDataDao.updateCurData(entity);
        }

        builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0)) // 設置PendingIntent
                .setLargeIcon(BitmapFactory.decodeResource(this.getResources(), R.mipmap.ic_launcher)) // 設置下拉列表中的圖標(大圖標)
                .setContentTitle("今日步數"+CURRENT_STEP+"步") // 設置下拉列表裏的標題
                .setSmallIcon(R.mipmap.ic_launcher) // 設置狀態欄內的小圖標
                .setContentText("加油,要記得勤加運動"); // 設置上下文內容 

        // 獲取構建好的Notification
        Notification stepNotification = builder.build();
        //調用更新
        notificationManager.notify(110,stepNotification);
    }


    @Override
    public void onDestroy() {
        super.onDestroy();
        //主界面中需要手動調用stop方法service纔會結束
        stopForeground(true);
        unregisterReceiver(mInfoReceiver);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }
}
 
目前我的做法是在service中每隔三秒進行調用廣播去更改狀態欄的實時步數,這樣體驗很不好,也不優雅。好的做法是onSensorChanged()方法中實時更改當前運動的的步數,由於我已經將代碼傳上去了不能更改,所以就在這裏說了。
 
其中關於四大組件之一的Service也有很多要去學習的,這幾天也是惡補了一下,算是彌補當年在學校沒有仔細學習這一塊的遺憾吧 - -
主要要說的就是以上了,源碼在這裏源碼點我點我(各位大佬下載了覺得不錯的話幫忙給個好評,謝謝啦~)
 

2017/4/18更新:

使用了一段時間後我發現我這種方法計算的步數和微信計步的記錄數據一樣呢~簡直不要太棒

2018/03/01更新:
一個非常關鍵的權限我居然忘了申請。。。

在AndroidManifest.xml文件添加以下權限,

<uses-permission android:name="android.permission.BODY_SENSORS" />

另外6.0系統以上的手機要在代碼中動態申請該權限,切記,我是說我這麼強大的MIX2怎麼不計步呢,o(╯□╰)o哎,改代碼去了

2020-04-22更新

由於android8.0系統不允許後臺應用創建後臺服務,需要通過startForegroundService來啓動,導致8.0以上機型會閃退。近日正在準備找工作,於是將代碼全部換成了kotlin,並且修改部分代碼,適配8.0系統,算是加深記憶,代碼已經上傳至github,歡迎給個star啊~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章