Refactor sources settings list

This commit is contained in:
Koitharu
2022-01-06 17:26:16 +02:00
parent 99186bf269
commit 17519db44e
22 changed files with 293 additions and 272 deletions

View File

@@ -92,7 +92,6 @@ class KotatsuApp : Application() {
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectTargetFragmentUsage()
.detectSetUserVisibleHint()
.build()
}

View File

@@ -8,7 +8,6 @@ object MangaProviderFactory {
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = settings.sourcesOrder
val hidden = settings.hiddenSources
val sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
@@ -16,6 +15,7 @@ object MangaProviderFactory {
return if (includeHidden) {
sorted
} else {
val hidden = settings.hiddenSources
sorted.filterNot { x ->
x.name in hidden
}

View File

@@ -1,28 +0,0 @@
package org.koitharu.kotatsu.base.ui.list
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import org.koin.core.component.KoinComponent
@Deprecated("")
abstract class BaseViewHolder<T, E, B : ViewBinding> protected constructor(val binding: B) :
RecyclerView.ViewHolder(binding.root), KoinComponent {
var boundData: T? = null
private set
val context get() = itemView.context!!
fun bind(data: T, extra: E) {
boundData = data
onBind(data, extra)
}
fun requireData(): T {
return boundData ?: throw IllegalStateException("Calling requireData() before bind()")
}
open fun onRecycled() = Unit
abstract fun onBind(data: T, extra: E)
}

View File

@@ -1,96 +0,0 @@
package org.koitharu.kotatsu.base.ui.list.decor
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.inflate
import kotlin.math.max
/**
* https://github.com/paetztm/recycler_view_headers
*/
class SectionItemDecoration(
private val isSticky: Boolean,
private val callback: Callback
) : RecyclerView.ItemDecoration() {
private var headerView: TextView? = null
private var headerOffset: Int = 0
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (headerOffset == 0) {
headerOffset = parent.resources.getDimensionPixelSize(R.dimen.header_height)
}
val pos = parent.getChildAdapterPosition(view)
outRect.set(0, if (callback.isSection(pos)) headerOffset else 0, 0, 0)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val textView = headerView ?: parent.inflate<TextView>(R.layout.item_filter_header).also {
headerView = it
}
fixLayoutSize(textView, parent)
for (child in parent.children) {
val pos = parent.getChildAdapterPosition(child)
if (callback.isSection(pos)) {
textView.text = callback.getSectionTitle(pos) ?: continue
c.save()
if (isSticky) {
c.translate(
0f,
max(0f, (child.top - textView.height).toFloat())
)
} else {
c.translate(
0f,
(child.top - textView.height).toFloat()
)
}
textView.draw(c)
c.restore()
}
}
}
/**
* Measures the header view to make sure its size is greater than 0 and will be drawn
* https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
*/
private fun fixLayoutSize(view: View, parent: ViewGroup) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec =
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width
)
val childHeight = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height
)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
interface Callback {
fun isSection(position: Int): Boolean
fun getSectionTitle(position: Int): CharSequence?
}
}

View File

@@ -30,7 +30,6 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>(),
}
}
@Suppress("DEPRECATION")
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference

View File

