Android大作業—樂道步走(HappyRunning)一款計步器和跑步運動軌跡記錄Android APP

Android大作業——樂道步走(HappyRunning)

(一款計步器和跑步運動軌跡記錄Android APP)

(作業要求體現四大組件Activity、Service、BroadCast Recevicer、Content provider,所以有些功能略顯多餘)
文章內附項目GitHub鏈接

前言

這是一款輕量、簡易的、採用高德地圖SDK記錄軌跡和三軸加速度傳感器的跑步、計步軟件

簡要功能介紹

跑步模塊
通過高德地圖SDK記錄每一次跑步的軌跡,並將每一天跑步的里程、時間、記錄在數據庫裏,支持查看歷史跑步記錄。

計步模塊
利用三軸加速度傳感器,記錄一天走過的步數、允許設置每天的鍛鍊計劃,以及提供歷史記錄起到監督反省自己的作用
(奇奇怪怪我上傳不了圖片)
1~7圖片鏈接

  1. 首次啓動獲取用戶相關權限後進入應用
  2. 可以使用用戶賬號密碼或者獲取驗證碼登錄的方式進入應用
  3. 通過手機號,獲取驗證碼進行用戶註冊
  4. 查看跑步總次數和總時長,並且可以進入新一次跑步記錄
  5. 跑步軌跡記錄,可切換地圖模式或普通模式
  6. 查看今日步數和設置鍛鍊計劃,查看歷史記錄
  7. 設置每日步數計劃和設置提醒時間

相關代碼
全局樣式,設置沉浸式和透明狀態欄

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="windowNoTitle">true</item>
    </style>
<style name="splash" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="windowNoTitle">true</item>
    <item name="android:windowTranslucentStatus">true</item>
</style>

    <style name="NoActionBar" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowContentOverlay">@null</item>
        <item name="colorPrimary">@color/basecolor</item>
        <item name="colorPrimaryDark">@color/basecolorDeep</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowAnimationStyle">@style/activityAnimation</item>
    </style>

    <!-- animation 樣式 -->
    <style name="activityAnimation" parent="@android:style/Animation">
        <item name="android:activityOpenEnterAnimation">@anim/slide_right_in</item>
        <item name="android:activityOpenExitAnimation">@anim/slide_left_out</item>
        <item name="android:activityCloseEnterAnimation">@anim/slide_left_in</item>
        <item name="android:activityCloseExitAnimation">@anim/slide_right_out</item>
    </style>

    <!--解決啓動時白屏問題-->
    <style name="Theme.Start" parent="NoActionBar">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowBackground">@color/white</item>
        <!--<item name="android:windowDisablePreview">true</item>-->
        <!--<item name="android:windowIsTranslucent">true</item>-->
    </style>
    <style name="TabRadioButton">
        <item name="android:layout_width">0dp</item>
        <item name="android:layout_weight">1</item>
        <item name="android:layout_height">match_parent</item>
        <item name="android:padding">5dp</item>
        <item name="android:gravity">center</item>
        <item name="android:button">@null</item>
        <item name="android:textSize">10sp</item>
        <item name="android:textColor">@color/tab_selector_text</item>
    </style>

    <style name="style_smile">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:gravity">center</item>
        <item name="android:layout_gravity">center</item>
    </style>
    <style name="style_text_large" parent="style_smile">
        <item name="android:textSize">@dimen/text_size_large</item>
    </style>
</resources>

啓動、註冊、登錄(部分)

package com.example.happyrunning.ui.activity;

import android.Manifest;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import com.blankj.utilcode.util.SPUtils;
import com.example.happyrunning.MyApplication;
import com.example.happyrunning.R;
import com.example.happyrunning.commons.utils.Status_sp;
import com.example.happyrunning.commons.utils.UIhelper;
import com.example.happyrunning.commons.utils.Utils;
import com.example.happyrunning.ui.BaseActivity;
import com.example.happyrunning.ui.permission.PermissionHelper;
import com.example.happyrunning.ui.permission.PermissionListener;
import com.example.happyrunning.ui.weight.CountDownProgress;
import com.gyf.barlibrary.ImmersionBar;

import butterknife.BindView;

public class Splash extends BaseActivity {
    @BindView(R.id.img_url)
    ImageView img_url;
    @BindView(R.id.countDownProgressView)
    CountDownProgress countDownProgress;
    @BindView(R.id.versions)
    TextView versions;

    /**
     * 上一次點擊返回鍵時間
     */
    private long lastBackPressed;

    /*
    上次點擊返回鍵時間
     */
    private  static final int QUIT_INTERVAL=3000;

//    申請權限
    private static String[] PERMISSIONS_STORAGE=
        {
                Manifest.permission.WRITE_EXTERNAL_STORAGE,
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
        };

