NumberPicker will raise ArrayIndexOutOfBoundsException if I select back to previous selection on first picker

65 views Asked by At

So I have two NumberPicker, the array of second picker will be based on the selections for the first picker. However in the following code, I got crush if I choose 4 han from 5 han by following exception

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)

Here is the code for this part, I use the Bottom Nagivation View Activity Template and modify it with my own use:

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
    }
}
1

There are 1 answers

2
Marcin Orlowski On

Try changing the order of the operations in the OnValueChangedListener for firstPicker to ensure displayedValues is updated before maxValue. The reason is secondPicker.maxValue = secondDisplayArraySize - 1 occurs before secondPicker.displayedValues = secondDisplayArray so if NumberPicker try to access values based on the old maxValue (before the new displayedValues array is set), that could lead to reach beyond the bounds thus causing the ArrayIndexOutOfBoundsException.

I'd also clear secondPicker.value before updating other vars, to ensure it is within bounds.

firstPicker.setOnValueChangedListener { picker, oldVal, newVal ->
    run {
        fan = selections.keys.elementAt(newVal)
        secondDisplayArray = selections[fan]
        if (secondDisplayArray != null) {
            var secondDisplayArraySize: Int = secondDisplayArray!!.size
            secondPicker.displayedValues = secondDisplayArray // Update displayedValues first
            secondPicker.maxValue = secondDisplayArraySize - 1 // Then update maxValue
            secondPicker.value = 0 // Ensure value is within bounds
        }
    }
}