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:有问题可以直接评论私信 ,一定回 。

发布了78 篇原创文章 · 获赞 63 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_37492806/article/details/104939940