@@ -11,6 +11,7 @@ import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.onboard.OnboardViewModel
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
val settingsModule
get() = module {
@@ -25,4 +26,5 @@ val settingsModule
}
viewModel { ProtectSetupViewModel(get()) }
viewModel { OnboardViewModel(get()) }
viewModel { SourcesSettingsViewModel(get()) }
}

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.onboard.model.SourceLocale
import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.toTitleCase
import java.util.*
class OnboardViewModel(
@@ -27,9 +28,9 @@ class OnboardViewModel(
init {
if (settings.isSourcesSelected) {
selectedLocales.removeAll(settings.hiddenSources.map { x -> MangaSource.valueOf(x).locale })
selectedLocales.removeAll(settings.hiddenSources.mapToSet { x -> MangaSource.valueOf(x).locale })
} else {
val deviceLocales = LocaleListCompat.getDefault().map { x ->
val deviceLocales = LocaleListCompat.getDefault().mapToSet { x ->
x.language
}
selectedLocales.retainAll(deviceLocales)
@@ -64,7 +65,7 @@ class OnboardViewModel(
} else null
SourceLocale(
key = key,
title = locale?.getDisplayLanguage(locale)?.capitalize(locale),
title = locale?.getDisplayLanguage(locale)?.toTitleCase(locale),
isChecked = key in selectedLocales
)
}.sortedWith(SourceLocaleComparator())

View File

@@ -1,18 +0,0 @@
package org.koitharu.kotatsu.settings.sources
import android.view.LayoutInflater
import android.view.ViewGroup
import org.koitharu.kotatsu.base.ui.list.BaseViewHolder
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
class SourceViewHolder(parent: ViewGroup) :
BaseViewHolder<MangaSource, Boolean, ItemSourceConfigBinding>(
ItemSourceConfigBinding.inflate(LayoutInflater.from(parent.context), parent, false)
) {
override fun onBind(data: MangaSource, extra: Boolean) {
binding.textViewTitle.text = data.title
binding.switchToggle.isChecked = extra
}
}

View File

@@ -1,69 +0,0 @@
package org.koitharu.kotatsu.settings.sources
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.mapToSet
class SourcesAdapter(
private val settings: AppSettings,
private val onItemClickListener: OnListItemClickListener<MangaSource>,
) : RecyclerView.Adapter<SourceViewHolder>() {
private val dataSet =
MangaProviderFactory.getSources(settings, includeHidden = true).toMutableList()
private val hiddenItems = settings.hiddenSources.mapNotNull {
runCatching {
MangaSource.valueOf(it)
}.getOrNull()
}.toMutableSet()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = SourceViewHolder(parent).also(::onViewHolderCreated)
override fun getItemCount() = dataSet.size
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
val item = dataSet[position]
holder.bind(item, !hiddenItems.contains(item))
}
@SuppressLint("ClickableViewAccessibility")
private fun onViewHolderCreated(holder: SourceViewHolder) {
holder.binding.switchToggle.setOnCheckedChangeListener { _, it ->
if (it) {
hiddenItems.remove(holder.requireData())
} else {
hiddenItems.add(holder.requireData())
}
settings.hiddenSources = hiddenItems.mapToSet { x -> x.name }
}
holder.binding.imageViewConfig.setOnClickListener { v ->
onItemClickListener.onItemClick(holder.requireData(), v)
}
holder.binding.imageViewHandle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onItemClickListener.onItemLongClick(
holder.requireData(),
holder.itemView
)
} else {
false
}
}
}
fun moveItem(oldPos: Int, newPos: Int) {
val item = dataSet.removeAt(oldPos)
dataSet.add(newPos, item)
notifyItemMoved(oldPos, newPos)
settings.sourcesOrder = dataSet.map { it.ordinal }
}
}

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.settings.sources
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
class SourcesReorderCallback :
ItemTouchHelper.SimpleCallback(ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val adapter = recyclerView.adapter as? SourcesAdapter ?: return false
val oldPos = viewHolder.bindingAdapterPosition
val newPos = target.bindingAdapterPosition
adapter.moveItem(oldPos, newPos)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun isLongPressDragEnabled() = false
}

View File

@@ -1,25 +1,28 @@
package org.koitharu.kotatsu.settings.sources
import android.os.Bundle
import android.view.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.databinding.FragmentSettingsSourcesBinding
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigAdapter
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigItem
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigListener
class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
OnListItemClickListener<MangaSource> {
SourceConfigListener {
private lateinit var reorderHelper: ItemTouchHelper
private val viewModel by viewModel<SourcesSettingsViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -39,11 +42,16 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val sourcesAdapter = SourceConfigAdapter(this)
with(binding.recyclerView) {
setHasFixedSize(true)
addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
adapter = SourcesAdapter(get(), this@SourcesSettingsFragment)
adapter = sourcesAdapter
reorderHelper.attachToRecyclerView(this)
}
viewModel.items.observe(viewLifecycleOwner) {
sourcesAdapter.items = it
}
}
override fun onDestroyView() {
@@ -51,22 +59,6 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
super.onDestroyView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
// TODO handle changes in dialog
// inflater.inflate(R.menu.opt_sources, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when(item.itemId) {
R.id.action_languages -> {
OnboardDialogFragment.show(parentFragmentManager)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding(
bottom = insets.bottom,
@@ -75,14 +67,43 @@ class SourcesSettingsFragment : BaseFragment<FragmentSettingsSourcesBinding>(),
)
}
override fun onItemClick(item: MangaSource, view: View) {
(activity as? SettingsActivity)?.openMangaSourceSettings(item)
override fun onItemSettingsClick(item: SourceConfigItem.SourceItem) {
(activity as? SettingsActivity)?.openMangaSourceSettings(item.source)
}
override fun onItemLongClick(item: MangaSource, view: View): Boolean {
reorderHelper.startDrag(
binding.recyclerView.findContainingViewHolder(view) ?: return false
)
return true
override fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean) {
viewModel.setEnabled(item.source, isEnabled)
}
override fun onDragHandleTouch(holder: RecyclerView.ViewHolder) {
reorderHelper.startDrag(holder)
}
override fun onHeaderClick(header: SourceConfigItem.LocaleHeader) {
viewModel.expandOrCollapse(header.localeId)
}
private inner class SourcesReorderCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP,
0,
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
if (viewHolder.itemViewType != target.itemViewType) {
return false
}
val oldPos = viewHolder.bindingAdapterPosition
val newPos = target.bindingAdapterPosition
viewModel.reorderSources(oldPos, newPos)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
override fun isLongPressDragEnabled() = false
}
}

View File

@@ -0,0 +1,59 @@
package org.koitharu.kotatsu.settings.sources
import androidx.lifecycle.MutableLiveData
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.sources.adapter.SourceConfigItem
import java.util.*
class SourcesSettingsViewModel(
private val settings: AppSettings,
) : BaseViewModel() {
val items = MutableLiveData<List<SourceConfigItem>>(emptyList())
private val expandedGroups = HashSet<String?>()
init {
buildList()
}
fun reorderSources(oldPos: Int, newPos: Int) {
val snapshot = items.value?.toMutableList() ?: return
Collections.swap(snapshot, oldPos, newPos)
settings.sourcesOrder = snapshot.mapNotNull {
(it as? SourceConfigItem.SourceItem)?.source?.ordinal
}
buildList()
}
fun setEnabled(source: MangaSource, isEnabled: Boolean) {
settings.hiddenSources = if (isEnabled) {
settings.hiddenSources - source.name
} else {
settings.hiddenSources + source.name
}
buildList()
}
fun expandOrCollapse(headerId: String?) {
if (headerId in expandedGroups) {
expandedGroups.remove(headerId)
} else {
expandedGroups.add(headerId)
}
buildList()
}
private fun buildList() {
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
val hiddenSources = settings.hiddenSources
items.value = sources.map {
SourceConfigItem.SourceItem(
source = it,
isEnabled = it.name !in hiddenSources,
)
}
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.settings.sources.adapter
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
class SourceConfigAdapter(
listener: SourceConfigListener,
) : AsyncListDifferDelegationAdapter<SourceConfigItem>(
SourceConfigDiffCallback(),
sourceConfigHeaderDelegate(listener),
sourceConfigItemDelegate(listener),
)

View File

@@ -0,0 +1,61 @@
package org.koitharu.kotatsu.settings.sources.adapter
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.widget.CompoundButton
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemExpandableBinding
import org.koitharu.kotatsu.databinding.ItemSourceConfigBinding
fun sourceConfigHeaderDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.LocaleHeader, SourceConfigItem, ItemExpandableBinding>(
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) }
) {
binding.root.setOnClickListener {
listener.onHeaderClick(item)
}
bind {
binding.root.text = item.title ?: getString(R.string.other)
binding.root.isChecked = item.isExpanded
}
}
@SuppressLint("ClickableViewAccessibility")
fun sourceConfigItemDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) }
) {
val eventListener = object : View.OnClickListener, View.OnTouchListener,
CompoundButton.OnCheckedChangeListener {
override fun onClick(v: View?) = listener.onItemSettingsClick(item)
override fun onTouch(v: View?, event: MotionEvent): Boolean {
return if (event.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onDragHandleTouch(this@adapterDelegateViewBinding)
true
} else {
false
}
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
listener.onItemEnabledChanged(item, isChecked)
}
}
binding.imageViewConfig.setOnClickListener(eventListener)
binding.switchToggle.setOnCheckedChangeListener(eventListener)
binding.imageViewHandle.setOnTouchListener(eventListener)
bind {
binding.textViewTitle.text = item.source.title
binding.switchToggle.isChecked = item.isEnabled
}
}

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.DiffUtil
class SourceConfigDiffCallback : DiffUtil.ItemCallback<SourceConfigItem>() {
override fun areItemsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
return when {
oldItem.javaClass != newItem.javaClass -> false
oldItem is SourceConfigItem.LocaleHeader && newItem is SourceConfigItem.LocaleHeader -> {
oldItem.localeId == newItem.localeId
}
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
oldItem.source == newItem.source
}
else -> false
}
}
override fun areContentsTheSame(oldItem: SourceConfigItem, newItem: SourceConfigItem): Boolean {
return when {
oldItem is SourceConfigItem.LocaleHeader && newItem is SourceConfigItem.LocaleHeader -> {
oldItem.title == newItem.title && oldItem.isExpanded == newItem.isExpanded
}
oldItem is SourceConfigItem.SourceItem && newItem is SourceConfigItem.SourceItem -> {
oldItem.isEnabled == newItem.isEnabled
}
else -> false
}
}
override fun getChangePayload(oldItem: SourceConfigItem, newItem: SourceConfigItem) = Unit
}

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.settings.sources.adapter
import org.koitharu.kotatsu.core.model.MangaSource
sealed interface SourceConfigItem {
data class LocaleHeader(
val localeId: String?,
val title: String?,
val isExpanded: Boolean,
) : SourceConfigItem
data class SourceItem(
val source: MangaSource,
val isEnabled: Boolean,
) : SourceConfigItem
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.settings.sources.adapter
import androidx.recyclerview.widget.RecyclerView
interface SourceConfigListener {
fun onItemSettingsClick(item: SourceConfigItem.SourceItem)
fun onItemEnabledChanged(item: SourceConfigItem.SourceItem, isEnabled: Boolean)
fun onDragHandleTouch(holder: RecyclerView.ViewHolder)
fun onHeaderClick(header: SourceConfigItem.LocaleHeader)
}

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.utils.ext
import androidx.core.os.LocaleListCompat
import java.util.*
import kotlin.collections.ArrayList
fun LocaleListCompat.toList(): List<Locale> {
val list = ArrayList<Locale>(size())
@@ -26,4 +25,8 @@ inline fun <R, C : MutableCollection<in R>> LocaleListCompat.mapTo(
inline fun <T> LocaleListCompat.map(block: (Locale) -> T): List<T> {
return mapTo(ArrayList(size()), block)
}
inline fun <T> LocaleListCompat.mapToSet(block: (Locale) -> T): Set<T> {
return mapTo(LinkedHashSet(size()), block)
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_expand_less" android:state_checked="true" />
<item android:drawable="@drawable/ic_expand_more" android:state_checked="false" />
</selector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
</vector>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground"
android:drawablePadding="12dp"
android:gravity="center_vertical|start"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
app:drawableEndCompat="@drawable/ic_expand_collapse"
app:drawableTint="?android:textColorPrimary"
tools:text="@tools:sample/full_names" />