描述:健康问卷的题目、题目对应的选项都是由后台配置的,所以界面的布局是要根据数据来决定,使用了RecycleView实现二级列表的效果来动态布局问卷,效果图如下(录了视频,因为转GIF麻烦,所以就不上了):
拿到这个问卷,想到的问题:
- 标题中,(单选)、(多选)紧随标题的右边,两个控件要怎么布局?
- JSON数据及模型、在正常的项目中,提交数据后再次进来会显示之前的选中答案,要怎么显示呢?
- RecycleView实现二级列表呢?(注:我是用了https://blog.csdn.net/fan7983377/article/details/77970063博客的ClassAdapter实现的,在此特别感谢!)
- 如何实现单选多选的功能?
带着上诉问题,在代码中逐渐解释如何解决它们。
代码安排如下(有用到DataBinding数据绑定):
一.试卷的数据实体
/**
* 试卷实体
* Created by Administrator on 2020/3/11
*
* @author mcl
*/
public class QuestionEntity {
/**
* num : 0
* title : string
* type : 0
* option : [{"num":0,"title":"string"}]
* answer : [0]
*/
private int num;//题号
private String title;//题目
private int type;//类型单多选(2单选、1多选)
private List<OptionBean> option;//选项
private List<Integer> answer;//这题的选择答案
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public List<OptionBean> getOption() {
return option;
}
public void setOption(List<OptionBean> option) {
this.option = option;
}
public List<Integer> getAnswer() {
return answer;
}
public void setAnswer(List<Integer> answer) {
this.answer = answer;
}
public static class OptionBean {
/**
* num : 0
* title : string
*/
private int num;
private String title;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
}
二. QuestionnaireActivity(activity_questionnaire.xml布局,一个RecyclerView不贴出)
数据源是后台传过来的,一般会按照题号排好序,里面的选项也排好序的,为了测试,这里的数据我动态添加。涉及到json转换成对象,adapter中自定义点击事件接口。BaseActivity是为了DataBinding中使用统一标题栏,换成AppCompatActivity但ToolBar不要。
public class QuestionnaireActivity extends BaseActivity {
private ActivityQuestionnaireBinding mBinding;
private List<QuestionEntity> mList = new ArrayList<>();//试卷题目
private Gson gson = new Gson();
private QuestionAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getToolbar().setTvTitle("健康问卷");//换成AppCompatActivity不要
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_questionnaire);
mBinding.setToolBar(getToolbar());//换成AppCompatActivity不要
mBinding.setActivity(this);
initData();
}
//试卷内容string(JSON)转成实体
private void initData() {
//收到添加数据源,且按题号顺序添加
String zm1 = "{\"num\":\"1\",\"title\":\"是否接触过韩国、伊朗、日本人员\",\"type\":\"2\",\"option\":[{\"num\":\"1\",\"title\":\"是\",\"isAbnormal\":\"1\"},{\"num\":\"2\",\"title\":\"否\",\"isAbnormal\":\"0\"},{\"num\":\"3\",\"title\":\"去过其他国家\",\"isAbnormal\":\"0\"}],\"answer\":null}";
String zm2 = "{\"num\":\"2\",\"title\":\"是否有以下症状\",\"type\":\"1\",\"option\":[{\"num\":\"1\",\"title\":\"肠胃不适\",\"isAbnormal\":\"1\"},{\"num\":\"2\",\"title\":\"结膜炎不适\",\"isAbnormal\":\"1\"},{\"num\":\"3\",\"title\":\"感冒、发烧\",\"isAbnormal\":\"1\"},{\"num\":\"4\",\"title\":\"暂无\",\"isAbnormal\":\"0\"},{\"num\":\"5\",\"title\":\"均有以上症状\",\"isAbnormal\":\"1\"}],\"answer\":null}";
QuestionEntity entity1 = gson.fromJson(zm1, QuestionEntity.class);
mList.add(entity1);
QuestionEntity entity2 = gson.fromJson(zm2, QuestionEntity.class);
mList.add(entity2);
initAdapter();
}
private void initAdapter() {
if (null != mList && mList.size() > 0) {
adapter = new QuestionAdapter(this, mList);
mBinding.rvQuestionnaire.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mBinding.rvQuestionnaire.setAdapter(adapter);
initClick();
}
}
private void initClick() {
//自定义点击事件的监听,这里处理单选还是多选、以及保存选中的答案到mList中,提交数据时,从mList中拿自己要的
adapter.setOnItemClickListener(new QuestionAdapter.onItemClickListener() {
/**
* @param HeaderPosition 题号
* @param ContentPositionForHeader 选择的选项号 List<Integer> answer
* @param type 该题号是否是单选 单选有就移没有就留,多选有就移没有就加
*/
@Override
public void onClick(int HeaderPosition, int ContentPositionForHeader, int type) {
QuestionEntity entity = mList.get(HeaderPosition);
QuestionEntity.OptionBean bean = entity.getOption().get(ContentPositionForHeader);
if (2 == type) {//单选
List list = new ArrayList<>();
if (null != entity.getAnswer()) {
list.addAll(entity.getAnswer());
}
if (list.contains(bean.getNum())) {//答案已经存在
list.remove(0);//单选答案只有一个的
} else {//不存在
list.add(bean.getNum());
}
entity.setAnswer(list);//更新答案
} else {//多选
List list = new ArrayList();
if (null != entity.getAnswer()) {
list.addAll(entity.getAnswer());//把原有的答案全部加入
}
int zm = bean.getNum();//点击的选项
if (list.contains(zm)) {//存在,找出来把它移走
for (int i = 0; i < list.size(); i++) {
int mm = (Integer) list.get(i);
if (zm == mm) {
list.remove(i);
break;
}
}
} else {//不存在就添加
list.add(zm);
}
entity.setAnswer(list);//更新答案
}
adapter.notifyDataSetChanged();//刷新界面,因为数据源mList里面的答案有改变
}
});
}
}
三.QuestionAdapter适配器,实现二级列表、item自定义点击事件,已经选中的选项要显示成选中(圆圈是绿色,其实是一个绿色、白色的图片切换),根据当前选项是否在答案中进行确定是否选中。ClassAdapter的由来请回看问题3。
/**
* 试卷adapter,是二级列表(TopicHolder题目,OptionHolder是选项)
* Created by Administrator on 2020/3/11
*
* @author mcl
*/
public class QuestionAdapter extends ClassAdapter<QuestionAdapter.TopicHolder, QuestionAdapter.OptionHolder> {
private List<QuestionEntity> mList;
private Context mContext;
private LayoutInflater mInflater;
private onItemClickListener onItemClickListener;//自定义点击监听事件
public QuestionAdapter(Context context, List<QuestionEntity> list) {
this.mContext = context;
this.mList = list;
this.mInflater = LayoutInflater.from(context);
}
public interface onItemClickListener {
void onClick(int HeaderPosition, int ContentPositionForHeader, int type);//点击的题目、选项、类型(单多选)
}
public void setOnItemClickListener(QuestionAdapter.onItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
/**
* @return 头布局的个数
*/
@Override
public int getHeadersCount() {
if (null != mList && mList.size() > 0) {
return mList.size();
} else {
return 0;
}
}
/**
* @param headerPosition 第几个头布局
* @return 头布局对应下的子布局个数
*/
@Override
public int getContentCountForHeader(int headerPosition) {
if (null != mList && mList.size() > 0 && null != mList.get(headerPosition).getOption() && mList.get(headerPosition).getOption().size() > 0) {
return mList.get(headerPosition).getOption().size();
} else {
return 0;
}
}
/**
* 创建头布局
*
* @param parent
* @param viewType
* @return
*/
@Override
public QuestionAdapter.TopicHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType) {
return new TopicHolder(mInflater.inflate(R.layout.item_topic, parent, false));
}
/**
* 创建子布局
*
* @param parent
* @param viewType
* @return
*/
@Override
public QuestionAdapter.OptionHolder onCreateContentViewHolder(ViewGroup parent, int viewType) {
return new OptionHolder(mInflater.inflate(R.layout.item_option, parent, false));
}
/**
* 绑定头布局数据
*
* @param holder
* @param position 第几个头布局
*/
@Override
public void onBindHeaderViewHolder(QuestionAdapter.TopicHolder holder, int position) {
/*
为了使题目右边紧随单双选,且字体颜色是不一样的;一开始是想用ZZ控件(显示单双选)紧随MM(显示题目)右边,
*可是当题目多行时,处理不了,所以用同一个TextView显示,只需处理特定位置的字体颜色即可
* */
String title = (position + 1) + "." + mList.get(position).getTitle();
if (2 == mList.get(position).getType()) {//(单选)、(多选)长度都是4,所以start=title.length() - 4
title = title + "(单选)";
} else {
title = title + "(多选)";
}
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(title);
stringBuilder.setSpan(new ForegroundColorSpan(mContext.getResources().getColor(R.color.viewfinder_mask)), title.length() - 4, title.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
holder.tvTitle.setText(stringBuilder);
if (0==position){//第一条线不要显示
holder.view.setVisibility(View.INVISIBLE);
}
}
/**
* 绑定对应头布局下的子布局数据
*
* @param holder
* @param HeaderPosition 第几个头布局
* @param ContentPositionForHeader 对应HeaderPosition下的第几个子布局
*/
@Override
public void onBindContentViewHolder(QuestionAdapter.OptionHolder holder, int HeaderPosition, int ContentPositionForHeader) {
holder.tvOption.setText(mList.get(HeaderPosition).getOption().get(ContentPositionForHeader).getTitle());
List<Integer> aList = mList.get(HeaderPosition).getAnswer();
if (2 == mList.get(HeaderPosition).getType()) {//单选,把答案(只有一个)给显示出来
if (null != aList && aList.size() > 0) {
if (aList.get(0) == mList.get(HeaderPosition).getOption().get(ContentPositionForHeader).getNum()) {//答案跟选项匹配
holder.checkBox.setChecked(true);
} else {
holder.checkBox.setChecked(false);
}
} else {
holder.checkBox.setChecked(false);
}
} else {//多选,拿选项跟答案对比
if (null != aList && aList.size() > 0) {
boolean isChecked = false;//默认是不选中
for (int i = 0; i < aList.size(); i++) {
if (mList.get(HeaderPosition).getOption().get(ContentPositionForHeader).getNum() == aList.get(i)) {
isChecked = true;
break;
}
}
holder.checkBox.setChecked(isChecked);
} else {
holder.checkBox.setChecked(false);
}
}
//子布局的点击事件
holder.cl.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClickListener.onClick(HeaderPosition, ContentPositionForHeader, mList.get(HeaderPosition).getType());//自定义点击事件
}
});
}
public class TopicHolder extends RecyclerView.ViewHolder {
private TextView tvTitle;//题目
private View view;//下划线
public TopicHolder(@NonNull View itemView) {
super(itemView);
tvTitle = itemView.findViewById(R.id.tv_item_subject_title);
view=itemView.findViewById(R.id.view_item_subject);
}
}
public class OptionHolder extends RecyclerView.ViewHolder {
private ConstraintLayout cl;//整个item布局
private RoundCheckBox checkBox;//选中与不选中状态
private TextView tvOption;//选项
public OptionHolder(@NonNull View itemView) {
super(itemView);
cl = itemView.findViewById(R.id.item_cl);
checkBox = itemView.findViewById(R.id.item_option_checkBox);
tvOption = itemView.findViewById(R.id.item_option_name);
}
}
}
四.头布局item_topic.xml(一个View、一个TextView不贴了) 、子布局item_option.xml(RoundCheckBox控件只是为了让CheckBox显示圆的好看一些,不贴)代码已贴出
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/item_cl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="11dp">
<com.example.module_login.ui.wigdet.RoundCheckBox
android:id="@+id/item_option_checkBox"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginLeft="27dp"
android:button="@drawable/checkbox_bg"
android:clickable="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/item_option_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:textColor="@color/black"
android:textSize="11sp"
app:layout_constraintBottom_toBottomOf="@id/item_option_checkBox"
app:layout_constraintLeft_toRightOf="@id/item_option_checkBox"
app:layout_constraintTop_toTopOf="@id/item_option_checkBox" />
</android.support.constraint.ConstraintLayout>
补:drawable里面的checkbox_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/report_checked_icon" android:state_checked="true" />
<item android:drawable="@drawable/report_uncheck_icon" android:state_checked="false" /><!--未选中时效果-->
</selector>
一开头提出的四个问题已经在代码中解决了,如果你没有发现,那么你得再细品了!
到此已经完成了,有什么建议,评论区见!
2020.5.9补充(因为回过头发现单选时不太流畅,需要优化,所以写了扩展)
扩展(写完博客后实现了扩展,且点击什么的都很流程,代码暂时不提供):
1.提交问卷时需要全部题目已选择,单选、多选要流畅 ;
2.再次进来时显示上次选择的答案(样式灰色不可点击);
3.标题栏右上角有"修改"(或其它的样式)点击时,答案灰色变色(eg:灰色变绿色,因为前面我给定的选中样式是绿色)且可修改答案,再次提交