Android——一个简单的APP模版

Android——一个简单的APP模版

  • 效果视频
  • 技术栈
  • 地图、定位、导航
    • 地图
    • 定位
    • 导航
    • 偏好设置
    • 当前导航信息
  • 预定车位和订单结算
    • 创建订单
    • 计时服务传导
      • 后台服务计时
    • 订单结算
  • 个人信息
    • 查询
    • 更新
  • 城市选择
  • 历史订单
  • 登录、注册、忘记密码
    • 验证码获取以及判定
    • 读秒
    • 正则表达式判定手机号码格式
    • 注册
    • 忘记密码
    • 登录

如果你恰好在学习Android,并且刚看完网上的一些入门视频以及一些相关书籍,需要一个实战项目来巩固自己所学,并提升自我开发能力,那么此文也许你可以浅看一下。此外重点在于叙述,并附上少部分关键代码,不会面面俱到介绍每一个功能代码实现;

效果视频

Android——一个简单的模版APP

技术栈

名称 功能
百度地图 百度地图电子地图、定位、导航
Mob 手机短信验证码
ArcSoft 人脸识别
Material Design 一些优质的控件
数据格式 json和xml数据的解析
存储方式 SharedPreferences、SQLite、File
自定义View 一些简易或者较难的自定义控件
可视化 hellocharts
线程和异步 Handler、Thraed、Timer
四大组件 Activity、Service、BroadcastReceiver
设计模式 单例、观察者、工厂

地图、定位、导航

使用百度地图SDK实现地图、定位、导航功能;

Android——一个简单的APP模版_第1张图片 Android——一个简单的APP模版_第2张图片 Android——一个简单的APP模版_第3张图片

地图

通过其封装的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()));
    }

预定车位和订单结算

Android——一个简单的APP模版_第4张图片 Android——一个简单的APP模版_第5张图片 Android——一个简单的APP模版_第6张图片 Android——一个简单的APP模版_第7张图片

创建订单

通过在预定车位界面开启订单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);

订单结算

通过密码框输入密码或者人脸识别两种方式进行支付验证,然后通过提取数据库此记录的余额判定是否支付成功,支付成功则修改balanceIsParking字段值,将订单进行时改为订单结束,方便下次创建订单

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();
                }
            }

个人信息

Android——一个简单的APP模版_第8张图片 Android——一个简单的APP模版_第9张图片

查询

通过判定是否登录,从而决定是否从数据库中获取数据,并显示在用户界面上

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();
        }
    }

城市选择

Android——一个简单的APP模版_第10张图片 Android——一个简单的APP模版_第11张图片

将模块将分为热门城市、附件城市、全部城市、全部县四部分,并通过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("无法获取登录实例");
                }
            }
        } );

历史订单

Android——一个简单的APP模版_第12张图片

通过使用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将注册和忘记密码进行子页面滑动绑定,通过通过对数据库进行查询和插入、更新操作完成上述操作

Android——一个简单的APP模版_第13张图片 Android——一个简单的APP模版_第14张图片 Android——一个简单的APP模版_第15张图片

验证码获取以及判定

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("异常");
        }

因为还有一些细节没有完善,暂不贴出下载地址,完善之后,在评论区贴出,若有需要的,可以私信我,可以提前发;

生如蝼蚁,应当有鸿鹄之志;命如纸薄,应当有不屈之心;祝愿诸君皆为时代缔造者,相约顶峰,共赏千里桃花地,把酒言欢,畅谈古今

你可能感兴趣的:(Android,停容易手机APP,android,sqlite,数据库,android,studio,android-studio)