如果你恰好在学习Android,并且刚看完网上的一些入门视频以及一些相关书籍,需要一个实战项目来巩固自己所学,并提升自我开发能力,那么此文也许你可以浅看一下。此外重点在于叙述,并附上少部分关键代码,不会面面俱到介绍每一个功能代码实现;
Android——一个简单的模版APP
名称 | 功能 |
---|---|
百度地图 | 百度地图电子地图、定位、导航 |
Mob | 手机短信验证码 |
ArcSoft | 人脸识别 |
Material Design | 一些优质的控件 |
数据格式 | json和xml数据的解析 |
存储方式 | SharedPreferences、SQLite、File |
自定义View | 一些简易或者较难的自定义控件 |
可视化 | hellocharts |
线程和异步 | Handler、Thraed、Timer |
四大组件 | Activity、Service、BroadcastReceiver |
设计模式 | 单例、观察者、工厂 |
使用百度地图SDK实现地图、定位、导航功能;
通过其封装的MapView
控件实现地图显示
并通过实现BDAbstractLocationListener
抽象类,获取自身地址的经纬度信息完成定位
MyLocationData.Builder builder = new MyLocationData.Builder();
builder.latitude(location.getLatitude());//纬度
builder.longitude(location.getLongitude());//经度
MyLocationData locationData = builder.build();
map.setMyLocationData(locationData);
通过输入起点、终点地址信息,然后使用geocoder
将地址信息转为地理信息
private String GetNodeValue(String Address){
builder = new StringBuilder( );
try {
List addresses = geocoder.getFromLocationName(Address,1 );
double start_latitude = addresses.get( 0 ).getLatitude();//纬度
double start_longitude = addresses.get( 0 ).getLongitude();//经度
builder.append( start_longitude ).append( "," ).append( start_latitude );
} catch (IOException e) {
utils.ShowFail("异常或者地址信息不够精确");
e.printStackTrace();
}
return builder.toString();
}
通过单例BNDemoFactory
类,获取转换之后的地理信息,并通过传入算路结点,计算地理信息精确度,并处理算路网络请求;
注:因为使用的是个人版
导航精确度不准确,需求一级一级输入,例如:xx省xx市xx大学
private void routePlanToNavi(final Bundle bundle) {
List list = new ArrayList<>();
//TODO 在主函数中获取经纬度,开始算路导航
list.add(BNDemoFactory.getInstance().getStartNode(this));
list.add(BNDemoFactory.getInstance().getEndNode(this));
// 关闭电子狗
if (BaiduNaviManagerFactory.getCruiserManager().isCruiserStarted()) {
BaiduNaviManagerFactory.getCruiserManager().stopCruise();
}
BaiduNaviManagerFactory.getRoutePlanManager().routePlanToNavi(
list,
IBNRoutePlanManager.RoutePlanPreference.ROUTE_PLAN_PREFERENCE_DEFAULT,
bundle, handler);
}
private void initRouteSortList() {
mRouteSortList = new ArrayList<>();
mRouteSortList.add(new RouteSortModel("智能推荐", IBNRoutePlanManager.RoutePlanPreference
.ROUTE_PLAN_PREFERENCE_DEFAULT));
mRouteSortList.add(new RouteSortModel("时间优先", IBNRoutePlanManager.RoutePlanPreference
.ROUTE_PLAN_PREFERENCE_TIME_FIRST));
mRouteSortList.add(new RouteSortModel("少收费", IBNRoutePlanManager.RoutePlanPreference
.ROUTE_PLAN_PREFERENCE_NOTOLL));
mRouteSortList.add(new RouteSortModel("躲避拥堵", IBNRoutePlanManager.RoutePlanPreference
.ROUTE_PLAN_PREFERENCE_AVOID_TRAFFIC_JAM));
mRouteSortList.add(new RouteSortModel("不走高速", IBNRoutePlanManager.RoutePlanPreference
.ROUTE_PLAN_PREFERENCE_NOHIGHWAY));
mRouteSortList.add(new RouteSortModel("高速优先", IBNRoutePlanManager.RoutePlanPreference
.ROUTE_PLAN_PREFERENCE_ROAD_FIRST));
}
private void initTabView(LinearLayout layout_tab, BNRoutePlanItem bnRoutePlanItem) {
TextView prefer = layout_tab.findViewById(R.id.prefer);
prefer.setText(bnRoutePlanItem.getPusLabelName());
TextView time = layout_tab.findViewById(R.id.time);
time.setText((int) bnRoutePlanItem.getPassTime() / 60 + "分钟");
TextView distance = layout_tab.findViewById(R.id.distance);
distance.setText((int) bnRoutePlanItem.getLength() / 1000 + "公里");
TextView traffic_light = layout_tab.findViewById(R.id.traffic_light);
traffic_light.setText(String.valueOf(bnRoutePlanItem.getLights()));
}
通过在预定车位界面开启订单Service
,后台服务自动开启订单计时,并修改当前用户SQLite字段IsParking
的值,只允许一个订单存在,所以改变数据库字段值,一是防止多次开启服务,二是,在另一界面读取后面服务计算的时间时,可以由此判定
private void StartService(){
utils.ShowSuccess("订单创建成功");
Intent intent = new Intent(ChooseParkActivity.this,ParkingService.class);
bindService(intent,connection, Service.BIND_AUTO_CREATE);
startService(intent);
getTimeThing();
}
首先通过判定是否登录,然后在判定是否存在订单,再决定是否开启新服务
private void IsLogin(){
try {
boolean result = dao.QueryLogin(Phone) == 1;
boolean isparking = IsParking();
if (result && isparking){
/**
* login success*/
StartService();
dao.UpdateIsParking(Phone,1);
Log.d("ChooseParkActivity","establish success");
}else if (!result){
/**login fail*/
utils.ShowFail("尚未登录");
}else if (!isparking){
utils.ShowFail("已存在订单,请先结束当前订单");
}else {
utils.ShowFail("未登录且已有订单,异常行为");
}
}catch (NullPointerException e){
utils.ShowFail("手机号码为空");
e.printStackTrace();
}
}
/**
* 如果未创建订单才能进行工作,同一时间只允许一个服务订单进行*/
private boolean IsParking(){
boolean result = false;
try {
result = dao.QueryIsParking(Phone) == 0;
Log.d("ChooseParkActivity","IsParking="+result);
}catch (NullPointerException e){
utils.ShowFail("手机号码为空");
e.printStackTrace();
}
return result;
}
在订单结算界面同样通过判定是否登录,是否创建订单,由此决定是否获取后台服务的计时数据
private void InitService(){
connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
ParkingService.LocalBinder binder = (ParkingService.LocalBinder)iBinder;
mService = binder.getService();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mService = null;
}
};
boolean result = IsLogin();
if (result){
Intent intent = new Intent(ParkingOrder.this,ParkingService.class);
bindService(intent,connection, Service.BIND_AUTO_CREATE);
startService(intent);
handler_time.sendEmptyMessageDelayed(1,1000);
ToPayment.setClickable(true);
ToPayment.setEnabled(true);
}else {
ToPayment.setClickable(false);
ToPayment.setEnabled(false);
handler_time.sendEmptyMessage(0);
}
}
通过Handler和Timer完成后台计时,将获取毫秒级时间通过转换为秒、分、时,并建立一个缓冲实体类,由此类将数据传导出去
currentTime = SystemClock.elapsedRealtime();
Log.d("ParkingService","SystemClock.elapsedRealtime"+currentTime);
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
/* 返回系统启动到现在的毫秒数,包含休眠时间。*/
currenttime = (int) ((SystemClock.elapsedRealtime() - currentTime) / 1000);
Log.d("ParkingService","currenttime"+currenttime);
String Hour = new DecimalFormat("00").format(currenttime / 3600);
String Minute = new DecimalFormat("00").format(currenttime % 3600 / 60);
String Second = new DecimalFormat("00").format(currenttime % 60);
//String Format = new String(GetTimeHour + ":" + GetTimeMinute + ":" + GetTimeSecond);
TimeFormat timeFormat = new TimeFormat(Hour,Minute,Second);
Message msg = new Message();
msg.obj = timeFormat;
handler_time.sendMessage(msg);
}
}, 0, 1000L);
通过密码框输入密码或者人脸识别两种方式进行支付验证,然后通过提取数据库此记录的余额判定是否支付成功,支付成功则修改balance
,IsParking
字段值,将订单进行时改为订单结束,方便下次创建订单
public void onFinish(String str) {
if (str.equals(PayPassword)){
//password true
if (information.getBalance() < PayMoney){
//余额不足
utils.ShowFail("余额不足,请先完成充值!");
}else {
//payment success
utils.ShowSuccess("支付成功");
mService.RefreshTime();
String time = OrderTime.getText().toString();
/**将订单信息插入数据库*/
dao.InsertRecord(new PaymentRecord(information.getPhone(),mPlace,time,PayMoney));
//更新余额
int balance = information.getBalance() - PayMoney;
Log.d("ParkingOrder","now balance"+information.getBalance());
Log.d("ParkingOrder","need pay money"+PayMoney);
Log.d("ParkingOrder","update balance"+balance);
dao.UpdateBalance(UserName,balance);
dao.UpdateIsParking(UserName,0);
ParkingUseTime.setText(00 + ":" + 00 + ":" + 00);
sp.PutData(ParkingOrder.this,"PayMoney",PayMoney+"");
StopService();
startActivity(new Intent(ParkingOrder.this,PaySuccessActivity.class));
}
}else {
//password false
utils.ShowFail("支付密码错误,请重试");
mPayPwdEditText.clearText();
}
}
通过判定是否登录,从而决定是否从数据库中获取数据,并显示在用户界面上
public int QueryLogin(String PhoneNumber){
DB = helper.getReadableDatabase();
//selection查询子句的条件,可以使用主键查询
String[] columns = {Helper.Row_Phone,Helper.Row_IsLogin}; // 需要的列
String selection = "PhoneNumber = ?";
String[] selectionArgs = {PhoneNumber + ""};
Cursor cursor = DB.query(Helper.TableName_System,columns,selection,selectionArgs,null,null,null);
if (cursor.moveToFirst())
{
int login = cursor.getInt(cursor.getColumnIndex( Helper.Row_IsLogin ));
return login;
}
DB.close();
cursor.close();
return 0;
}
通过在个人信息界面对所需要的信息进行修改完成对数据库更新操作
public void UpdatePassWord(String PhoneNumber,String PassWord){
DB = helper.getReadableDatabase();
if (DB.isOpen()){
ContentValues values = new ContentValues();
values.put(Helper.Row_PassWord,PassWord);
int count = DB.update(Helper.TableName_System,values,"PhoneNumber = ?",new String[]{PhoneNumber});
if (count > 0){
Log.d(TAG,"update password row="+count);
}else {
Log.d(TAG,"update PassWord fail");
}
DB.close();
}
}
将模块将分为热门城市、附件城市、全部城市、全部县四部分,并通过viewpager+tablayout+pageradapter
进行子页面滑动绑定,将城市的xml文件与json文件信息进行解析并导入适配器子项中,在次界面可以选择任意城市,然后通过监听RecyclerView子项点击事件,完成对数据库字段City
的更新,方便在主界面对其进行读取
nearCitiesRecyclerView.setOnclick( new NearCitiesRecyclerView.OnClick() {
@Override
public void OnClickListener(View view, int Position) {
String city = nearCity[Position];
Log.d("City",city);
String phoneNumber = (String) sp.GetData(City.this,"PhoneNumber","");
if (!TextUtils.isEmpty(phoneNumber)){
dao.UpdateCity(phoneNumber,city);
toastUtils.ShowSuccess("所在地已更改为"+city);
}else {
toastUtils.ShowFail("无法获取登录实例");
}
}
} );
通过使用hellocharts
可视化工具,将历史订单信息通过柱状图显示出来,数据库创建两张表,一张已手机号码为主键,保存一些个人信息,状态等字段,另一张表存储消费记录,主键id自增长,以手机号码为查询某个账户所拥有的历史记录,并在此界面进行取出;在此封装了一个工具类,只需要传入柱状图实例即可,在其中对数据库数据进行取出,以消费时间为x轴,以消费金额为y轴
public class ChartUtils {
private Dao dao;
private Context context;
private List paymentRecords = new ArrayList<>();
private ColumnChartData columnData;
//底部标题
private List title = new ArrayList<>();
//颜色值
private List color = new ArrayList<>();
//X、Y轴值list
private List axisXValues = new ArrayList<>();
private List mPointValues;
//所有的柱子
private List columns = new ArrayList<>();
private int Totalnum;
private int single = 1;
public void Init(Context context){
this.context = context;
dao = new Dao(context);
paymentRecords = dao.QueryAllRecord();
int size = paymentRecords.size();
if (size > 16){
Totalnum = 16;
}else {
Totalnum = size;
}
}
/**
* 下一步只取最近支付16次,防止屏幕装载太多*/
public void setChartViewData(ColumnChartView chart) {
for (int i = 0; i ();
for (int i = 0; i < single; i++) {
mPointValues.add(new SubcolumnValue((paymentRecords.get(j).getParkingPrice()), color.get(j)));
//值的大小、颜色
//设置X轴的柱子所对应的属性名称(底部文字)
axisXValues.add(new AxisValue(j).setLabel(title.get(j)));
}
Column column = new Column(mPointValues);
ColumnChartValueFormatter chartValueFormatter = new SimpleColumnChartValueFormatter(2);
column.setFormatter(chartValueFormatter);
column.setHasLabelsOnlyForSelected(false);
column.setHasLabels(true);
//column.setValues(mPointValues);
//将每个属性得列全部添加到List中
//添加了7个大柱子Column,单个大柱子里面mPointValues大小为3(自行调整)
columns.add(column);
}
//设置每个柱子的Lable是否选中,为false,表示不用选中,一直显示在柱子上
//设置Columns添加到Data中
ColumnChartData columnData = new ColumnChartData(columns);
//底部
Axis axisBottom = new Axis(axisXValues);
//是否显示X轴的网格线
axisBottom.setHasLines(false);
//分割线颜色
axisBottom.setLineColor(Color.parseColor("#ff0000"));
//字体颜色
axisBottom.setTextColor(Color.parseColor("#666666"));
//字体大小
axisBottom.setTextSize(10);
//底部文字
axisBottom.setName("消费时间");
//每个柱子的便签是否倾斜着显示
axisBottom.setHasTiltedLabels(false);
//距离各标签之间的距离,包括离Y轴间距 (0-32之间)
axisBottom.setMaxLabelChars(10);
//设置是否自动生成轴对象,自动适应表格的范围(设置之后底部标题变成0-5)
//axisBottom.setAutoGenerated(true);
axisBottom.setHasSeparationLine(true);
//设置x轴在底部显示
columnData.setAxisXBottom(axisBottom);
//左边 属性与上面一致
Axis axisLeft = new Axis();
axisLeft.setHasLines(false);
axisLeft.setName("消费金额");
axisLeft.setHasTiltedLabels(true);
axisLeft.setTextColor(Color.parseColor("#666666"));
columnData.setAxisYLeft(axisLeft);
//设置组与组之间的间隔比率,取值范围0-1,1表示组与组之间不留任何间隔
columnData.setFillRatio(0.7f);
chart.setInteractive(false);
//最后将所有值显示在View中
chart.setColumnChartData(columnData);
}
}
通过使用Mob手机短信验证码完成账户设置,通过使用viewpager+tablayout+pageradapter
将注册和忘记密码进行子页面滑动绑定,通过通过对数据库进行查询和插入、更新操作完成上述操作
eh = new EventHandler() {
@Override
public void afterEvent(int event, int result, Object data) {
// TODO 此处为子线程!不可直接处理UI线程!处理后续操作需传到主线程中操作!
if (event == SMSSDK.EVENT_SUBMIT_VERIFICATION_CODE) {
if (result == SMSSDK.RESULT_COMPLETE) {
runOnUiThread(() -> {
if (FuncFlag){
RegisterSuccess();
}else {
ResetSuccess();
}
//toastUtils.ShowSuccess("验证成功");
});
} else {
runOnUiThread(() -> {
//processError("验证码不匹配");
toastUtils.ShowFail("验证码不匹配");
});
}
} else if (event == SMSSDK.EVENT_GET_VERIFICATION_CODE) {
runOnUiThread(() -> {
//processError("获取短信验证码成功");
toastUtils.ShowSuccess("获取短信验证码成功");
});
} else {
runOnUiThread(() -> {
//processError("异常");
toastUtils.ShowFail("异常");
});
((Throwable) data).printStackTrace();
}
在通过点击获取验证码之后,对其进行读秒操作,并将秒数更改到Button内容上
Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (handlerFlag) {
if (Current > 0) {
Current--;
RGetCode.setEnabled(false);
RGetCode.setText("(" + Current + "s)");
sendEmptyMessageDelayed(0, 1000);
Log.d("RegisterAddForget", Current + "S");
} else {
RGetCode.setEnabled(true);
RGetCode.setText("验证码");
handlerFlag = false;
Current = 60;
}
}else {
RGetCode.setEnabled(true);
RGetCode.setText("验证码");
handlerFlag = false;
Current = 60;
}
}
};
public boolean JudgePhoneNumberLength(String Number) {
if (TextUtils.isEmpty(Number)) {
return false;
} else {
return Number.length() == 11 ? true : false;
}
}
/*一、中国电信号段:
133、153、173、177、180、181、189、191、199
二、中国联通号段:
130、131、132、155、156、166、175、176、185、186
三、中国移动号段:
134(0-8)、135、136、137、138、139、147、150、151、152、157、158、159、178、182、183、184、187、188、198*/
private boolean JudgePhoneNumberFormat(String Number) {
//第一位必须为1,第二位为345789其中一位,后面九位从0-9都可以
String NumberFormat = "[1][345789]\\d[9]";
if (TextUtils.isEmpty(Number)) {
return false;
} else {
return Number.matches(NumberFormat);
}
}
完成数据库插入操作
private void RegisterSuccess() {
handlerFlag = false;
toastUtils.ShowSuccess("注册成功");
dao.Insert(new Information("", PassWord, "", PhoneNumber, "DefaultCity", "000000", 0, 0, 0,0));
handler.sendEmptyMessageDelayed(0, 1000);
}
完成更新数据库操作
private void ResetSuccess(){
handlerFlag = false;
toastUtils.ShowSuccess("重置密码成功");
dao.UpdatePassWord(ResetPhoneNumber,NewPassWord);
handler.sendEmptyMessageDelayed(0, 1000);
}
通过对数据进行读取操作,并对顺序表进行顺序查询,判定是否存在相同记录,从而判定是否登录成功
if (informationList != null) {
Log.d("Login","get into if");
Log.d("Login",informationList.size() +"size");
for (int i = 0; i < informationList.size(); i++) {
Log.d("Login","get into for");
if ((GetPhoneNumber.equals(informationList.get(i).getPhone())) && (GetPassWordEdit.equals(informationList.get(i).getPassWord()))) {
toastUtils.ShowSuccess("登录成功");
dao.UpdateStatus(GetPhoneNumber, 1);
sp.PutData(Login.this, "PhoneNumber", GetPhoneNumber);
startActivity(new Intent(Login.this, MainActivity.class));
return;
}
}
toastUtils.ShowFail("账号或密码错误");
} else {
toastUtils.ShowFail("异常");
}
因为还有一些细节没有完善,暂不贴出下载地址,完善之后,在评论区贴出,若有需要的,可以私信我,可以提前发;
生如蝼蚁,应当有鸿鹄之志;命如纸薄,应当有不屈之心;祝愿诸君皆为时代缔造者,相约顶峰,共赏千里桃花地,把酒言欢,畅谈古今