描述:健康問卷的題目、題目對應的選項都是由後臺配置的,所以界面的佈局是要根據數據來決定,使用了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:灰色變綠色,因爲前面我給定的選中樣式是綠色)且可修改答案,再次提交