Android课程项目:寻求替换嵌套RecyclerView的通用问卷实现方案
优化问卷RecyclerView方案:单RecyclerView + 多类型Item
嘿,我完全懂你嵌套RecyclerView做问卷的那种怪异感——之前做类似项目时也踩过这个坑,滚动时偶尔会有小卡顿,而且代码结构也显得冗余。给你推荐一个更通用、更优雅的方案:单RecyclerView + 多类型Item,完美适配不同问题类型和可变选项的需求,还能避开嵌套的各种问题。
核心思路
把每个不同类型的问题(单选、多选、填空等)看作RecyclerView的一种Item类型,每个Item内部直接承载对应的选项布局,不需要嵌套子RecyclerView。这样既保持了通用性,又能让代码逻辑更清晰。
步骤1:定义统一的问题数据模型
先创建一个问题基类,再为每种问题类型实现子类,子类中包含该类型特有的数据(比如选项列表、选中状态等):
// 所有问题的基类 abstract class Question(val id: String, val title: String) // 单选问题子类 class SingleChoiceQuestion( id: String, title: String, val options: List<String>, var selectedOption: String? = null ) : Question(id, title) // 多选问题子类 class MultipleChoiceQuestion( id: String, title: String, val options: List<String>, val selectedOptions: MutableList<String> = mutableListOf() ) : Question(id, title) // 填空问题子类 class TextInputQuestion( id: String, title: String, var inputText: String = "" ) : Question(id, title)
步骤2:实现多类型Adapter
在RecyclerView Adapter中,通过getItemViewType()区分不同问题类型,为每种类型创建对应的ViewHolder:
class QuestionnaireAdapter(private val questions: List<Question>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { companion object { // 定义不同问题类型对应的ViewType private const val VIEW_TYPE_SINGLE_CHOICE = 0 private const val VIEW_TYPE_MULTIPLE_CHOICE = 1 private const val VIEW_TYPE_TEXT_INPUT = 2 } override fun getItemViewType(position: Int): Int { return when (questions[position]) { is SingleChoiceQuestion -> VIEW_TYPE_SINGLE_CHOICE is MultipleChoiceQuestion -> VIEW_TYPE_MULTIPLE_CHOICE is TextInputQuestion -> VIEW_TYPE_TEXT_INPUT else -> throw IllegalArgumentException("未知的问题类型") } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val inflater = LayoutInflater.from(parent.context) return when (viewType) { VIEW_TYPE_SINGLE_CHOICE -> { val view = inflater.inflate(R.layout.item_single_choice, parent, false) SingleChoiceViewHolder(view) } VIEW_TYPE_MULTIPLE_CHOICE -> { val view = inflater.inflate(R.layout.item_multiple_choice, parent, false) MultipleChoiceViewHolder(view) } VIEW_TYPE_TEXT_INPUT -> { val view = inflater.inflate(R.layout.item_text_input, parent, false) TextInputViewHolder(view) } else -> throw IllegalArgumentException("未知的ViewType") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val question = questions[position] when (holder) { is SingleChoiceViewHolder -> holder.bind(question as SingleChoiceQuestion) is MultipleChoiceViewHolder -> holder.bind(question as MultipleChoiceQuestion) is TextInputViewHolder -> holder.bind(question as TextInputQuestion) } } override fun getItemCount() = questions.size // 单选问题的ViewHolder,负责绑定数据和交互 class SingleChoiceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvTitle: TextView = itemView.findViewById(R.id.tv_question_title) private val rgOptions: RadioGroup = itemView.findViewById(R.id.rg_options) fun bind(question: SingleChoiceQuestion) { tvTitle.text = question.title // 清空之前的选项,避免复用问题 rgOptions.removeAllViews() question.options.forEach { option -> val radioBtn = RadioButton(itemView.context).apply { text = option isChecked = option == question.selectedOption setOnCheckedChangeListener { _, isChecked -> if (isChecked) question.selectedOption = option } } rgOptions.addView(radioBtn) } } } // 多选问题的ViewHolder class MultipleChoiceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvTitle: TextView = itemView.findViewById(R.id.tv_question_title) private val llOptions: LinearLayout = itemView.findViewById(R.id.ll_options_container) fun bind(question: MultipleChoiceQuestion) { tvTitle.text = question.title llOptions.removeAllViews() question.options.forEach { option -> val checkBox = CheckBox(itemView.context).apply { text = option isChecked = question.selectedOptions.contains(option) setOnCheckedChangeListener { _, isChecked -> if (isChecked) question.selectedOptions.add(option) else question.selectedOptions.remove(option) } } llOptions.addView(checkBox) } } } // 填空问题的ViewHolder class TextInputViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val tvTitle: TextView = itemView.findViewById(R.id.tv_question_title) private val etAnswer: EditText = itemView.findViewById(R.id.et_answer_input) fun bind(question: TextInputQuestion) { tvTitle.text = question.title etAnswer.setText(question.inputText) etAnswer.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { question.inputText = s.toString() } }) } } }
步骤3:创建对应布局文件
为每种问题类型创建简单的布局,比如:
item_single_choice.xml:包含一个TextView(标题)和RadioGroup(选项容器)item_multiple_choice.xml:包含TextView和LinearLayout(选项容器)item_text_input.xml:包含TextView和EditText
布局可以根据你的UI需求自定义样式,保持统一即可。
方案优势
- 避免嵌套问题:没有子RecyclerView的滚动冲突、重复测量等性能问题
- 高通用性:新增问题类型只需要添加对应的子类、ViewHolder和布局,无需修改核心逻辑
- 逻辑清晰:每种问题的UI和数据绑定都封装在各自的ViewHolder中,代码模块化
- 易维护:后续修改某类问题的交互或样式,只需要调整对应的ViewHolder和布局
额外优化建议
- 如果某类问题的选项数量特别多,可以考虑在ViewHolder内部使用小型RecyclerView承载选项,但一般问卷选项不会过多,线性布局足够
- 可以把选项的View创建逻辑封装成工具方法,避免ViewHolder代码冗余
- 为所有问题布局添加统一的间距、标题样式,保证UI一致性
内容的提问来源于stack exchange,提问作者Rafael Lopes
相关产品推荐
相关产品推荐

