Browse Source
android: Proper state restoration on settings dialogs
android: Proper state restoration on settings dialogs
All dialog code (except for the Date/Time ones) has been extracted out into a generic settings dialog fragment that handles everything through a viewmodel. State for each dialog will now be retained and dialogs will stay shown through configuration changes. I won't be changing the current state of the date and time dialog fragments until Google decides to make their classes non-final or if/when we migrate to Jetpack Compose.pull/15/merge
9 changed files with 319 additions and 198 deletions
-
227src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
-
2src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
-
235src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt
-
37src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
@ -0,0 +1,235 @@ |
|||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
package org.yuzu.yuzu_emu.fragments |
|||
|
|||
import android.app.Dialog |
|||
import android.content.DialogInterface |
|||
import android.os.Bundle |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.fragment.app.DialogFragment |
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.lifecycle.lifecycleScope |
|||
import androidx.lifecycle.repeatOnLifecycle |
|||
import com.google.android.material.dialog.MaterialAlertDialogBuilder |
|||
import com.google.android.material.slider.Slider |
|||
import kotlinx.coroutines.launch |
|||
import org.yuzu.yuzu_emu.R |
|||
import org.yuzu.yuzu_emu.databinding.DialogSliderBinding |
|||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem |
|||
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting |
|||
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting |
|||
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting |
|||
import org.yuzu.yuzu_emu.model.SettingsViewModel |
|||
|
|||
class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener { |
|||
private var type = 0 |
|||
private var position = 0 |
|||
|
|||
private var defaultCancelListener = |
|||
DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() } |
|||
|
|||
private val settingsViewModel: SettingsViewModel by activityViewModels() |
|||
|
|||
private lateinit var sliderBinding: DialogSliderBinding |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
type = requireArguments().getInt(TYPE) |
|||
position = requireArguments().getInt(POSITION) |
|||
|
|||
if (settingsViewModel.clickedItem == null) dismiss() |
|||
} |
|||
|
|||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { |
|||
return when (type) { |
|||
TYPE_RESET_SETTING -> { |
|||
MaterialAlertDialogBuilder(requireContext()) |
|||
.setMessage(R.string.reset_setting_confirmation) |
|||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> |
|||
settingsViewModel.clickedItem!!.setting.reset() |
|||
settingsViewModel.setAdapterItemChanged(position) |
|||
settingsViewModel.shouldSave = true |
|||
} |
|||
.setNegativeButton(android.R.string.cancel, null) |
|||
.create() |
|||
} |
|||
|
|||
SettingsItem.TYPE_SINGLE_CHOICE -> { |
|||
val item = settingsViewModel.clickedItem as SingleChoiceSetting |
|||
val value = getSelectionForSingleChoiceValue(item) |
|||
MaterialAlertDialogBuilder(requireContext()) |
|||
.setTitle(item.nameId) |
|||
.setSingleChoiceItems(item.choicesId, value, this) |
|||
.create() |
|||
} |
|||
|
|||
SettingsItem.TYPE_SLIDER -> { |
|||
sliderBinding = DialogSliderBinding.inflate(layoutInflater) |
|||
val item = settingsViewModel.clickedItem as SliderSetting |
|||
|
|||
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units) |
|||
sliderBinding.slider.apply { |
|||
valueFrom = item.min.toFloat() |
|||
valueTo = item.max.toFloat() |
|||
value = settingsViewModel.sliderProgress.value.toFloat() |
|||
addOnChangeListener { _: Slider, value: Float, _: Boolean -> |
|||
settingsViewModel.setSliderTextValue(value, item.units) |
|||
} |
|||
} |
|||
|
|||
MaterialAlertDialogBuilder(requireContext()) |
|||
.setTitle(item.nameId) |
|||
.setView(sliderBinding.root) |
|||
.setPositiveButton(android.R.string.ok, this) |
|||
.setNegativeButton(android.R.string.cancel, defaultCancelListener) |
|||
.create() |
|||
} |
|||
|
|||
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { |
|||
val item = settingsViewModel.clickedItem as StringSingleChoiceSetting |
|||
MaterialAlertDialogBuilder(requireContext()) |
|||
.setTitle(item.nameId) |
|||
.setSingleChoiceItems(item.choices, item.selectValueIndex, this) |
|||
.create() |
|||
} |
|||
|
|||
else -> super.onCreateDialog(savedInstanceState) |
|||
} |
|||
} |
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View? { |
|||
return when (type) { |
|||
SettingsItem.TYPE_SLIDER -> sliderBinding.root |
|||
else -> super.onCreateView(inflater, container, savedInstanceState) |
|||
} |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
when (type) { |
|||
SettingsItem.TYPE_SLIDER -> { |
|||
viewLifecycleOwner.lifecycleScope.launch { |
|||
repeatOnLifecycle(Lifecycle.State.CREATED) { |
|||
settingsViewModel.sliderTextValue.collect { |
|||
sliderBinding.textValue.text = it |
|||
} |
|||
} |
|||
repeatOnLifecycle(Lifecycle.State.CREATED) { |
|||
settingsViewModel.sliderProgress.collect { |
|||
sliderBinding.slider.value = it.toFloat() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onClick(dialog: DialogInterface, which: Int) { |
|||
when (settingsViewModel.clickedItem) { |
|||
is SingleChoiceSetting -> { |
|||
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting |
|||
val value = getValueForSingleChoiceSelection(scSetting, which) |
|||
if (scSetting.selectedValue != value) { |
|||
settingsViewModel.shouldSave = true |
|||
} |
|||
scSetting.selectedValue = value |
|||
} |
|||
|
|||
is StringSingleChoiceSetting -> { |
|||
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting |
|||
val value = scSetting.getValueAt(which) |
|||
if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true |
|||
scSetting.selectedValue = value |
|||
} |
|||
|
|||
is SliderSetting -> { |
|||
val sliderSetting = settingsViewModel.clickedItem as SliderSetting |
|||
if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) { |
|||
settingsViewModel.shouldSave = true |
|||
} |
|||
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value |
|||
} |
|||
} |
|||
closeDialog() |
|||
} |
|||
|
|||
private fun closeDialog() { |
|||
settingsViewModel.setAdapterItemChanged(position) |
|||
settingsViewModel.clickedItem = null |
|||
settingsViewModel.setSliderProgress(-1f) |
|||
dismiss() |
|||
} |
|||
|
|||
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { |
|||
val valuesId = item.valuesId |
|||
return if (valuesId > 0) { |
|||
val valuesArray = requireContext().resources.getIntArray(valuesId) |
|||
valuesArray[which] |
|||
} else { |
|||
which |
|||
} |
|||
} |
|||
|
|||
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { |
|||
val value = item.selectedValue |
|||
val valuesId = item.valuesId |
|||
if (valuesId > 0) { |
|||
val valuesArray = requireContext().resources.getIntArray(valuesId) |
|||
for (index in valuesArray.indices) { |
|||
val current = valuesArray[index] |
|||
if (current == value) { |
|||
return index |
|||
} |
|||
} |
|||
} else { |
|||
return value |
|||
} |
|||
return -1 |
|||
} |
|||
|
|||
companion object { |
|||
const val TAG = "SettingsDialogFragment" |
|||
|
|||
const val TYPE_RESET_SETTING = -1 |
|||
|
|||
const val TITLE = "Title" |
|||
const val TYPE = "Type" |
|||
const val POSITION = "Position" |
|||
|
|||
fun newInstance( |
|||
settingsViewModel: SettingsViewModel, |
|||
clickedItem: SettingsItem, |
|||
type: Int, |
|||
position: Int |
|||
): SettingsDialogFragment { |
|||
when (type) { |
|||
SettingsItem.TYPE_HEADER, |
|||
SettingsItem.TYPE_SWITCH, |
|||
SettingsItem.TYPE_SUBMENU, |
|||
SettingsItem.TYPE_DATETIME_SETTING, |
|||
SettingsItem.TYPE_RUNNABLE -> |
|||
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") |
|||
|
|||
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( |
|||
(clickedItem as SliderSetting).selectedValue.toFloat() |
|||
) |
|||
} |
|||
settingsViewModel.clickedItem = clickedItem |
|||
|
|||
val args = Bundle() |
|||
args.putInt(TYPE, type) |
|||
args.putInt(POSITION, position) |
|||
val fragment = SettingsDialogFragment() |
|||
fragment.arguments = args |
|||
return fragment |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue