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

切换NumberPicker选项触发ArrayIndexOutOfBoundsException问题求助

问题:联动NumberPicker切换时触发ArrayIndexOutOfBoundsException崩溃

切换第一个NumberPicker从“5 han”回到“4 han”时,应用崩溃,抛出异常:

java.lang.ArrayIndexOutOfBoundsException: length=1; index=1
    at android.widget.NumberPicker.ensureCachedScrollSelectorValue(NumberPicker.java:2010)
    at android.widget.NumberPicker.initializeSelectorWheelIndices(NumberPicker.java:1822)
    at android.widget.NumberPicker.setMaxValue(NumberPicker.java:1520)
    at com.vincent.majcal.ui.picker.PickerFragment.onCreateView$lambda$1(PickerFragment.kt:114)

相关实现代码:

package com.vincent.majcal.ui.picker

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.NumberPicker
import android.widget.Switch
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.vincent.majcal.databinding.FragmentPickerTestBinding
import com.vincent.majcal.databinding.FragmentSearchBinding


class PickerFragment : Fragment() {

    private var _binding: FragmentPickerTestBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        val pickerViewModel = ViewModelProvider(this).get(PickerViewModel::class.java)

        _binding = FragmentPickerTestBinding.inflate(inflater, container, false)
        val root: View = binding.root
        val textView: TextView = binding.textSearch
        val firstPicker: NumberPicker = binding.firstPicker
        val secondPicker: NumberPicker = binding.secondPicker
        var fan = "1 han"
        val selections = mapOf(
            "1 han" to arrayOf(
                "30",
                "40",
                "50",
                "60",
                "70",
                "80",
                "90",
                "100",
                "110"
            ),
            "2 han" to arrayOf(
                "20",
                "25",
                "30",
                "40",
                "50",
                "60",
                "70",
                "80",
                "90",
                "100",
                "110"
            ),
            "3 han" to arrayOf(
                "20",
                "25",
                "30",
                "40",
                "50",
                "60",
                "70+",
            ),
            "4 han" to arrayOf(
                "20",
                "25",
                "30",
                "40+",
            ),
            "5 han" to arrayOf(
                "Mangan",
            ),
            "6 ~ 7 han" to arrayOf(
                "Haneman",
            ),
            "8 ~ 10 han" to arrayOf(
                "Baiman",
            ),
            "11 ~ 12 han" to arrayOf(
                "Sanbaiman",
            ),
            "13 han+" to arrayOf(
                "Kazoe Yakuman",
            )
        )

        firstPicker.minValue = 0
        firstPicker.maxValue = selections.keys.size - 1
        firstPicker.displayedValues = selections.keys.toTypedArray()
        firstPicker.wrapSelectorWheel = false

        secondPicker.wrapSelectorWheel = false
        secondPicker.minValue = 0
        // default the max value to the first Picker value set
        var secondDisplayArray = selections[fan]
        if (secondDisplayArray != null) {
            secondPicker.maxValue = secondDisplayArray.size - 1
        }
        secondPicker.displayedValues = secondDisplayArray

        firstPicker.setOnValueChangedListener { picker, oldVal, newVal ->
            run {
                fan = selections.keys.elementAt(newVal)
                secondDisplayArray = selections[fan]
                if (secondDisplayArray != null) {
                    var secondDisplayArraySize: Int = secondDisplayArray!!.size
                    secondPicker.value = 0
                    secondPicker.maxValue = secondDisplayArraySize - 1 //the Logcat show this line cause the exception
                    secondPicker.displayedValues = secondDisplayArray
                }
            }
        }

        pickerViewModel.text.observe(viewLifecycleOwner) {
            textView.text = it
        }
        return root
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
原因分析

崩溃核心是NumberPicker内部状态不同步:切换时先设置secondPicker.value = 0,此时第二个Picker的displayedValues还是“5 han”对应的长度为1的数组,Picker内部缓存了旧状态。紧接着设置maxValue时,Picker尝试更新选择器轮盘索引,错误使用旧数组长度计算,导致索引越界。

解决方法

调整第二个Picker属性的修改顺序,先更新displayedValues,再设置maxValue,最后设置value,确保内部状态始终同步:

修改firstPicker.setOnValueChangedListener中的代码:

firstPicker.setOnValueChangedListener { picker, oldVal, newVal ->
    fan = selections.keys.elementAt(newVal)
    secondDisplayArray = selections[fan]
    if (secondDisplayArray != null) {
        val secondDisplayArraySize = secondDisplayArray!!.size
        // 顺序调整:先更新选项数组,再设置最大值,最后重置选中值
        secondPicker.displayedValues = secondDisplayArray
        secondPicker.maxValue = secondDisplayArraySize - 1
        secondPicker.value = 0
    }
}

同时,初始化第二个Picker时也建议保持相同顺序,避免初始化阶段的潜在问题:

var secondDisplayArray = selections[fan]
if (secondDisplayArray != null) {
    secondPicker.displayedValues = secondDisplayArray
    secondPicker.maxValue = secondDisplayArray.size - 1
    secondPicker.value = 0
}
额外说明

NumberPicker的API对属性修改顺序有隐含要求:关联displayedValues时,必须先设置选项数组,再调整minValue/maxValue,最后设置选中值,这样Picker才能正确计算内部选择器状态,避免状态不同步引发的崩溃。

内容的提问来源于stack exchange,提问作者Vincent Zheng

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 03:34:50