效果图
项目编写顺序
- 集成Retrofit网络请求插件
- 集成数据库操作Greendao插件
- 定时器编写
- 编写题目悬浮框
1、集成Retrofit网络请求插件
引用依赖并加入网络请求权限:
build.gradle
dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
//retrofit
implementation '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")
@Override
public 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);
//加入token
OkHttpClient 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>>() {
@Override
public void onResponse(final Call<RResult<ImageCode>> call, final Response<RResult<ImageCode>> response) {
//成功后的处理逻辑
}
@Override
public 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;
@Unique
private 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;
@Transient
private 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() {
@Override
public 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);
}
@Override
public 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;
}
@Override
public 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);
}
@Override
public 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.FloatingLayerView
android: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">
<RelativeLayout
android:id="@+id/rl_questionMenu"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
>
<Button
android: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" />
<TextView
android: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>
<Button
android: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" />
<Button
android: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>
<GridView
android: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;
}
@Override
public int getCount() {
return count;
}
@Override
public Object getItem(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public 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() {
@Override
public void onClick(View view) {
viewPager.setCurrentItem(position);
}
});
return view;
}
}
项目Github地址:
examOnLineApp