Skip to main content

Создание диалога для выбора рингтона

· 5 min read

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

Вот и все. Как и обещал, исходники можно взять здесь.