效果图




项目编写顺序

  1. 集成Retrofit网络请求插件
  2. 集成数据库操作Greendao插件
  3. 定时器编写
  4. 编写题目悬浮框

1、集成Retrofit网络请求插件

引用依赖并加入网络请求权限:
build.gradle

dependencies {implementation 'io.reactivex.rxjava2:rxjava:2.2.1'implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'//retrofitimplementation 'com.squareup.retrofit2:retrofit:2.3.0'
}

AndroidManifest.xml

   <uses-permission android:name="android.permission.INTERNET" />

因为后台是根据jwt方式进行请求的,我们要构建两个Retrofit:一个有token的请求方式;一个没有token的请求方式。这里使用工厂模式,详细代码请看git地址。

只展示有token的创建方式。

public class RetrofitHaveTokenBuilder<T> implements IBuilder{@SuppressWarnings("unchecked")@Overridepublic T createRetrofitService() {OkHttpClient.Builder builder = new OkHttpClient.Builder();builder.connectTimeout(ApiStores.DEFAULT_TIMEOUT, TimeUnit.SECONDS);builder.writeTimeout(ApiStores.DEFAULT_TIMEOUT, TimeUnit.SECONDS);builder.readTimeout(ApiStores.DEFAULT_TIMEOUT, TimeUnit.SECONDS);//加入tokenOkHttpClient okClient = builder.addInterceptor(new RetrofitTokenInterceptor()).build();//构建Retrofit实例Retrofit retrofit = new Retrofit.Builder()//设置网络请求BaseUrl地址.baseUrl(ApiStores.API_SERVER_URL)//设置数据解析器.addConverterFactory(GsonConverterFactory.create()).client(okClient).build();ApiStores apiStores = retrofit.create(ApiStores.class);return (T) apiStores;}
}

使用:
定义接口:

    /*** 验证码*/@GET("auth/code")Call<RResult<ImageCode>> getImageCode();

请求接口:

