You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的旧状态。

修复步骤:

  1. 删除布局文件中CardView和LinearLayout的android:animateLayoutChanges="true"属性,避免动画干扰视图复用。
  2. 在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判断选项组的逻辑不够健壮,一旦列表顺序或数量变化就会出错。

优化修复:

  1. 修改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) // 新增选项组字段
)
  1. 初始化问卷列表时,给每个问题设置对应的allowedAnswers,然后在Adapter中直接使用:
override fun onBindViewHolder(holder: PreUsoViewHolder, position: Int) {
    val item = listpreguntaspreuso[position]
    holder.render(item)
    holder.displayChecked(item.answer)
    // 直接使用问题自带的选项组
    holder.displayAnswers(item.allowedAnswers)
}
  1. 优化点击事件处理,避免无效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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 02:05:24