Android大作业——乐道步走(HappyRunning)
(一款计步器和跑步运动轨迹记录Android APP)
(作业要求体现四大组件Activity、Service、BroadCast Recevicer、Content provider,所以有些功能略显多余)
前言
这是一款轻量、简易的、采用高德地图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比目云的数据库。