    @Override
    protected void initImmersionBar() {
        super.initImmersionBar();
        if (ImmersionBar.hasNavigationBar(this)) {
            ImmersionBar.with(this).transparentNavigationBar().init();
        }
    }

    @Override
    public int getLayoutId() {
        return R.layout.activity_splash;
    }

    @Override
    public void initData(Bundle savedInstanceState) {
        img_url.setImageResource(R.mipmap.splash_bg);

        versions.setText(UIhelper.getString(R.string.splash_appversionname, MyApplication.getAppVersionName()));
        showToast("初始化中,請稍後...");
        countDownProgress.setTimeMillis(2000);
        countDownProgress.setProgressType(CountDownProgress.ProgressType.COUNT_BACK);
        countDownProgress.start();
    }

    @Override
    public void initListener() {
        countDownProgress.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                countDownProgress.stop();

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    // 獲取權限
                    PermissionHelper.requestPermissions(Splash.this, PERMISSIONS_STORAGE, new PermissionListener() {
                        @Override
                        public void onPassed() {
                            startActivity();
                        }
                    });
                } else {
                    Splash.this.startActivity();
                }
            }
        });

        countDownProgress.setProgressListener(new CountDownProgress.OnProgressListener() {
            @Override
            public void onProgress(int progress) {
                if (progress==0) {
                    // 版本判斷。當手機系統大於 23 時,纔有必要去判斷權限是否獲取
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        // 獲取權限
                        PermissionHelper.requestPermissions(Splash.this, PERMISSIONS_STORAGE, Splash.this.getResources().getString(R.string.app_name) + "需要獲取存儲、位置權限", new PermissionListener() {
                            @Override
                            public void onPassed() {
                                startActivity();
                            }
                        });
                    } else {
                        Splash.this.startActivity();
                    }
                }
            }
        });
    }

    public void startActivity() {
        if (SPUtils.getInstance().getBoolean(Status_sp.ISLOGIN)) {
            startActivity(new Intent(Splash.this, MainActivity.class));
            finish();
        } else {
            startActivity(new Intent(Splash.this, Login.class));
            finish();
        }
        countDownProgress.stop();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (keyCode == KeyEvent.KEYCODE_BACK) { // 表示按返回鍵 時的操作
                long backPressed = System.currentTimeMillis();
                if (backPressed - lastBackPressed > QUIT_INTERVAL) {
                    lastBackPressed = backPressed;
                    Utils.showToast(Splash.this, "再按一次退出");
                } else {
                    if (countDownProgress != null) {
                        countDownProgress.stop();
                        countDownProgress.clearAnimation();
                    }
                    moveTaskToBack(false);
                    MyApplication.closeApp(this);
                    finish();
                }
            }
        }
        return false;
    }


}

package com.example.happyrunning.ui.activity;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.KeyEvent;
import android.view.View;
import android.widget.Button;

import com.blankj.utilcode.util.SPUtils;
import com.example.happyrunning.MyApplication;
import com.example.happyrunning.R;
import com.example.happyrunning.commons.utils.Conn;
import com.example.happyrunning.commons.utils.Status_sp;
import com.example.happyrunning.commons.utils.Utils;
import com.example.happyrunning.db.DataManager;
import com.example.happyrunning.db.RealmHelper;
import com.example.happyrunning.ui.BaseActivity;
import com.example.happyrunning.ui.fragment.FastLoginFragment;
import com.example.happyrunning.ui.fragment.PwdLoginFragment;
import com.flyco.tablayout.SlidingTabLayout;

import java.util.ArrayList;

import butterknife.BindView;
import butterknife.OnClick;


public class Login extends BaseActivity {

    @BindView(R.id.slidingTabLayout)
    SlidingTabLayout slidingTabLayout;
    @BindView(R.id.vp)
    ViewPager vp;
    @BindView(R.id.btLogin)
    Button btLogin;
    @BindView(R.id.btReg)
    Button btReg;

    /**
     * 上次點擊返回鍵的時間
     */
    private long lastBackPressed;

    //上次點擊返回鍵的時間
    public static final int QUIT_INTERVAL = 2500;

    private final String[] mTitles = {"普通登錄", "快速登錄"};

    private ArrayList<Fragment> mFragments = new ArrayList<>();

    private boolean isPsd = true;//是否是密碼登錄

    private PwdLoginFragment psdLoginFragment = new PwdLoginFragment();
    private FastLoginFragment fastLoginFragment = new FastLoginFragment();

    private DataManager dataManager = null;

