Android 實現答題、做題功能包含(多選、單選、材料、填空 、判斷 、問答 )以及題卡交卷查看解析功能

此博客文章爲了還之前的技術債 ,去年 8 月份已經寫了一篇答題功能的博客 ,由於種種原因那篇文章寫的比較簡單 ,也沒有能直接用的 Demo 。然後有好多朋友私信我說 demo 有問題 ,給那些陌生的朋友們帶來不便 ,不好意思 。 這不 ,我來還債來了 。

 

去年博客文章地址 :Android 實現答題 

 

鍥子

關於這個答題做題功能 ,其實我 17 年的時候就已經接觸到了 ,但是做的是高中數學的題庫 ,而且裏面數學公式比較多 ,我們採用的是 RN 實現方式 (當時還考慮到 IOS 熱更新問題 )。數學公式的圖片起初用的是圖片渲染 ,後來使用的是可汗渲染 。然後 18 做的項目也有題庫 ,裏面用的是 WebView 的方式渲染 ;19維護的項目還有題庫 ,這次用的是 Android 原生渲染 。

簡單的聊下這些實現方式 。

首先是 ReactNative 實現 ,當時 17 這個技術還屬於比較新穎的 ,WEB 端的小夥伴也是現學現用 ,我們這邊也是安裝各種環境 。當時客戶端跟 Web 端的聯調頻率還是挺高的 ,從效率考慮這不是一個優秀的方案 ;然後是技術上 ,聽 Web 端小夥伴說 ,IOS 和 Android 的同一個功能可能要寫兩套代碼 ,並沒有做到完全兼容 。

H5 實現的題庫主要是性能問題 ,比如啓動白屏時間或者響應流暢度 。對於 H5 的使用常識就是應用在一些交互不太複雜的場景 。

原生要考慮的東西主要是性能和緩存方面 ,比如一套試卷有一百多道題等等 。

 

效果(僅單選題型)

效果

 

 

正文

答題需求所實現的功能

1.各種題型組合(多選、單選、材料、填空、判斷)

2.題卡功能(點擊跳轉對應的題)

3.交卷功能(主要是保存所選的答案)

4.自己擴展(比如糾錯、查看解析、評分、倒計時、收藏等等) 

 

分析一下:

首先這些題型不會給你分的特別清,一個題型一套作業或者一套試卷是不可能的 ,每套作業都是幾種題型的組合 。接下來思考一下題庫的結構 ,因爲題型是組合的 ,所以題庫的結構會比較複雜一點 。如果是一個題型一套作業,那完全可以全部單獨寫出來 ,哈哈哈哈 。

然後看頁面設計 ,下面的題卡和交卷可以放在 Activity 裏面 ,因爲涉及到數據之間的回調 ,最好是放在頂層 ,減少不必要的數據傳遞 。題題之間的滑動跳轉 ,毫無疑問 ,唯有 ViewPager 控件可以承擔這個艱鉅的任務 。

比較難的是中間各個題型的頁面 ,使用 ViewPager 滑動沒問題 ,但是 ViewPager 裏面的頁面怎麼去處理兼容各個題型頁面 。這邊採用的是自定你 View 根據 Type 加載不同的頁面 ,類似 RecyclerView 的多佈局 。

 

上代碼分析

1. 自定義 View BaseHomeworkQuestionWidget 做爲題庫的父類 。

五百多行的代碼就不全部貼出來了 。

public abstract class BaseHomeworkQuestionWidget extends RelativeLayout {

    protected TextView tvType;
    protected TextView tvNumber;

    protected ConstraintLayout clChild;
    protected TextView tvMaterialStem;
    protected TextView tvChildType;

    protected TextView tvStem;
    protected AppCompatCheckBox checkShowAnalysis;
    protected TextView tvQuestionAnalysis;
    protected LinearLayout llQuestionAnalysis;
    protected ViewStub mAnalysisVS;

    protected Context mContext;
    protected HomeworkQuestionBean mQuestion;
    protected HomeworkQuestionBean mChildQuestion;

    //當前題目編號
    protected int mIndex;
    //總題數
    protected int mTotalNum;

    protected Pattern stemPattern = Pattern.compile("(\\[\\[[^\\[\\]]+]])", Pattern.DOTALL);

    public static final String[] CHOICE_ANSWER = {
            "A", "B", "C",
            "D", "E", "F",
            "G", "H", "I",
            "J", "K", "L"
    };

    public void setData(HomeworkQuestionBean question, int index, int totalNum) {
        mIndex = index;
        mQuestion = question;
        if (mQuestion.getType() == HomeworkQuestionTypeBean.material) {
            mChildQuestion = question.getItems().get(0);
        } else {
            mChildQuestion = question;
        }
        mTotalNum = totalNum;
    }

    public BaseHomeworkQuestionWidget(Context context) {
        super(context);
        mContext = context;
        initView(null);
    }

    public BaseHomeworkQuestionWidget(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        initView(attrs);
    }

    protected abstract void initView(AttributeSet attrs);

