1.注册:判断输入是否为空。接着访问leancloud是否存在该用户名,存在则提示用户请重新输入其他用户名,不存在则向leancloud添加用户名和密码,提示注册成功并跳转到登陆页面。
2.登陆:动态注册监听网络的广播。向SharedPreferences(下面简称sp)取用户名和密码数据并设置给文本框,如果用户名为空则设置RadioButton选中安全登陆,如果用户名和密码都不为空则设置选中快速登陆,否则设置选中记住账号。
判断输入是否为空。判断RadioButton的选中,如果选择安全登陆则sp调用remove方法删除记录并提交,如果选择记住账号则sp调用putString记录账号信息并提交,如果选择快速登陆则调用sp的putString记录用户名和密码并提交。
判断用户名密码是否正确。向leancloud查询用户名和密码,
AVQuery<AVObject> accountQuery = new AVQuery<>("exam"); accountQuery.whereEqualTo("account", account); AVQuery<AVObject> passwordQuery = new AVQuery<>("exam"); passwordQuery.whereEqualTo("password", password); AVQuery<AVObject> avQuery = AVQuery.and(Arrays.asList(accountQuery,passwordQuery));
调用avQuery的findInBackground方法查询,如果e为null且list.size大于0则表示验证成功,弹出对话框正在登陆
dialog = ProgressDialog.show(mContext,"登陆","正在登陆,请稍候!");对话框记得在activity的销毁onDestroy方法中取消显示,否则会报异常。
接着删除数据库表user_info的数据,向数据库中添加用户名密码,及生成的uuid。并将uuid保存到leancloud中,以便监听用户异地登陆强制下线使用。
如果list.size不大于0则提示用户名密码输入错误,并将一个int类型为0的数据自增,当该数据大于等于3的时候将文本框设置为不可获取焦点。并提示限制登陆。
3.欢迎界面:获取登录时携带过来的数据(yes),如果该数据等于yes,则开启服务获取数据库的uuid并通过Handler实时访问leancloud获取uuid,如果数据库的uuid和leancloud的不一致则发送广播强制下线。并在该欢迎界面的onDestroy方法中stopService停止服务。
private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what){ case 1: new Thread(){ @Override public void run() { super.run(); AVQuery<AVObject> avQuery = new AVQuery<AVObject>("exam"); avQuery.whereEqualTo("account",name); avQuery.findInBackground(new FindCallback<AVObject>() { @Override public void done(List<AVObject> list, AVException e) { for (AVObject obj : list){ String offline = obj.getString("offline"); if (!uuid.equals(offline)){ sendBroadcast(new Intent("com.example.seven.login_exam.login_exam.Broadcast.Broadcast_force_offline")); } } } }); } }.start(); Message message = handler.obtainMessage(1); handler.sendMessageDelayed(message,10000); break; case 2: handler.removeMessages(1); break; } } };并在该service的destroy方法中调用stopSelf方法并且发送handler的message为2。
展示 开始考试、查询成绩、考试规则 三大模块。点击开始考试时访问该用户的leancloud数据中的分数是否为空,为空时则跳转到考试界面,不为空时则提示已经考完试。点击查询成绩时访问该用户的leancloud数据中的分数是否为空,不为空时则跳转到查询成绩页面并携带用户名、分数、考试日期过去显示。点击考试规则时则跳转到考试规则界面。
4.考试界面:向leancloud获取要考的科目、题数、考试时间并保存到数据库中,首先从数据库获取这些信息,获取不到则从leancloud获取。用RecyclerView和CardView展示考题,用ListView展示考题是否被选中,用自定义控件SideBar索引考题。
在RecyclerView的适配器构造方法中,访问leancloud的考题题目、选项、正确答案并保存到数据库中,首先从数据库获取这些数据没有则从leancloud获取。接着从数据库获取考题题目及选项添加到考题集合,获取正确答案添加到正确答案集合。
在RecyclerView的适配器中将布局文件添加进来,在ViewHolder方法初始化控件,并设置RadioGroup被选中时候将选择的答案添加到数据库中,在这之前先清空答案表的数据。
SQLiteDatabase db = helper.getReadableDatabase(); db.delete("user_answer", null, null); rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { rb = (RadioButton) itemView.findViewById(checkedId); if (rb.isChecked()){ SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from user_answer", null); if (cursor != null && cursor.getCount() > 0){ ContentValues values = new ContentValues(); values.put("answer"+getLayoutPosition(),rb.getText().toString()); db.update("user_answer", values, null, null); values.clear(); }else { ContentValues values = new ContentValues(); values.put("answer"+getLayoutPosition(), rb.getText().toString()); db.insert("user_answer", null, values); values.clear(); } } } });设置提交按钮的点击事件。点击时调用该方法,弹出对话框是否提交答案,在positive事件中将答案表中用户选择的答案和正确答案集合比对,定义一个int类型为0的数据,如果答案表的选项为空则continue继续循环,如果和正确答案相等则该数据自增5分。并将结果 分数、当前考试日期保存到数据库中,最后跳转到查询成绩页面。
public void commit_event(){ final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setTitle("提示"); builder.setMessage("您确定提交答案吗?"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { int num = 0; SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from user_answer", null); if (cursor != null && cursor.getCount() > 0) { if (cursor.moveToFirst()) { for (int i = 0; i < answer_right_list.size(); i++) { if (TextUtils.isEmpty(cursor.getString(cursor.getColumnIndex("answer" + i)))) { continue; } if (cursor.getString(cursor.getColumnIndex("answer" + i)).equals(answer_right_list.get(i))) { num += 5; } } } } ContentValues values = new ContentValues(); SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); String date = sdf.format(new Date()); values.put("score", String.valueOf(num)); values.put("date", date); db.update("user_info", values, null, null); values.clear(); Intent intent = new Intent(mContext, ResultActivity.class); mContext.startActivity(intent); } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) {} }); builder.show(); }
将控件暴露出来给onBindViewHolder方法获取,并设置考题和选项到控件中。
最后在考试界面中设置RecyclerView的样式、适配器、第一级缓存。
//recyclerview设置样式、适配器、第一级缓存 holder.rv.setLayoutManager(new StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)); holder.rv.setAdapter(new MyRecyclerViewAdapter(this)); holder.rv.setItemViewCacheSize(20);创建ListView的适配器并设置给ListView控件,默认为暗图标,表示考题未选择。
创建自定义控件类SideBar来索引题目。模板如下:
public class SideBar extends View { public SideBar(Context context) { super(context); } public SideBar(Context context, AttributeSet attrs) { super(context, attrs); } public SideBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private String[] alphabet = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20" }; private int currentChoosenAlphabetIndex = -1; private Paint paint = new Paint(); private TextView textViewDialog = null; @Override protected void onDraw(Canvas canvas) { // 得到SideBar的高度 int viewHeight = getHeight(); // 得到SideBar的宽度 int viewWidth = getWidth(); // 每一个字母索引的高度 = SideBar的高度 / 字母索引的总个数 int heightPerAlphabet = viewHeight / alphabet.length; // 绘制每一个字母索引 for (int i = 0; i < alphabet.length; i++) { paint.setColor(Color.rgb(34, 66, 99)); // 字体颜色 paint.setTypeface(Typeface.DEFAULT_BOLD); // 设置字体 paint.setTextSize(60); // 字体大小 paint.setAntiAlias(true); // 抗锯齿 // 如果当前的字母索引被手指触摸到,那么字体颜色要进行区分 if (currentChoosenAlphabetIndex == i) { paint.setColor(Color.parseColor("#3399ff")); // 颜色进行区分 paint.setFakeBoldText(true); // 字体加粗 } /* * 绘制字体,需要制定绘制的x、y轴坐标 * * x轴坐标 = 控件宽度的一半 - 字体宽度的一半 * y轴坐标 = heightPerAlphabet * i + heightPerAlphabet */ float xPos = viewWidth / 2 - paint.measureText(alphabet[i]) / 2; float yPos = heightPerAlphabet * i + heightPerAlphabet; canvas.drawText(alphabet[i], xPos, yPos, paint); // 重置画笔,准备绘制下一个字母索引 paint.reset(); } super.onDraw(canvas); } /** * 当手指触摸的字母索引发生变化时,调用该回调接口 * * @author owen */ public interface onLetterTouchedChangeListener { public void onTouchedLetterChange(String letterTouched); } /** * 触摸字母索引发生变化的回调接口 */ private onLetterTouchedChangeListener onLetterTouchedChangeListener = null; public void setOnLetterTouchedChangeListener( onLetterTouchedChangeListener onLetterTouchedChangeListener) { this.onLetterTouchedChangeListener = onLetterTouchedChangeListener; } private onLetterTouchedChangeListener getOnLetterTouchedChangeListener() { return onLetterTouchedChangeListener; } @Override public boolean dispatchTouchEvent(MotionEvent event) { // 触摸事件的代码 int action = event.getAction(); // 手指触摸点在屏幕的y轴坐标 float touchYPos = event.getY(); // 因为currentChoosenAlphabetIndex会不断发生变化,所以用一个变量存储起来 int preChoosenAlphabetIndex = currentChoosenAlphabetIndex; onLetterTouchedChangeListener listener = getOnLetterTouchedChangeListener(); // 比例 = 手指触摸点在屏幕的y轴坐标 / SideBar的高度 // 触摸点的索引 = 比例 * 字母索引数组的长度 int currentTouchIndex = (int) (touchYPos / getHeight() * alphabet.length); if (MotionEvent.ACTION_UP == action) { // 如果手指没有触摸屏幕,SideBar的背景颜色为默认,索引字母提示控件不可见 setBackgroundDrawable(new ColorDrawable(0x00000000)); currentChoosenAlphabetIndex = -1; invalidate(); if (textViewDialog != null) { textViewDialog.setVisibility(View.INVISIBLE); } } else { // 其他情况,比如滑动屏幕、点击屏幕等等,SideBar会改变背景颜色,索引字母提示控件可见,同时需要设置内容 setBackgroundResource(R.drawable.sidebar_background); // 不是同一个字母索引 if (currentTouchIndex != preChoosenAlphabetIndex) { // 如果触摸点没有超出控件范围 if (currentTouchIndex >= 0 && currentTouchIndex < alphabet.length) { if (listener != null) { listener.onTouchedLetterChange(alphabet[currentTouchIndex]); } if (textViewDialog != null) { textViewDialog.setText(alphabet[currentTouchIndex]); textViewDialog.setVisibility(View.VISIBLE); } currentChoosenAlphabetIndex = currentTouchIndex; invalidate(); } } } // 事件在这里消耗完毕,不继续向上传递 return true; } }设置sidebar的点击事件令其RecyclerView滑动到相应的题目。并查询数据库中的答案表是否有数据,如果有则将listview中的暗图标变为亮图标。
//设置sidebar点击事件 holder.exam_sb.setOnLetterTouchedChangeListener(new SideBar.onLetterTouchedChangeListener() { @Override public void onTouchedLetterChange(String letterTouched) { holder.rv.smoothScrollToPosition(Integer.parseInt(letterTouched) - 1); SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from user_answer", null); if (cursor != null && cursor.getCount() > 0){ if (cursor.moveToNext()){ for (int i=0; i<20; i++){ String answer = cursor.getString(cursor.getColumnIndex("answer"+i)); if (!TextUtils.isEmpty(answer)) { ImageView iv = (ImageView) holder.exam_lv.getChildAt(i).findViewById(R.id.lv_image); iv.setImageResource(R.drawable.check); } } } } db.close(); } });设置RecyclerView滑动监听,滑动时查询数据库的答案表是否为数据,有数据则将listview的暗图标变为亮图标。
//recyclerview设置滑动监听 holder.rv.setOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (dy>50 || dy < -50) { SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from user_answer", null); if (cursor != null && cursor.getCount() > 0) { if (cursor.moveToNext()) { for (int i = 0; i < 20; i++) { String answer = cursor.getString(cursor.getColumnIndex("answer" + i)); if (!TextUtils.isEmpty(answer)) { ImageView iv = (ImageView) holder.exam_lv.getChildAt(i).findViewById(R.id.lv_image); iv.setImageResource(R.drawable.check); } } db.close(); } } } } });
倒计时的实现:注册广播获取服务发送过来的时间来更新UI并开启服务,服务获取时间后当时间大于等于0则每隔一秒发送一次广播,由广播接收时间更新UI实现倒计时。
服务代码如下:
public class TimeService extends Service { private MySqliteOpenHelper helper = new MySqliteOpenHelper(this,"exam.db",null,1); private int time_int; private Handler handler = new Handler(){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what){ case 1: //时间递减 time_int --; //时间大于等于0时每隔一秒发送一次广播,由广播更新UI实现倒计时 if (time_int>=0) { Intent intent = new Intent("com.example.seven.login_exam.login_exam.Activity.ExamActivity.TimeBroadcast"); intent.putExtra("time",String.valueOf(time_int)); sendBroadcast(intent); Message message = handler.obtainMessage(1); handler.sendMessageDelayed(message, 1000); } break; case 2: handler.removeMessages(1); } } }; @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("select * from course", null); //从数据库获取时间 if (cursor != null && cursor.getCount() > 0){ if (cursor.moveToFirst()) { time_int = cursor.getInt(cursor.getColumnIndex("time")); } }else{//没有则从leancloud获取 new Thread(){ @Override public void run() { super.run(); AVQuery<AVObject> avQuery = new AVQuery<>("course"); avQuery.findInBackground(new FindCallback<AVObject>() { @Override public void done(List<AVObject> list, AVException e) { for (AVObject obj : list) { time_int = obj.getInt("time"); } } }); } }.start(); } //1秒后发送 Message message = handler.obtainMessage(1); handler.sendMessageDelayed(message,1000); db.close(); return startId; } @Override public void onDestroy() { super.onDestroy(); //发送message为2停止message为1发送广播 Message message = handler.obtainMessage(2); handler.sendMessage(message); //将剩余时间保存到数据库,避免用户考试途中退出应用了可以继续显示剩余时间 SQLiteDatabase db = helper.getReadableDatabase(); ContentValues values = new ContentValues(); values.put("time",time_int); db.update("course", values, null, null); values.clear(); db.close(); //停止服务 stopSelf(); } }注册广播代码如下:
//开启服务 intent_service = new Intent(this, TimeService.class); startService(intent_service); //注册倒计时广播 receiver = new TimeBroadcast(); IntentFilter filter = new IntentFilter(); filter.addAction("com.example.seven.login_exam.login_exam.Activity.ExamActivity.TimeBroadcast"); registerReceiver(receiver,filter); } //考试时间倒计时的广播 public class TimeBroadcast extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { String time_service = intent.getStringExtra("time"); int time_int = Integer.parseInt(time_service); if (time_int>0) { Toast.makeText(mContext,"时间!"+time_int,Toast.LENGTH_SHORT).show(); long time_long = (long) time_int; long minute = time_long / 60; long second = time_long % 60; if (second < 10) { holder.exam_tv_time.setText(minute + ":0" + second); } else { holder.exam_tv_time.setText(minute + ":" + second); } }else if (time_int == 0){ Toast.makeText(mContext,"时间已到!",Toast.LENGTH_SHORT).show(); submit_exam(); } } }时间为0时则调用提交逻辑。提交逻辑代码如下:
//提交考题功能 public void submit_exam(){ SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor_right = db.rawQuery("select * from title", null); list_answer = new ArrayList<String>(); if (cursor_right != null && cursor_right.getCount() > 0){ while (cursor_right.moveToNext()){ String answer_right = cursor_right.getString(cursor_right.getColumnIndex("answer_right")); list_answer.add(answer_right); } } Cursor cursor = db.rawQuery("select * from user_answer", null); int num = 0; if (cursor != null && cursor.getCount() > 0){ if (cursor.moveToFirst()){ for (int i=0; i<list_answer.size(); i++){ if (TextUtils.isEmpty(cursor.getString(cursor.getColumnIndex("answer" + i)))) { continue; } if (cursor.getString(cursor.getColumnIndex("answer"+i)).equals(list_answer.get(i))){ num += 5; } } } } SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd"); String date = format.format(new Date()); ContentValues values = new ContentValues(); values.put("score",String.valueOf(num)); values.put("date",date); db.update("user_info", values, null, null); values.clear(); startActivity(new Intent(this, ResultActivity.class)); }设置返回键的点击事件,当点击时也调用提交逻辑。代码如下:
//设置返回键的点击事件 @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK){ AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("警告"); builder.setMessage("您确定交卷吗?"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { submit_exam(); } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); builder.show(); } return false; }
注意该考试界面销毁要解除广播注册和停止服务!!
5:查询成绩界面
首先获取intent携带过来的数据,如果有数据则代表是从欢迎界面点击查询成绩跳转过来的,就可以直接将信息设置出来。
没有intent数据则表示是考完试跳转过来的,这时从数据库获取考试分数、考试日期、考生名字设置出来并保存到leancloud中。保存代码如下:
//将分数和考试日期保存到leancloud new Thread() { @Override public void run() { super.run(); AVQuery<AVObject> avQuery = new AVQuery<AVObject>("exam"); avQuery.whereEqualTo("account", account); avQuery.findInBackground(new FindCallback<AVObject>() { @Override public void done(List<AVObject> list, AVException e) { for (AVObject obj : list) { String id = obj.getObjectId(); AVObject avObject = AVObject.createWithoutData("exam", id); avObject.put("score", score); avObject.put("date", date_db); avObject.saveInBackground(); } } }); } }.start();
设置返回键的点击事件,让其回到欢迎界面防止回到考试界面,这时欢迎界面的启动模式要设置成singletask。当查询成绩界面销毁时将数据库的考试时间重新设置为1小时。
//设置返回键的点击事件,跳转到欢迎界面,避免重新回到考试界面,将欢迎界面的启动模式设置为single task。 @Override public void onBackPressed() { super.onBackPressed(); finish(); startActivity(new Intent(this, WelcomeActivity.class)); } //查询成绩界面销毁时将数据库的时间重新设置为原来的一小时。 @Override protected void onDestroy() { super.onDestroy(); SQLiteDatabase db_course = helper.getReadableDatabase(); ContentValues values = new ContentValues(); values.put("time", 3600); db_course.update("course", values, null, null); }
数据表: leancloud有三个表:exam(account String、password String、date String、score String、offline String)
course(course String、amount Number、score Number、time Number)
title(title String、option_A String、option_B String、option_C String、option_D String、answer_right String)
Sqlite有四个表:
private final String COURSE = "create table course(course_id integer,course text,amount integer,score integer,time integer)"; private final String TITLE = "create table title(title text,option_A text," + "option_B text,option_C text,option_D text,answer_right text,score integer)"; private final String USER_ANSWER = "create table user_answer(answer0 text,answer1 text,answer2 text,answer3 text," + "answer4 text,answer5 text,answer6 text,answer7 text,answer8 text,answer9 text,answer10 text,answer11 text," + "answer12 text,answer13 text,answer14 text,answer15 text,answer16 text,answer17 text,answer18 text,answer19 text)"; private final String USER_INFO = "create table user_info(account text,password text,score text,date text,uuid text)";