    @Override
    public int getLayoutId() {
        return R.layout.activity_login;
    }

    @Override
    public void initData(Bundle savedInstanceState) {

        dataManager = new DataManager(new RealmHelper());

        MyPagerAdapter mAdapter = new MyPagerAdapter(getSupportFragmentManager());
        vp.setAdapter(mAdapter);

        mFragments.add(psdLoginFragment);
        mFragments.add(fastLoginFragment);

        slidingTabLayout.setViewPager(vp, mTitles, this, mFragments);

        isPsd = true;
    }

    @Override
    public void initListener() {
        vp.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int i, float v, int i1) {

            }

            @Override
            public void onPageSelected(int i) {
                isPsd = i == 0;
            }

            @Override
            public void onPageScrollStateChanged(int i) {

            }
        });
    }

    @OnClick({R.id.container, R.id.btLogin, R.id.btReg})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.container:
                hideSoftKeyBoard();
                break;
            case R.id.btLogin:

                if (isPsd) {
                    psdLoginFragment.checkAccount(this::login);
                } else {
                    fastLoginFragment.checkAccount(this::login);
                }
                break;
            case R.id.btReg:
                startActivity(new Intent(Login.this, Regist.class));
                break;
            default:
                break;
        }
    }

    /**
     * 登錄
     */
    public void login(String account, String psd) {
        btLogin.setEnabled(false);
        showLoadingView();
        new Handler().postDelayed(() -> {
            dismissLoadingView();
            btLogin.setEnabled(true);
            if (isPsd) {
                if (dataManager.checkAccount(account, psd))
                    loginSuccess(account, psd);
                else
                    showToast("賬號或密碼錯誤!");
            } else {
                if (dataManager.checkAccount(account))
                    loginSuccess(account, "");
                else
                    showToast("賬號不存在!");
            }
        }, Conn.Delayed);
    }

    private void loginSuccess(String account, String psd) {
        SPUtils.getInstance().put(Status_sp.ISLOGIN, true);

        SPUtils.getInstance().put(Status_sp.USERID, account.substring(8));

        SPUtils.getInstance().put(Status_sp.PHONE, account);
        SPUtils.getInstance().put(Status_sp.PASSWORD, psd);

        startActivity(new Intent(Login.this, MainActivity.class));
        Utils.showToast(Login.this, "恭喜您,登錄成功...");

        finish();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (keyCode == KeyEvent.KEYCODE_BACK) { // 表示按返回鍵 時的操作
                long backPressed = System.currentTimeMillis();
                if (backPressed - lastBackPressed > QUIT_INTERVAL) {
                    lastBackPressed = backPressed;
                    showToast("再按一次退出");
                } else {
                    moveTaskToBack(false);
                    MyApplication.closeApp(this);
                    finish();
                }
            }
        }
        return super.onKeyDown(keyCode, event);
    }

    private class MyPagerAdapter extends FragmentPagerAdapter {
        MyPagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public int getCount() {
            return mFragments.size();
        }

        @Override
        public CharSequence getPageTitle(int position) {
            return mTitles[position];
        }

        @Override
        public Fragment getItem(int position) {
            return mFragments.get(position);
        }
    }

    @Override
    protected void onDestroy() {
        if (null != dataManager)
            dataManager.closeRealm();
        super.onDestroy();
    }
}

具體代碼看項目https://github.com/Aristochi/HappyRun

各頁面採用RadioButton+Fragment的方式實現頁面之間的滑動
定位地圖SDK採用高德地圖

package com.example.happyrunning.sport_motion;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;

import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationListener;
import com.amap.api.maps.model.LatLng;
import com.example.happyrunning.commons.utils.LogUtils;
import com.example.happyrunning.sport_motion.servicecode.RecordService;
import com.example.happyrunning.sport_motion.servicecode.impl.RecordServiceImpl;

/**
 * 定位的Service類,用戶在運動時此服務會在後臺進行定位。
 */
public class LocationService extends Service {

    private InterfaceLocationed interfaceLocationed = null;

    public static final String TAG = "LocationService";

    public final IBinder mBinder = new LocalBinder();

    public class LocalBinder extends Binder {
        // 在Binder中定義一個自定義的接口用於數據交互
        // 這裏直接把當前的服務傳回給宿主
        public LocationService getService() {
            return LocationService.this;
        }
    }

    //定位的時間間隔,單位是毫秒
    private static final int LOCATION_SPAN = 10 * 1000;

    //高德地圖中定位的類
    public AMapLocationClient mLocationClient = null;
    //記錄着運動中移動的座標位置
//    private List<LatLng> mSportLatLngs = new LinkedList<>();

