Implement an IntegerListPreference

Unlike `ListPreference`, this preference class uses integers as its values instead of strings, avoiding unnecessary casting. It also doesn't require an array for values: in that case it will be using the clicked entry position as its value.
This commit is contained in:
sspacelynx 2021-08-27 18:47:47 +02:00 committed by ◱ Mark
parent fdc7e1238a
commit c1a3ddcd1c
3 changed files with 339 additions and 1 deletions

View File

@ -9,8 +9,10 @@ import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import emu.skyline.databinding.SettingsActivityBinding
import emu.skyline.preference.IntegerListPreference
class SettingsActivity : AppCompatActivity() {
val binding by lazy { SettingsActivityBinding.inflate(layoutInflater) }
@ -45,6 +47,9 @@ class SettingsActivity : AppCompatActivity() {
* This fragment is used to display all of the preferences
*/
class PreferenceFragment : PreferenceFragmentCompat() {
companion object {
private const val DIALOG_FRAGMENT_TAG = "androidx.preference.PreferenceFragment.DIALOG"
}
/**
* This constructs the preferences from [R.xml.preferences]
@ -52,6 +57,21 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreatePreferences(savedInstanceState : Bundle?, rootKey : String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
override fun onDisplayPreferenceDialog(preference : Preference?) {
if (preference is IntegerListPreference) {
// check if dialog is already showing
if (parentFragmentManager.findFragmentByTag(DIALOG_FRAGMENT_TAG) != null) {
return
}
val f = IntegerListPreference.IntegerListPreferenceDialogFragmentCompat.newInstance(preference.getKey())
f.setTargetFragment(this, 0)
f.show(parentFragmentManager, DIALOG_FRAGMENT_TAG)
} else {
super.onDisplayPreferenceDialog(preference)
}
}
}
/**

View File

@ -0,0 +1,318 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.preference
import java.lang.Exception
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.os.Parcelable.Creator
import android.util.AttributeSet
import android.content.Context
import android.content.res.Resources
import android.content.res.TypedArray
import android.content.DialogInterface
import android.annotation.SuppressLint
import androidx.annotation.ArrayRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.res.TypedArrayUtils
import androidx.preference.R
import androidx.preference.DialogPreference
import androidx.preference.PreferenceDialogFragmentCompat
/**
* A Preference that displays a list of entries as a dialog.
* This preference saves an integer value instead of a string one.
* @see androidx.preference.ListPreference
*/
@SuppressLint("RestrictedApi", "ResourceType")
class IntegerListPreference @JvmOverloads constructor(
context : Context,
attrs : AttributeSet? = null,
defStyleAttr : Int = TypedArrayUtils.getAttr(
context, R.attr.dialogPreferenceStyle,
R.attr.dialogPreferenceStyle
),
defStyleRes : Int = 0
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
/**
* The list of entries to be shown in the list in subsequent dialogs.
*/
var entries : Array<CharSequence>?
/**
* The array to find the value to save for a preference when an entry from entries is
* selected. If a user clicks on the second item in entries, the second item in this array
* will be saved to the preference.
*/
var entryValues : IntArray?
private var value : Int? = null
set(value) {
// Always persist/notify the first time.
val changed = field != value
if (changed || !isValueSet) {
field = value
isValueSet = true
value?.let { persistInt(it) }
if (changed) {
notifyChanged()
}
}
}
private var isValueSet = false
init {
val res : Resources = context.resources
val entryValuesId = attrs!!.getAttributeResourceValue("http://schemas.android.com/apk/res/android", "entryValues", 0)
var a = context.obtainStyledAttributes(
attrs, R.styleable.ListPreference, defStyleAttr, defStyleRes
)
entries = TypedArrayUtils.getTextArray(
a, R.styleable.ListPreference_entries,
R.styleable.ListPreference_android_entries
)
entryValues = try { res.getIntArray(entryValuesId) } catch (e : Exception) { null }
if (TypedArrayUtils.getBoolean(
a, R.styleable.ListPreference_useSimpleSummaryProvider,
R.styleable.ListPreference_useSimpleSummaryProvider, false
)
) {
summaryProvider = SimpleSummaryProvider.instance
}
a.recycle()
//Retrieve the Preference summary attribute since it's private in the Preference class.
a = context.obtainStyledAttributes(
attrs,
R.styleable.Preference, defStyleAttr, defStyleRes
)
a.recycle()
}
/**
* @param entriesResId The entries array as a resource
*/
fun setEntries(@ArrayRes entriesResId : Int) {
entries = context.resources.getTextArray(entriesResId)
}
/**
* @param entryValuesResId The entry values array as a resource
*/
fun setEntryValues(@ArrayRes entryValuesResId : Int) {
entryValues = context.resources.getIntArray(entryValuesResId)
}
/**
* @return The entry corresponding to the current value, or `null`
*/
fun getEntry() : CharSequence? {
return entries?.getOrNull(getValueIndex())
}
/**
* @param value The value whose index should be returned
* @return The index of the value, or -1 if not found
*/
private fun findIndexOfValue(value : Int?) : Int {
entryValues?.let {
if (value != null) {
for (i in it.indices.reversed()) {
if (it[i] == value) {
return i
}
}
}
}
return value ?: -1
}
/**
* Sets the value to the given index from the entry values.
* @param index The index of the value to set
*/
fun setValueIndex(index : Int) {
value = entryValues?.get(index) ?: index
}
private fun getValueIndex() : Int {
return findIndexOfValue(value)
}
override fun onGetDefaultValue(a : TypedArray, index : Int) : Any? {
return a.getInt(index, 0)
}
override fun onSetInitialValue(defaultValue : Any?) {
// `Preference` superclass passes a null defaultValue if it is sure there
// is already a persisted value, se we have to account for that here by
// passing a random number as default value.
value = if (defaultValue != null) {
getPersistedInt(defaultValue as Int)
} else {
getPersistedInt(0)
}
}
override fun onSaveInstanceState() : Parcelable {
val superState = super.onSaveInstanceState()
if (isPersistent) {
// No need to save instance state since it's persistent
return superState
}
val myState = SavedState(superState)
myState.value = value
return myState
}
override fun onRestoreInstanceState(state : Parcelable?) {
if (state == null || state.javaClass != SavedState::class.java) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state)
return
}
val myState = state as SavedState
super.onRestoreInstanceState(myState.superState)
value = myState.value
}
private class SavedState : BaseSavedState {
var value : Int? = null
constructor(source : Parcel) : super(source) {
value = source.readSerializable() as Int?
}
constructor(superState : Parcelable?) : super(superState)
override fun writeToParcel(dest : Parcel, flags : Int) {
super.writeToParcel(dest, flags)
dest.writeSerializable(value)
}
companion object {
@JvmField
val CREATOR : Creator<SavedState> = object : Creator<SavedState> {
override fun createFromParcel(input : Parcel) : SavedState? {
return SavedState(input)
}
override fun newArray(size : Int) : Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}
/**
* A simple [androidx.preference.Preference.SummaryProvider] implementation for a
* [IntegerListPreference]. If no value has been set, the summary displayed will be 'Not set',
* otherwise the summary displayed will be the entry set for this preference.
*/
class SimpleSummaryProvider private constructor() : SummaryProvider<IntegerListPreference> {
override fun provideSummary(preference : IntegerListPreference) : CharSequence {
return preference.getEntry() ?: preference.context.getString(R.string.not_set)
}
companion object {
private var simpleSummaryProvider : SimpleSummaryProvider? = null
/**
* Retrieve a singleton instance of this simple
* [androidx.preference.Preference.SummaryProvider] implementation.
*
* @return a singleton instance of this simple
* [androidx.preference.Preference.SummaryProvider] implementation
*/
val instance : SimpleSummaryProvider?
get() {
if (simpleSummaryProvider == null) {
simpleSummaryProvider = SimpleSummaryProvider()
}
return simpleSummaryProvider
}
}
}
class IntegerListPreferenceDialogFragmentCompat : PreferenceDialogFragmentCompat() {
var clickedDialogEntryIndex = 0
private var entries : Array<CharSequence>? = null
private var entryValues : IntArray? = null
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val preference = listPreference
check(preference.entries != null) { "IntegerListPreference requires at least the entries array." }
clickedDialogEntryIndex = preference.findIndexOfValue(preference.value)
entries = preference.entries
entryValues = preference.entryValues
} else {
clickedDialogEntryIndex = savedInstanceState.getInt(SAVE_STATE_INDEX, 0)
entries = savedInstanceState.getCharSequenceArray(SAVE_STATE_ENTRIES)
entryValues = savedInstanceState.getIntArray(SAVE_STATE_ENTRY_VALUES)
}
}
override fun onSaveInstanceState(outState : Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVE_STATE_INDEX, clickedDialogEntryIndex)
outState.putCharSequenceArray(SAVE_STATE_ENTRIES, entries)
outState.putIntArray(SAVE_STATE_ENTRY_VALUES, entryValues)
}
private val listPreference : IntegerListPreference
get() = preference as IntegerListPreference
override fun onPrepareDialogBuilder(builder : AlertDialog.Builder) {
super.onPrepareDialogBuilder(builder)
builder.setSingleChoiceItems(
entries, clickedDialogEntryIndex
) { dialog, which ->
clickedDialogEntryIndex = which
// Clicking on an item simulates the positive button click, and dismisses
// the dialog.
onClick(
dialog,
DialogInterface.BUTTON_POSITIVE
)
dialog.dismiss()
}
// The typical interaction for list-based dialogs is to have click-on-an-item dismiss the
// dialog instead of the user having to press 'Ok'.
builder.setPositiveButton(null, null)
}
override fun onDialogClosed(positiveResult : Boolean) {
if (positiveResult && clickedDialogEntryIndex >= 0) {
val value = entryValues?.get(clickedDialogEntryIndex) ?: clickedDialogEntryIndex
val preference = listPreference
if (preference.callChangeListener(value)) {
preference.value = value
}
}
}
companion object {
private const val SAVE_STATE_INDEX = "ListPreferenceDialogFragment.index"
private const val SAVE_STATE_ENTRIES = "ListPreferenceDialogFragment.entries"
private const val SAVE_STATE_ENTRY_VALUES = "ListPreferenceDialogFragment.entryValues"
fun newInstance(key : String?) : IntegerListPreferenceDialogFragmentCompat {
val fragment = IntegerListPreferenceDialogFragmentCompat()
val b = Bundle(1)
b.putString(ARG_KEY, key)
fragment.arguments = b
return fragment
}
}
}
}

View File

@ -72,7 +72,7 @@
app:key="username_value"
app:limit="31"
app:title="@string/username" />
<ListPreference
<emu.skyline.preference.IntegerListPreference
android:defaultValue="1"
android:entries="@array/system_languages"
android:entryValues="@array/system_lang_values"