Предисловие

Классическая ситуация: допустим у нас есть какой-то RecyclerView, элементы которого - карточки с картинками.

В этой статье мы будем раскрашивать MaterialCardView и её дочерние элементы в цвета, сочетающиеся с цветами изображения. Для этого воспользуемся DynamicColors API.

Мы не будем использовать Compose, на эту тему уже есть несколько статей с его использованием.

Заготовка

Для наших экспериментов создадим какой-то абстрактный data-класс, который будет нашим элементом списка в RecyclerView:

data class SomeListItem(
    val image: Drawable,
    val title: String,
    val tag: String
)

У него есть image для хранения картинки, title для хранения заголовка, а также tag для хранения каких-нибудь тегов.

В реальном проекте, наверное, image будет не Drawable, а ссылкой на картинку. Всё-таки изображения обычно подгружаются из сети. Но мы сделаем так для упрощения примера: все наши картинки будут лежать в ресурсах приложения.

Ну и давайте сразу создадим какой-то простенький адаптер для RecyclerView:

class SomeListAdapter(
    private val items: List<SomeListItem>
) : RecyclerView.Adapter<SomeListAdapter.ViewHolder>() {

    inner class ViewHolder(val binding: ItemSomeListBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(item: SomeListItem) {
            binding.imageView.setImageDrawable(item.image)
            binding.titleView.text = item.title
            binding.tagView.text = item.tag
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemSomeListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(items[position])
    }

    override fun getItemCount(): Int = items.size
}

Ну и в классе Activity в onCreate создадим простой список из элементов и подключим адаптер к RecyclerView:

 val myListOfItems = listOf(
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower1)!!,
                title = "Красный цветок",
                tag = "#red #flowers #simple"
            ),
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower2)!!,
                title = "Синий цветок",
                tag = "#simple"
            ),
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower3)!!,
                title = "Розовый цветок",
                tag = "#flowers #simple"
            ),
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower4)!!,
                title = "Фиолетовый цветок",
                tag = "#purple #flowers #prettynice"
            ),
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower5)!!,
                title = "Зелёный цветок",
                tag = "#simple #green"
            ),
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower6)!!,
                title = "Жёлтый цветок",
                tag = "#flowers #yellow"
            ),
            SomeListItem(
                image = ContextCompat.getDrawable(this, R.drawable.flower7)!!,
                title = "Голубой цветок",
                tag = "#blue #nice"
            ),
        )

        val adapter = SomeListAdapter(myListOfItems)
        binding.recyclerview.adapter = adapter
        binding.recyclerview.layoutManager = LinearLayoutManager(this)

Теперь у нас есть простенькое приложение со списком карточек.

Теперь наша задача - заставить карточки раскрашиваться в зависимости от изображения на них.

В целом можно придумать много способов это реализовать. В этом примере мы воспользуемся DynamicColors API.

К сожалению, Dynamic Colors API не будет работать на версиях Android ниже 12ой. Однако если у вас есть желание реализовать подобное на ранних версиях Android - можно воспользоваться Palette API.

Красим карточки

Раскрашиванием карточек будет заниматься адаптер для RecyclerView. В документации, написанной Google, есть прекрасный пример того, как это можно реализовать.

В нашем ViewHolder модифицируем метод bind. Для начала возьмём bitmap из imageView и сожмём его. Так мы ускорим процесс определения цветовой схемы карточки:

val bitmap = binding.image.drawable.toBitmap()
val compressedBitmap = bitmap.scale(10, 6, false)

Теперь можно воспользоваться прекрасным функционалом wrapContextIfAvailable из Dynamic Colors API. Создадим контекст, используя compressedBitmap в качестве сontentBasedSource:

val newContext: Context = DynamicColors.wrapContextIfAvailable(
                   itemView.context,
                    DynamicColorsOptions.Builder()
                        .setContentBasedSource(compressedBitmap)
                        .build()
                )

Дело остаётся за малым: перекрасить элементы ViewHolder цветами темы нового контекста:

//устанавливаем цвет для карточки
(binding.root as MaterialCardView).setCardBackgroundColor(MaterialColors.getColor(newContext, com.google.android.material.R.attr.colorSecondaryContainer, Color.GRAY))

//и цвет заголовка
binding.title.setTextColor(MaterialColors.getColor(newContext, com.google.android.material.R.attr.colorPrimary, Color.GRAY))

//и цвет для тегов
binding.tags.setTextColor(MaterialColors.getColor(newContext, com.google.android.material.R.attr.colorOnSecondaryContainer, Color.GRAY))

Стоит учесть, что wrapContextIfAvailable - достаточно трудоёмкая функция. Чтобы избежать лагов - можно воспользоваться корутинами.

Таким образом метод bind нашего ViewHolder выглядит следующим образом:

 fun bind(item: SomeListItem) {
            binding.image.setImageDrawable(item.image)
            binding.title.text = item.title
            binding.tags.text = item.tag

            CoroutineScope(Dispatchers.IO).launch {
                val bitmap = binding.image.drawable.toBitmap()
                val compressedBitmap = bitmap.scale(10, 6, false)

                val newContext: Context = DynamicColors.wrapContextIfAvailable(
                   itemView.context,
                    DynamicColorsOptions.Builder()
                        .setContentBasedSource(compressedBitmap)
                        .build()
                )
                withContext(Dispatchers.Main){
                    (binding.root as MaterialCardView).setCardBackgroundColor(MaterialColors.getColor(newContext, com.google.android.material.R.attr.colorSecondaryContainer, Color.GRAY))
                    binding.title.setTextColor(MaterialColors.getColor(newContext, com.google.android.material.R.attr.colorPrimary, Color.GRAY))
                    binding.tags.setTextColor(MaterialColors.getColor(newContext, com.google.android.material.R.attr.colorOnSecondaryContainer, Color.GRAY))
                }
            }


        }

Теперь карточки в нашем RecyclerView будут раскрашиваться в зависимости от цвета изображения. Цвет карточки мы выбрали ColorSecondaryContainer, а цвет заголовка: ColorPrimary. Вы можете выбрать какие-то другие цвета на свой вкус.

Результат

Заключение

Надеюсь, статься была полезной для вас. Вы можете ознакомиться с исходным кодом на GitHub. Всем удачи!