 ICreate iCreate = new RetrofitNoTokenFactory();ApiStores retrofitService = (ApiStores) iCreate.getRetrofit().createRetrofitService();mImageCodeCall = retrofitService.getImageCode();mImageCodeCall.enqueue(new Callback<RResult<ImageCode>>() {@Overridepublic void onResponse(final Call<RResult<ImageCode>> call, final Response<RResult<ImageCode>> response) {//成功后的处理逻辑  }@Overridepublic void onFailure(Call<RResult<ImageCode>> call, Throwable t) {System.out.println(t.getMessage());Toast.makeText(LoginActivity.this, "接口调用异常!", Toast.LENGTH_SHORT).show();}});

2、集成数据库操作Greendao插件

引用依赖
build.gradle

apply plugin: 'org.greenrobot.greendao'
android {//greendao配置greendao {//版本号,升级时可配置schemaVersion 1}
}
dependencies {//GreenDao3依赖 数据库操作implementation 'org.greenrobot:greendao:3.2.2'
}

每次升级时,需要修改schemaVersion 的版本号!

使用:
创建实体类:

@Entity
public class QbQuestion {//注意此处只能是Long,否则插入不成功哦!@Id(autoincrement = true) private Long questionId;@Uniqueprivate String id;private String paperId;private String title;private String typeCode;private Double score;@Convert(columnType = String.class, converter = StringConverter.class)private List<String> answers;private Integer sort;@Convert(columnType = String.class, converter = StringConverter.class)private List<String> studentAnswers;private Double studentScore;private Boolean bingo;private Boolean deleted;@Transientprivate List<QbQuestionOption> options;// get set方法省略...
}

注意:id必须是Long类型,否则插入不成功会报错!
数据库操作:

//获取数据库dao
DaoSession   daoSession = DBManager.getInstance(this).getDaoSession();
QbQuestionDao qbQuestionDao = daoSession.getQbQuestionDao();
//查询
List<QbQuestion> questionList = qbQuestionDao.queryBuilder().where(QbQuestionDao.Properties.PaperId.eq(id)).list();
//批量插入
qbQuestionDao.insertOrReplaceInTx(questionList);

3、定时器编写

    static int minute = -1;static int second = -1;Timer timer;TimerTask timerTask;//学生剩余时长 单位是秒static int studentDuration = 0;Handler handler = new Handler() {public void handleMessage(Message msg) {if (minute == 0) {if (second == 0) {tv_timer.setText("正在提交!");if (timer != null) {timer.cancel();timer = null;}if (timerTask != null) {timerTask = null;}minute = -1;second = -1;submitPapers();} else {second--;if (second >= 10) {tv_timer.setText("0" + minute + ":" + second);} else {tv_timer.setText("0" + minute + ":0" + second);}if (second == 10 * 60) {Toast.makeText(TestActivity.this, "距离考试结束还有十分钟!", Toast.LENGTH_SHORT).show();}}} else {if (second == 0) {second = 59;minute--;if (minute >= 10) {tv_timer.setText(minute + ":" + second);} else {tv_timer.setText("0" + minute + ":" + second);}} else {second--;if (second >= 10) {if (minute >= 10) {tv_timer.setText(minute + ":" + second);} else {tv_timer.setText("0" + minute + ":" + second);}} else {if (minute >= 10) {tv_timer.setText(minute + ":0" + second);} else {tv_timer.setText("0" + minute + ":0" + second);}}}}}};/*** 开始倒计时** @param duration 考试时长 单位秒*/private void beginTimer(int duration) {if (minute == -1 && second == -1) {minute = duration/60;second = duration%60;}tv_timer.setText(minute + ":" + second);timerTask = new TimerTask() {@Overridepublic void run() {Message msg = new Message();msg.what = 0;handler.sendMessage(msg);}};timer = new Timer();timer.schedule(timerTask, 0, 1000);}

4、编写题目悬浮框

自定义view:


import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;import com.starcity.R;public class FloatingLayerView extends LinearLayout implements View.OnTouchListener {/*** 视图显示类型。*/private int type = NONE;/*** 浮层的高度。*/private int floating_height;/*** 浮层的宽度*/private int floating_width;/*** 滑动高度*/private float move_height;/*** 是否向下滑动,交由onTouch事件处理。*/private boolean isCanHide = false;/*** 是否进行动画*/private boolean isCanAnimation = false;/*** 触发拦截触摸事件时的坐标点。* 按下:* interceptTouch_X:按下时的X坐标点。* interceptTouch_Y:按下时的Y坐标点。* 滑动:* interceptMove_X:滑动时的X坐标点。* interceptMove_Y:滑动时的Y坐标点。* 距离:* interceptTouch_Move_X:从按下到滑动之间的距离(横向滑动)* interceptTouch_Move_Y:从按下到滑动之间的距离(纵向滑动)* 滑动距离:* moveLength:根据此值判断是否进行了滑动。*/private float interceptTouch_X;private float interceptTouch_Y;private float interceptMove_X;private float interceptMove_Y;private float interceptTouch_Move_X;private float interceptTouch_Move_Y;private int moveLength = 10;/*** 触发触摸事件时的坐标点* down_X:按下时的X坐标点。* down_Y:按下时的Y坐标点。* move_X:移动时的X坐标点。* move_Y:移动时的Y坐标点。* down_move_X:横向滑动的距离。* down_move_Y:纵向滑动的距离。*/private float down_X;private float down_Y;private float move_X;private float move_Y;private float down_move_X;private float down_move_Y;/*** 定义三种浮层显示类型* 0:不显示 1:显示一半 2:全部显示*/public static final int NONE = 0;public static final int HALF = 1;public static final int ALL = 2;/*** 获取菜单的高度来设置要显示的高度** @param context* @param attrs*/private View showView;public FloatingLayerView(Context context, AttributeSet attrs) {super(context, attrs);setOnTouchListener(this);}public FloatingLayerView(Context context) {super(context);}@Overridepublic void onWindowFocusChanged(boolean hasWindowFocus) {if (hasWindowFocus) {floating_width = getWidth();floating_height = getHeight();/*** 每次滑动的距离是当前View宽度的三分之一。*/move_height = floating_height / 3;}super.onWindowFocusChanged(hasWindowFocus);}public void setShowView(View showView) {this.showView = showView;}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {switch (ev.getAction()) {/*** 当按下时获取x,y的坐标点。*/case MotionEvent.ACTION_DOWN:interceptTouch_X = ev.getX();interceptTouch_Y = ev.getY();isCanAnimation = true;break;/*** 当滑动时操作如下:* 1、获取滑动距离* 2、判断向上滑动还是向下滑动* 3、向上滑动时* 4、向下滑动时,判断当前显示方式:* (1)、显示一半时,交由onTouch事件处理。* (2)、全部显示时,是否向下滑动交由当前View的子View处理,* 是否交由onTouch事件处理。*/case MotionEvent.ACTION_MOVE:interceptMove_X = ev.getX();interceptMove_Y = ev.getY();interceptTouch_Move_X = Math.abs(interceptTouch_X - interceptMove_X);interceptTouch_Move_Y = Math.abs(interceptTouch_Y - interceptMove_Y);/*** 向下滑动*/if (interceptMove_Y > interceptTouch_Y && interceptTouch_Move_Y > moveLength && interceptTouch_Move_Y > interceptTouch_Move_X) {return isDounTransferOnTouch();}/*** 向上滑动*/if (interceptTouch_Y > interceptMove_Y && interceptTouch_Move_Y > moveLength && interceptTouch_Move_Y > interceptTouch_Move_X) {return isUpTransferOnTouch();}break;case MotionEvent.ACTION_UP:break;default:break;}return super.onInterceptTouchEvent(ev);}@Overridepublic boolean onTouch(View v, MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:break;/*** 当滑动时动画操作*/case MotionEvent.ACTION_MOVE:down_X = interceptTouch_X;down_Y = interceptTouch_Y;move_X = event.getX();move_Y = event.getY();down_move_X = Math.abs(down_X - move_X);down_move_Y = Math.abs(down_Y - move_Y);/*** 向下滑动*/if (move_Y > down_Y && down_move_Y > moveLength && getCanAnimation()) {downAnimationConfig();}/*** 向上滑动*/if (down_Y > move_Y && down_move_Y > moveLength && getCanAnimation()) {upAnimationConfig();}/*** 执行完上面动画处理后,停止执行动画*/setCanAnimation(false);break;case MotionEvent.ACTION_UP:break;default:break;}return true;}/*** 是否进行动画处理** @return true:处理*/private boolean getCanAnimation() {return isCanAnimation;}/*** 获取当前视图显示类型** @return*/public int getType() {return type;}private void setType(int type) {this.type = type;}/*** 设置是否进行动画处理** @param canAnimation*/private void setCanAnimation(boolean canAnimation) {this.isCanAnimation = canAnimation;}/*** 向下滑动时的动画处理*/private void downAnimationConfig() {switch (getType()) {case HALF://当视图显示一半时half2None();break;case ALL://当视图全部显示时all2Half();break;default:break;}}/*** 向上滑动时的动画处理*/private void upAnimationConfig() {switch (getType()) {case HALF://当视图显示一半时half2All();break;case ALL://当视图全部显示时/*** 当视图已经完整显示,再往* 上滑动也就没任何意义进行* 动画处理。*/break;default:break;}}/*** 向下滑动时是否交由onTouch事件处理** @return true:由onTouch事件处理,不传递给子View*/private boolean isDounTransferOnTouch() {switch (type) {case NONE:break;case HALF:return true;case ALL:if (isCanHide) {return true;}break;default:break;}return false;}/*** 向上滑动时是否交由onTouch事件处理** @return true:由onTouch事件处理,不传递给子View*/private boolean isUpTransferOnTouch() {switch (type) {case NONE:break;case HALF:return true;case ALL:break;default:break;}return false;}/*** 当点击隐藏时,当前视图显示全部,直接隐藏。* type设置为NONE*/private void all2None() {int showHeight=0;if(showView!=null){showHeight=showView.getHeight();}float[] values = new float[]{0, getHeight() - showHeight};startAnimation(values);setType(NONE);}/*** 当向下滑动时,当前视图显示一半,再往下滑动隐藏。* type设置为NONE*/private void half2None() {int showHeight=0;if(showView!=null){showHeight=showView.getHeight();}float[] values = new float[]{move_height, getHeight() - showHeight};startAnimation(values);setType(NONE);}/*** 当向上滑动时,当前视图显示完整,再往下滑动视图显示一半。* type设置为HALF*/private void all2Half() {float[] values = new float[]{0, move_height};startAnimation(values);setType(HALF);}/*** 当向上滑动时,当前视图显示一半,再往上滑动,视图显示完整。* type设置为ALL*/private void half2All() {float[] values = new float[]{move_height, 0};startAnimation(values);setType(ALL);}/*** 执行动画** @param values*/@TargetApi(Build.VERSION_CODES.HONEYCOMB)private void startAnimation(float[] values) {AnimatorSet as = new AnimatorSet();ObjectAnimator anim = ObjectAnimator.ofFloat(this, "translationY", values);anim.setDuration(500);as.playTogether(anim);as.start();}/*** 隐藏浮层*/public void beforeInput() {switch (getType()) {case NONE:break;case HALF:half2None();break;case ALL:all2None();break;default:break;}}/*** 显示浮层一半*/public void none2Half() {float[] values = new float[]{getHeight(), move_height};startAnimation(values);setType(HALF);}/*** 显示全部浮层*/public void none2All() {float[] values = new float[]{getHeight(), 0};startAnimation(values);setType(HALF);}/*** 是否进行动画滚动** @param canHide*/public void setCanHide(boolean canHide) {this.isCanHide = canHide;}

自定义view使用:

   <!-- 覆盖层 --><com.starcity.view.FloatingLayerViewandroid:id="@+id/activity_shine_ll_cover"android:layout_width="match_parent"android:layout_height="400dp"android:layout_alignParentBottom="true"android:background="@color/white"android:orientation="vertical"android:translationY="350dp"android:visibility="visible"><RelativeLayoutandroid:id="@+id/rl_questionMenu"android:layout_width="match_parent"android:layout_height="50dp"android:orientation="horizontal"><Buttonandroid:id="@+id/btn_questionFloat"android:layout_width="30dp"android:layout_height="30dp"android:layout_gravity="center_vertical"android:layout_centerVertical="true"android:layout_marginLeft="20dp"android:background="@drawable/question" /><TextViewandroid:id="@+id/tv_questionNum"android:layout_width="wrap_content"android:layout_toRightOf="@id/btn_questionFloat"android:layout_height="wrap_content"android:text="1/50"android:textSize="20sp"android:textColor="#000000"android:layout_centerVertical="true"android:layout_marginLeft="6dp"></TextView><Buttonandroid:id="@+id/toLastQuestion"android:layout_toLeftOf="@+id/toNextQuestion"android:layout_width="30dp"android:layout_height="30dp"android:layout_centerVertical="true"android:background="@drawable/btn_last_question" /><Buttonandroid:id="@+id/toNextQuestion"android:layout_width="30dp"android:layout_height="30dp"android:layout_alignParentRight="true"android:layout_marginRight="20dp"android:layout_marginLeft="14dp"android:layout_centerVertical="true"android:background="@drawable/btn_next_question" /></RelativeLayout><GridViewandroid:id="@+id/activity_shine_gv_all"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_marginLeft="20dp"android:layout_marginRight="20dp"android:horizontalSpacing="4dp"android:listSelector="@null"android:numColumns="7"android:verticalSpacing="8dp"></GridView></com.starcity.view.FloatingLayerView>

adpter编写:

/*** 下面显示的所有题目索引*/
public class ChooseQuestionAdapter extends BaseAdapter {private Context context;private int count;private ViewPager viewPager;public ChooseQuestionAdapter(Context context, int count, ViewPager viewPager) {this.context = context;this.count = count;this.viewPager = viewPager;}@Overridepublic int getCount() {return count;}@Overridepublic Object getItem(int position) {return position;}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {View view = LayoutInflater.from(context).inflate(R.layout.list_item_fragment_question_bt, null);Button imagView = (Button) view.findViewById(R.id.iv_show);QbQuestion qbQuestion = TestActivity.qbQuestionList.get(position);List<String> studentAnswers = qbQuestion.getStudentAnswers();if(position==8){System.out.println(position);}if (studentAnswers != null && studentAnswers.size() > 0) {imagView.setBackgroundResource(R.drawable.option_btn_single_checked);} else if (position == TestActivity.currentIndex) {imagView.setBackgroundResource(R.drawable.option_btn_single_normal);} else {imagView.setBackgroundResource(R.drawable.btn_question_circle_white);}imagView.setText((position + 1) + "");imagView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {viewPager.setCurrentItem(position);}});return view;}}

项目Github地址:
examOnLineApp

Android的在线考试app相关推荐

  1. 安卓使用网络技术实战——简易的在线考试app

    文章目录 一.实现内容 二.用到的部分知识点及参考 1.实现界面跳转并传递数据--使用Intent 2.控件的隐藏与显示 3.通过Handler把需要在另一个线程执行的操作加入到消息队列中去 4.使用 ...

  2. 基于uni-app的安卓在线考试系统软件设计

    随着编程技术的进步,出现了越来越多的跨平台开发语言和框架,比较常用的有Flutter.uni-app.Ionic.React native等,在我的开发实践中感觉Flutter虽然接近原生,谷歌独创语 ...

  3. 基于Android课程学习在线考试系统APP

    课程学习在线考试系统APP采用结构化设计方法,可以实现用户注册登陆.查看教程,在线测试,错题收集,视频观看,在线交流.利用RecyclerView等进行界面布局.用户可登录注册系统,在线浏览相关操作系 ...

  4. 微信小程序 springboot在线考试系统Android hbuilderx App毕业设计

    本系统包含微信小程序前台和Java做的后台管理系统,该后台采用前后台前后分离的形式使用Java+VUE 微信小程序--前台涉及技术:WXML 和 WXSS.JavaScript.uniapp Java ...

  5. 在线学习与考试App源码

    在线学习与考试App源码 1.支持PC web.Android App.IOS  App 考试,手机移动端支持离线练习考试,多端数据同步: 2.支持多种判断.单选.多选.填空.简单等多种题型,同题干多 ...

  6. 在线打包app平台,Android春招实习面试经验汇总

    免费在线制作App的无线应用开发工具集合 -追信魔盒成就全球最大的手机软件在线制作平台 不会编程?教你用开发工具制作高下载量的App! 现在很多站长,商家和个人都想自己开发优质的App,利用高下载量去 ...

  7. 基于Android实现的小型在线订餐APP饿了么

    源码及论文下载:http://www.byamd.xyz/tag/android/ 1.1 研究背景 进入到移动互联时代,人们会愈加频繁地使用手机,安卓系统占据了智能手机界很大市场份额,安卓开发,在今 ...

  8. 在线聊天App研发说明(android学习总结)

    初识android 确定学习android之后,开始动手,跟之前做过android的同事,要了开发环境和一本电子书<深入浅出Android--Google手持设备应用程序设计>.书很薄只有 ...

  9. 基于android驾校模拟考试系统app

    驾驶理论考试就是在线考试的一个实际应用,它实现了理论考试的无纸化,以往出题.印试卷.批改试卷等繁琐的工作,现在都可以由计算机来替代.本系统有驾校模拟考试功能,包括用户管理及试题库管理.试卷管理和随机出 ...

最新文章

  1. 海思3559A上编译FFmpeg源码操作步骤
  2. css编写要注意什么 及一些公用的样式和外部引用 转码
  3. TX Pattern Generator功能块
  4. i3处理器_十代酷睿i3-10100F配GTX1650S组装机配置清单
  5. SQLite数据库管理工具(SQLiteStudio)v3.1.1
  6. FetchType.LAZY和FetchType.EAGER什么区别?(懒加载和急加载的理解)
  7. 科大星云诗社动态20210525
  8. 蓝桥杯-代码-数字三角形
  9. codeblock不能调试
  10. 《YOLO算法笔记》(草稿)
  11. 四款855旗舰对比:除开价格,各有优点
  12. 渗透攻击exp共享站点
  13. 恢复初始快捷键_CAD常用命令快捷键大全,47个快捷键50个CAD技巧,教你快速画图...
  14. 来到深圳奋斗的这些年(不断更新!)
  15. ArcMAP 空间连接和相交工具使用
  16. 全国离线地图矢量地图矢量数据点线面数据
  17. Face Recognition 库-人脸识别
  18. 手机app开发(新手教程)
  19. spring boot启动报错:Reason: Canonical names should be kebab-case (‘-‘ separated), lowercase
  20. ROC曲线面积AUC详解

热门文章

  1. 问题解决:系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的about_Execution_Policies
  2. 如何注册和申请阿里云域名?
  3. ct上的img表示什么_X线/CT/MR影像片子上的标识你是否都认识?
  4. 秉持技术普惠的华为,致力于无处不在的联接
  5. 微信小程序 - 二维码数据解析,如何扫码进入开发版测试二维码数据
  6. android 开发sdk 找不到java类_开发Cordova插件(Android)时找不到源Java类
  7. 重磅成果丨ASAM SOVD 1.0.0正式发布
  8. 二十六、多线程练习题
  9. 歌尔首次闪耀CES Asia,展示全面创新力量
  10. Android手机减少微信步数,iPhone微信步数竟然比安卓少很多,原因已明确!