Kotlin RecyclerView单选Checkbox异常:题目重叠隐藏修复求助
Kotlin RecyclerView问卷功能异常排查与修复
问题描述
我用Kotlin基于RecyclerView实现问卷功能,每个问题对应不同的Checkbox选项组(比如部分是YES/NO,部分是IVSS/DSS,还有的是NA/SI/NO),已经实现每题仅能勾选一个选项的互斥逻辑,但出现两个异常:
- 勾选第1题的选项时,第1题会被其他问题覆盖隐藏;
- 部分题目显示了错误的选项组。
相关代码
preusoadapter.kt
class preusoadapter( private val context : Context, private val listpreguntaspreuso: ArrayList<epreguntas> ) : RecyclerView.Adapter<preusoadapter.PreUsoViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreUsoViewHolder { val layoutInflater = LayoutInflater.from(context) return PreUsoViewHolder(layoutInflater.inflate(R.layout.activity_estructura_listapreuso, parent, false) ) } override fun onBindViewHolder(holder: PreUsoViewHolder, position: Int) { val item = listpreguntaspreuso[position] holder.render(item) holder.displayChecked(item.answer) if (position == 2) { holder.displayAnswers(setOf(Answer.IVSS, Answer.DSS)) } else if (position == 6) { holder.displayAnswers(setOf(Answer.FRESERV, Answer.FREDMANO)) } else if (position == 14) { holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO)) } else if (position == 18) { holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO)) } else if (position == 22) { holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO)) } else if (position == 25) { holder.displayAnswers(setOf(Answer.NA, Answer.SI, Answer.NO)) } } override fun getItemCount(): Int = listpreguntaspreuso.size //CLASE INTERNA PREUSOVIEWHOLDER// inner class PreUsoViewHolder(view: View) : RecyclerView.ViewHolder(view) { val binding = ActivityEstructuraListapreusoBinding.bind(view) private val idpregunta = view.findViewById<TextView>(R.id.txtidpregunta) private val numeropregunta = view.findViewById<TextView>(R.id.txtnumeropregunta) private val pregunta = view.findViewById<TextView>(R.id.txtpreguntas) private val imgestado = view.findViewById<ImageView>(R.id.icosemaforo) fun render (epreguntas: epreguntas){ idpregunta.text = epreguntas.id_pregunta numeropregunta.text = epreguntas.num_pregunta pregunta.text = epreguntas.pregunta Glide.with(imgestado.context).load(epreguntas.icono_estado).into(imgestado) } private val checkboxAnswers = mapOf( binding.chbksi to Answer.SI, binding.chbkno to Answer.NO, binding.chbkna to Answer.NA, binding.chbkIVSS to Answer.IVSS, binding.chbkDSS to Answer.DSS, binding.chbkFSERV to Answer.FRESERV, binding.chbkfmano to Answer.FREDMANO ) init { // set the listener on all the checkboxes checkboxAnswers.keys.forEach { checkbox -> checkbox.setOnClickListener { handleCheckboxClick(checkbox) } } } // A function that handles all the checkboxes private fun handleCheckboxClick(checkbox: CheckBox) { // get the item for the position the VH is displaying val item = listpreguntaspreuso[adapterPosition] // update the item's checked state with the Answer associated with this checkbox // If it's just been -unchecked-, then that means nothing is checked checkboxAnswers[checkbox]?.let { answer -> item.answer = if (!checkbox.isChecked) null else answer // remember to notify the adapter (so it can redisplay and uncheck any other boxes) notifyItemChanged(adapterPosition) } } fun displayChecked(answer: Answer?) { // set the checked state for all the boxes, checked if it matches the answer // and unchecked otherwise. // Setting every box either way clears any old state from the last displayed item checkboxAnswers.forEach { (checkbox, answerType) -> checkbox.isChecked = answerType == answer } } fun displayAnswers(answers: Collection<Answer>) { // iterate over each checkbox/answer pair, hiding or displaying as appropriate checkboxAnswers.forEach { (checkbox, answerType) -> checkbox.visibility = if (answerType in answers) View.VISIBLE else View.GONE } } }
epreguntas.kt
class epreguntas( var id_pregunta: String, var num_pregunta: String, var pregunta : String, var icono_estado: String, var checkvalor: Boolean = false, var answer: Answer? = null ) { }
RecyclerView布局文件
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto" android:animateLayoutChanges="true" app:cardCornerRadius="2dp" app:cardElevation="4dp" app:cardUseCompatPadding="true"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:animateLayoutChanges="true" android:orientation="vertical"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/icosemaforo" android:layout_width="match_parent" android:layout_height="wrap_content" android:src="@drawable/ic_android" /> </RelativeLayout> <LinearLayout android:id="@+id/contenedor_categoria1" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="right" android:orientation="vertical" > <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <TextView android:id="@+id/txtnumeropregunta" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:layout_alignParentTop="true" android:text="N°" android:textSize="14sp" android:textStyle="bold" /> <TextView android:id="@+id/txtpreguntas" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginTop="5dp" android:layout_marginEnd="20dp" android:layout_toStartOf="@+id/contenedorcheck" android:textSize="14sp" android:layout_toEndOf="@+id/txtnumeropregunta" android:textStyle="bold" android:text="Preguntas" /> <LinearLayout android:id="@+id/contenedorcheck" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:paddingEnd="20dp" > <CheckBox android:id="@+id/chbksi" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:text="@string/check_si" /> <CheckBox android:id="@+id/chbkIVSS" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:visibility="gone" android:text="@string/check_IVSS" /> <CheckBox android:id="@+id/chbkFSERV" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:visibility="gone" android:text="@string/check_freserv" /> <CheckBox android:id="@+id/chbkno" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="10dp" android:text="@string/check_no" /> <CheckBox android:id="@+id/chbkfmano" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" android:layout_marginEnd="10dp" android:text="@string/check_fredmano" /> <CheckBox android:id="@+id/chbkDSS" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" android:layout_marginEnd="10dp" android:text="@string/check_DSS" /> <CheckBox android:id="@+id/chbkna" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="gone" android:layout_marginEnd="10dp" android:text="@string/check_na" /> </LinearLayout> </RelativeLayout> </LinearLayout> </LinearLayout> </androidx.cardview.widget.CardView>
异常现象演示

