Понадобилось мне тут для одного проекта сделать свой диалог с выбором рингтона в настройках. Сразу по 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() } }
Вот и все. Как и обещал, исходники можно взять здесь.
