親測實現:RecycleView健康問卷佈局(題目、選項動態)、邏輯(單選、多選功能、提交答案、顯示上次選擇的答案)

描述:健康問卷的題目、題目對應的選項都是由後臺配置的,所以界面的佈局是要根據數據來決定,使用了RecycleView實現二級列表的效果來動態佈局問卷,效果圖如下(錄了視頻,因爲轉GIF麻煩,所以就不上了):

  

拿到這個問卷,想到的問題:

  1. 標題中,(單選)、(多選)緊隨標題的右邊,兩個控件要怎麼佈局?
  2. JSON數據及模型、在正常的項目中,提交數據後再次進來會顯示之前的選中答案,要怎麼顯示呢?
  3. RecycleView實現二級列表呢?(注:我是用了https://blog.csdn.net/fan7983377/article/details/77970063博客的ClassAdapter實現的,在此特別感謝!)
  4. 如何實現單選多選的功能? 

帶着上訴問題,在代碼中逐漸解釋如何解決它們。 

 代碼安排如下(有用到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:灰色變綠色,因爲前面我給定的選中樣式是綠色)且可修改答案,再次提交

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章