问题原因与修复方案
问题1:勾选第1题时被覆盖隐藏
核心原因是RecyclerView的ViewHolder复用机制,加上布局中设置的android:animateLayoutChanges="true",在调用notifyItemChanged时,动画会和复用逻辑冲突,导致视图错乱;同时未处理默认选项组,部分position的题目会保留复用ViewHolder的旧状态。
修复步骤:
- 删除布局文件中
CardView和LinearLayout的android:animateLayoutChanges="true"属性,避免动画干扰视图复用。 - 在
onBindViewHolder中补充默认选项组逻辑,确保所有题目都有明确的选项组:
override fun onBindViewHolder(holder: PreUsoViewHolder, position: Int) { val item = listpreguntaspreuso[position] holder.render(item) holder.displayChecked(item.answer) // 统一处理所有position的选项组,添加默认情况 val answerSet = when(position) { 2 -> setOf(Answer.IVSS, Answer.DSS) 6 -> setOf(Answer.FRESERV, Answer.FREDMANO) 14, 18, 22, 25 -> setOf(Answer.NA, Answer.SI, Answer.NO) else -> setOf(Answer.SI, Answer.NO) // 默认显示YES/NO } holder.displayAnswers(answerSet) }
问题2:部分题目显示错误的选项组
原因是ViewHolder复用后,未完全重置Checkbox可见状态,且依赖position判断选项组的逻辑不够健壮,一旦列表顺序或数量变化就会出错。
优化修复:
- 修改
epreguntas类,让每个问题自带允许的选项组,摆脱对position的依赖:
class epreguntas( var id_pregunta: String, var num_pregunta: String, var pregunta : String, var icono_estado: String, var checkvalor: Boolean = false, var answer: Answer? = null, var allowedAnswers: Set<Answer> = setOf(Answer.SI, Answer.NO) // 新增选项组字段 )
- 初始化问卷列表时,给每个问题设置对应的
allowedAnswers,然后在Adapter中直接使用:
override fun onBindViewHolder(holder: PreUsoViewHolder, position: Int) { val item = listpreguntaspreuso[position] holder.render(item) holder.displayChecked(item.answer) // 直接使用问题自带的选项组 holder.displayAnswers(item.allowedAnswers) }
- 优化点击事件处理,避免无效position的错误:
private fun handleCheckboxClick(checkbox: CheckBox) { val position = adapterPosition if (position == RecyclerView.NO_POSITION) return // 过滤无效位置 val item = listpreguntaspreuso[position] checkboxAnswers[checkbox]?.let { answer -> val newAnswer = if (checkbox.isChecked) answer else null // 仅当状态变化时更新,避免重复刷新 if (item.answer != newAnswer) { item.answer = newAnswer notifyItemChanged(position) } } }
内容的提问来源于stack exchange,提问作者kenny
相关产品推荐
相关产品推荐

