Pull to refresh

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

Reading time5 min
Views2.3K

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

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

Tags:
Hubs:
+3
Comments0

Articles

Change theme settings