    //記錄運動信息的Service
    private RecordService mRecordService = null;

    @Override
    public void onCreate() {
        super.onCreate();

        //聲明LocationClient類
        mLocationClient = new AMapLocationClient(this);
        //給定位類加入自定義的配置
        initLocationOption();
        //註冊監聽函數
        mLocationClient.setLocationListener(MyAMapLocationListener);

        //初始化信息記錄類
        mRecordService = new RecordServiceImpl(this);

        //啓動定位
        mLocationClient.startLocation();
    }

    //初始化定位的配置
    private void initLocationOption() {
        AMapLocationClientOption mOption = new AMapLocationClientOption();
        mOption.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy);//可選,設置定位模式,可選的模式有高精度、僅設備、僅網絡。默認爲高精度模式
        mOption.setGpsFirst(true);//可選,設置是否gps優先,只在高精度模式下有效。默認關閉
        mOption.setHttpTimeOut(30000);//可選,設置網絡請求超時時間。默認爲30秒。在僅設備模式下無效
        mOption.setInterval(4000);//可選,設置定位間隔。默認爲2秒
        mOption.setNeedAddress(true);//可選,設置是否返回逆地理地址信息。默認是true
        mOption.setOnceLocation(false);//可選,設置是否單次定位。默認是false
        mOption.setOnceLocationLatest(false);//可選,設置是否等待wifi刷新,默認爲false.如果設置爲true,會自動變爲單次定位,持續定位時不要使用
        AMapLocationClientOption.setLocationProtocol(AMapLocationClientOption.AMapLocationProtocol.HTTP);//可選, 設置網絡請求的協議。可選HTTP或者HTTPS。默認爲HTTP
        mOption.setSensorEnable(true);//可選,設置是否使用傳感器。默認是false
        mOption.setWifiScan(true); //可選,設置是否開啓wifi掃描。默認爲true,如果設置爲false會同時停止主動刷新,停止以後完全依賴於系統刷新,定位位置可能存在誤差
        mOption.setLocationCacheEnable(false); //可選,設置是否使用緩存定位,默認爲true
        mOption.setGeoLanguage(AMapLocationClientOption.GeoLanguage.DEFAULT);//可選,設置逆地理信息的語言,默認值爲默認語言(根據所在地區選擇語言)
        mLocationClient.setLocationOption(mOption);
    }

    //定位回調
    private AMapLocationListener MyAMapLocationListener = aMapLocation -> {

        if (null == aMapLocation)
            return;

        if (aMapLocation.getErrorCode() == 0) {
            //先暫時獲得經緯度信息,並將其記錄在List中
            LogUtils.d("緯度信息爲" + aMapLocation.getLatitude() + "\n經度信息爲" + aMapLocation.getLongitude());
            LatLng locationValue = new LatLng(aMapLocation.getLatitude(), aMapLocation.getLongitude());
//                mSportLatLngs.add(locationValue);

            //將運動信息上傳至服務器
            recordLocation(locationValue, aMapLocation.getLocationDetail());

            //定位成功,發送通知
            if (null != interfaceLocationed)
                interfaceLocationed.locationed(aMapLocation);

        } else {
            String errText = "定位失敗," + aMapLocation.getErrorCode() + ": " + aMapLocation.getErrorInfo();
            LogUtils.e("AmapErr", errText);
        }
    };

    private void recordLocation(LatLng latLng, String location) {
        if (mRecordService != null) {
            mRecordService.recordSport(latLng, location);
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        LogUtils.i(TAG, "綁定服務 The service is binding!");
        // 綁定服務,把當前服務的IBinder對象的引用傳遞給宿主
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        LogUtils.i(TAG, "解除綁定服務 The service is unbinding!");
        //解除綁定後銷燬服務
        stopSelf();
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (null != mLocationClient) {
            mLocationClient.stopLocation();
            mLocationClient.unRegisterLocationListener(MyAMapLocationListener);
            mLocationClient.onDestroy();
            mLocationClient = null;
        }
    }

    public void setInterfaceLocationed(InterfaceLocationed interfaceLocationed) {
        this.interfaceLocationed = null;
        this.interfaceLocationed = interfaceLocationed;
    }

    public interface InterfaceLocationed {
        void locationed(AMapLocation aMapLocation);
    }
}

數據庫使用了Realm記錄運動,存儲個人信息,註冊登錄判斷,本項目沒有使用服務器,所有數據存儲在本地,故註冊驗證使用的是隨機數,如果要在線驗證可以使用mob的SDK實現號碼短信驗證。雲數據庫如果是自己個人測試或者作業使用可以使用bmob比目雲的數據庫。

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