Android大作業——樂道步走(HappyRunning)
(一款計步器和跑步運動軌跡記錄Android APP)
(作業要求體現四大組件Activity、Service、BroadCast Recevicer、Content provider,所以有些功能略顯多餘)
文章內附項目GitHub鏈接
前言
這是一款輕量、簡易的、採用高德地圖SDK記錄軌跡和三軸加速度傳感器的跑步、計步軟件
簡要功能介紹
跑步模塊
通過高德地圖SDK記錄每一次跑步的軌跡,並將每一天跑步的里程、時間、記錄在數據庫裏,支持查看歷史跑步記錄。
計步模塊
利用三軸加速度傳感器,記錄一天走過的步數、允許設置每天的鍛鍊計劃,以及提供歷史記錄起到監督反省自己的作用
(奇奇怪怪我上傳不了圖片)
1~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比目雲的數據庫。