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