Понадобилось мне тут для одного проекта сделать свой диалог с выбором рингтона в настройках. Сразу по 2 причинам – во-первых, в support library RingtonePreference
отсутствует, так что использовать стандартный диалог в PreferenceFragmentCompat
не получится. А во-вторых, мне надо было туда в дополнение к стандартным мелодиям добавить несколько звуков из ресурсов. Так что решено было написать свой диалог.
Продемонстрирую создание подобного диалога на примере простого приложения: на одном экране есть кнопка "Play ringtone", нажатие на которую проигрывает установленный в настройках рингтон, и ссылка на экран с настройками:
Я не буду описывать создание этих двух экранов – там все как всегда. На всякий случай, в конце будет ссылка на репозиторий с кодом приложения.
Итак, начнем с xml-файла с описанием экрана настроек. Разместим файл settings.xml
в res/xml
со следующим содержимым:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="ringtone"
android:title="Ringtone"/>
</PreferenceScreen>
И теперь добавим эти настройки в наш фрагмент:
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
}
}
Запускаем, открываем экран с настройками, видим следующее:
Вступление на этом заканчиваем, переходим к цели статьи. План такой: при нажатии на "Ringtone" открывается диалог со списком рингтонов и кнопками OK и Cancel, при выборе рингтона он проигрывается (как и в случае стандартного RingtonePreference
), при нажатии на OK сохраняется в настройках.
Итак, создаем диалоговый фрагмент:
class RingtonePreferenceDialog : DialogFragment() {
private val prefKey: String
get() = arguments?.getString(ARG_PREF_KEY) ?: throw IllegalArgumentException("ARG_PREF_KEY not set")
companion object {
private const val ARG_PREF_KEY = "ARG_PREF_KEY"
fun create(prefKey: String): RingtonePreferenceDialog {
val fragment = RingtonePreferenceDialog()
fragment.arguments = Bundle().apply {
putString(ARG_PREF_KEY, prefKey)
}
return fragment
}
}
}
В prefKey
мы передаем ключ, по которому будет извлекаться текущий рингтон, и туда же он будет записываться по нажатию кнопки OK.
Для дальнейшей работы нам понадобится вспомогательный класс Ringtone
, объявим его внутри нашего фрагмента:
private data class Ringtone(val title: String, val uri: Uri)
И напишем вспомогательную функцию, которая вытащит все встроенные рингтоны в Андроиде, и вернет нам список из Ringtone
:
private fun getAndroidRingtones(): List<Ringtone> {
val ringtoneManager = RingtoneManager(context)
val cursor = ringtoneManager.cursor
return (0 until cursor.count).map {
cursor.moveToPosition(it)
Ringtone(
title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX),
uri = ringtoneManager.getRingtoneUri(it)
)
}
}
Здесь ringtoneManager.cursor
вернет курсор со всеми доступными рингтонами, мы просто проходим по всем элементам и мапим их в наш вспомогательный класс Ringtone
(так с ними удобнее работать).
Давайте сначала организуем работу со встроенным списком рингтонов – добавить потом наши ресурсы будет очень просто. Для этого создаем диалог, переопределяя метод onCreateDialog
:
private var ringtones: List<Ringtone> = emptyList()
private var currentUri: Uri? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
ringtones = getAndroidRingtones()
currentUri = getCurrentRingtoneUri()
val currentPosition = ringtones.indexOfFirst { currentUri == it.uri }
return AlertDialog.Builder(context!!)
.setPositiveButton(android.R.string.ok) { _, _ -> saveCurrentUri() }
.setNegativeButton(android.R.string.cancel) { _, _ -> dialog.dismiss() }
.setSingleChoiceItems(adapter, currentPosition) { _, which ->
currentUri = ringtones[which].uri
}
.create()
}
Адаптер нужен для отображения списка элементов в диалоге, его можно определить так:
private val adapter by lazy {
SimpleAdapter(
context,
ringtones.map { mapOf("title" to it.title) },
R.layout.simple_list_item_single_choice,
arrayOf("title"),
intArrayOf(R.id.text1)
)
}
И нужен еще вспомогательный метод для сохранения выделенной позиции (он будет вызываться при нажатии на кнопку OK):
private fun saveCurrentUri() {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(prefKey, currentUri?.toString())
.apply()
}
Осталось привязать наш элемент к диалогу, для этого определим вспомогательную функцию в файле с диалогом:
fun Preference.connectRingtoneDialog(fragmentManager: FragmentManager?) = setOnPreferenceClickListener {
RingtonePreferenceDialog.create(key).apply {
fragmentManager?.let { show(it, "SOUND") }
}
true
}
И добавим findPreference("ringtone").connectRingtoneDialog(fragmentManager)
в наш SettingsFragment
, теперь он должен выглядеть так:
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
findPreference("ringtone").connectRingtoneDialog(fragmentManager)
}
}
Если мы теперь перейдем на экран с настройками и нажмем на "Ringtone", то увидим что-то подобное:
Теперь добавим рингтоны из ресурсов к нашему диалогу. Например, у нас есть рингтон sample.mp3
в папке res/raw
, и мы хотим отображать его в начале списка. Добавим еще один метод в класс диалога:
private fun getResourceRingtones(): List<Ringtone> = listOf(
Ringtone(
title = "Sample ringtone",
uri = Uri.parse("${ContentResolver.SCHEME_ANDROID_RESOURCE}://${context!!.packageName}/raw/sample")
)
)
И поменяем первую строчку в методе onCreateDialog
:
ringtones = getResourceRingtones() + getAndroidRingtones()
Запускаем, смотрим, радуемся, что все так просто:
Осталось добавить "предпросмотр" для рингтонов. Для этого введем дополнительное поле:
private var playingRingtone: android.media.Ringtone? = null
И немного изменим callback-метод для setSingleChoiceItems
:
playingRingtone?.stop()
ringtones[which].also {
currentUri = it.uri
playingRingtone = it.uri?.let { RingtoneManager.getRingtone(context, it) }
playingRingtone?.play()
}
Что здесь происходит: останавливаем воспроизведение текущего рингтона (если он не null), устанавливаем в качестве текущего выбранный, запускаем воспроизведение. Теперь при выборе ринтона в диалоге он будет воспроизводиться. Чтобы останавливать воспроизведение при закрытии диалога, переопределим метод onPause
:
override fun onPause() {
super.onPause()
playingRingtone?.stop()
}
Ну и осталось только привязать кнопку на главном экране к воспроизведению рингтона, например, так:
findViewById<Button>(R.id.playRingtone).setOnClickListener {
val ringtone = PreferenceManager.getDefaultSharedPreferences(this)
.getString("ringtone", null)
?.let { RingtoneManager.getRingtone(this, Uri.parse(it)) }
if (ringtone == null) {
Toast.makeText(this, "Select ringtone in settings", Toast.LENGTH_SHORT).show()
} else {
ringtone.play()
}
}
Вот и все. Как и обещал, исходники можно взять здесь.