    /**
     * 初始化
     */
    protected void invalidateData() {
        tvType = this.findViewById(R.id.tv_type);
        tvNumber = this.findViewById(R.id.tv_number);

        clChild = this.findViewById(R.id.cl_child);
        tvMaterialStem = this.findViewById(R.id.tv_material_stem);
        tvChildType = this.findViewById(R.id.tv_child_type);

        tvStem = this.findViewById(R.id.question_stem);
        llQuestionAnalysis = this.findViewById(R.id.ll_question_analysis);
        checkShowAnalysis = this.findViewById(R.id.check_show_analysis);
        tvQuestionAnalysis = this.findViewById(R.id.tv_question_analysis);

        showQuestionTopTitle();

        SpannableStringBuilder spanned = (SpannableStringBuilder) getQuestionStem(mChildQuestion.getStem());
        spanned = EduHtml.addImageClickListener(spanned, tvStem, mContext);
        tvStem.setText(setHtmlContent(spanned));

        tvQuestionAnalysis.setText(TextUtils.isEmpty(mChildQuestion.getAnalysis()) ? "暫無解析" : Html.fromHtml(mChildQuestion.getAnalysis()));
        checkShowAnalysis.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                if (llQuestionAnalysis.isShown()) {
                    llQuestionAnalysis.setVisibility(GONE);

                    llQuestionAnalysis.setFocusable(false);
                    llQuestionAnalysis.setFocusableInTouchMode(false);
                } else {
                    llQuestionAnalysis.setVisibility(VISIBLE);

                    llQuestionAnalysis.setFocusable(true);
                    llQuestionAnalysis.setFocusableInTouchMode(true);
                    llQuestionAnalysis.requestFocus();
                }
            }
        });


        if (getContext() instanceof QuestionActivity) {
            QuestionActivity testpaperActivity = (QuestionActivity) getContext();
            ArrayList<HomeworkAnswerBean> answerList = testpaperActivity.answerList;
            //頁面切換 回填
            setAnswer(mIndex - 1, answerList);
        }
    }
}

前部部分的代碼就可以了 ,首先是所有題型的公有部分 ,比如頭部和底部 。題頁面的 頭部包括題型 ,序號索引 ;底部包括查看那解析等等 。

還有就是數據回傳邏輯  ,簡單解釋一下,數據回傳是在你做過的題頁面進行保存數據 ,有人可能會說 ViewPager 不是有緩存嗎 ,你緩存一百多個試試 。哈哈哈 。

2. 集成 BaseHomeworkQuestionWidget 實現各中題型 

  • QuestionHomeworkChoicedWidget 多選題
  • QuestionHomeworkSingleChoiceWidget 單選題
  • QuestionHomeworkEssayWidget 簡答題
  • QuestionHomeworkDetermineWidget 判斷題
  • QuestionHomeworkFillWidget 填空題

PS:在各個題型 View 根據自己的業務做處理 。

3. ViewPager 的 Adapter 根據不同的 Type 加載不同的頁面 。

public class HomeworkPagerAdapter extends PagerAdapter {


    protected LayoutInflater inflater;
    protected Context mContext;
    protected ArrayList<HomeworkQuestionBean> mList;

    public HomeworkPagerAdapter(Context context, ArrayList<HomeworkQuestionBean> list) {
        mList = list;
        mContext = context;
        inflater = LayoutInflater.from(context);
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        container.removeView((View) object);
    }


    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return view == object;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        HomeworkQuestionBean questionBean = mList.get(position);

        View view = switchQuestionWidget(questionBean, position + 1, mList.size());
        ScrollView scrollView = new ScrollView(mContext);
        scrollView.setFillViewport(true);
        scrollView.setVerticalScrollBarEnabled(false);
        scrollView.addView(view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);

        container.addView(scrollView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        return scrollView;
    }

    /**
     * 選擇不同題型
     *
     * @return
     */
    private View switchQuestionWidget(HomeworkQuestionBean question, int index, int totalNum) {
        BaseHomeworkQuestionWidget mWidget;
        HomeworkQuestionTypeBean typeBean = question.getType();
        if (typeBean == HomeworkQuestionTypeBean.material) {
            typeBean = question.getItems().get(0).getType();
        }
        int layoutId = 0;
        switch (typeBean) {
            case choice:
            case uncertain_choice:
                layoutId = R.layout.item_pager_homework_question_choice;
                break;
            case single_choice:
                layoutId = R.layout.item_pager_homework_question_singlechoice;
                break;
            case essay:
                layoutId = R.layout.item_pager_homework_question_essay;
                break;
            case determine:
                layoutId = R.layout.item_pager_homework_question_determine;
                break;
            case fill:
                layoutId = R.layout.item_pager_homework_question_fill;
                break;
            default:
                break;
        }
        mWidget = (BaseHomeworkQuestionWidget) LayoutInflater.from(mContext).inflate(layoutId, null);
        mWidget.setData(question, index, totalNum);
        return mWidget;
    }
}

PS:有什麼問題可以直接評論或者私信我 。一定回 。

 

4. 難點可能就是數據之間的問題 

作業試卷做過題的答案回傳邏輯  ;做完題的答案保存邏輯用於交卷和題卡 。

 

 

PS:在項目中比這個 demo 複雜的多 ,要處理其他很多邏輯  ,如果一個作業試卷一百多道題的話 ,還要考慮數據跟性能 。

 

 

附上 Github 地址 https://github.com/spuermax/SuperTest

 

搜索SuperTest 項目中 的代碼 

 case R.id.tv_question:
                intent2Activity(QuestionActivity.class);
                QsToast.show("123");
                break;

 

 

 

PS:有問題可以直接評論私信 ,一定回 。

 

 

 

 

 

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