Files
Murine-launcher/src/app/murinelauncher/widget/radio/RadioGroupPreference.kt
T

357 lines
14 KiB
Kotlin

package app.murinelauncher.widget.radio
import android.content.Context
import android.content.ContextWrapper
import android.content.res.TypedArray
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import com.android.launcher3.R
class RadioGroupPreference @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle,
defStyleRes: Int = 0
) : Preference(context, attrs, defStyleAttr, defStyleRes) {
private var sheetTitle: CharSequence? = null
var iconPosition: RadioGroupBottomSheet.IconPosition = RadioGroupBottomSheet.IconPosition.START
private set
var entryCount: Int = 0
internal set
private var showPreviewIcon: Boolean = true
private var iconTintColor: Int? = null
private var tintSheetIcons: Boolean = true
private var tintPreviewIcon: Boolean = true
private val defaultIconTint: Int
get() = ContextCompat.getColor(context,
com.android.settingslib.widget.theme.R.color.settingslib_materialColorPrimary)
internal var textProviderIdx: ((Context, Int) -> CharSequence)? = null
internal var iconProviderIdx: ((Context, Int) -> Drawable?)? = null
internal var currentIdxProvider: (() -> Int)? = null
internal var onSelectedIdx: ((Int) -> Unit)? = null
internal var summaryProviderIdx: ((Context, Int) -> CharSequence?)? = null
internal var isVisibleProviderIdx: ((Context, Int) -> Boolean)? = null
internal var isEnabledProviderIdx: ((Context, Int) -> Boolean)? = null
internal var extraActionProviderIdx: ((Context, Int) -> android.view.View.OnClickListener?)? = null
private var keyEntries: Array<String>? = null
private var fragmentManager: FragmentManager? = null
private var defaultValueRaw: String? = null
private var needsViewUpdate = false
init {
if (attrs != null) {
val a = context.obtainStyledAttributes(attrs, R.styleable.RadioGroupPreference)
try {
sheetTitle = a.getString(R.styleable.RadioGroupPreference_sheetTitle)
iconPosition = when (a.getInt(R.styleable.RadioGroupPreference_iconPosition, 0)) {
1 -> RadioGroupBottomSheet.IconPosition.END
else -> RadioGroupBottomSheet.IconPosition.START
}
val xmlEntries = a.getInt(R.styleable.RadioGroupPreference_indexedEntries, -1)
if (xmlEntries > 0) entryCount = xmlEntries
showPreviewIcon = a.getBoolean(R.styleable.RadioGroupPreference_showPreviewIcon, true)
iconTintColor = if (a.hasValue(R.styleable.RadioGroupPreference_iconTint))
a.getColor(R.styleable.RadioGroupPreference_iconTint, 0) else null
tintSheetIcons = a.getBoolean(R.styleable.RadioGroupPreference_tintSheetIcons, true)
tintPreviewIcon = a.getBoolean(R.styleable.RadioGroupPreference_tintPreviewIcon, true)
} finally {
a.recycle()
}
}
}
// Common setters
fun setFragmentManager(fm: FragmentManager) { fragmentManager = fm }
fun getSheetTitle(): CharSequence = sheetTitle ?: title ?: ""
fun setSheetTitle(title: CharSequence?) { sheetTitle = title }
fun setIconPosition(position: RadioGroupBottomSheet.IconPosition) { iconPosition = position }
fun setShowPreviewIcon(show: Boolean) { showPreviewIcon = show }
fun setIconTint(color: Int?) { iconTintColor = color }
fun setTintSheetIcons(tint: Boolean) { tintSheetIcons = tint }
fun setTintPreviewIcon(tint: Boolean) { tintPreviewIcon = tint }
fun setEntryCount(count: Int) { entryCount = count; keyEntries = null }
fun setTextProvider(provider: (Context, Int) -> CharSequence) {
textProviderIdx = provider
scheduleViewUpdate()
}
fun setIconProvider(provider: ((Context, Int) -> Drawable?)?) {
iconProviderIdx = provider
scheduleViewUpdate()
}
fun setCurrentValue(provider: () -> Int) {
currentIdxProvider = provider
scheduleViewUpdate()
}
fun setOnSelected(listener: (Int) -> Unit) {
onSelectedIdx = listener
}
fun setOnItemSelectedListener(listener: RadioGroupBottomSheet.OnItemSelectedListener) {
onSelectedIdx = { listener.onItemSelected(it) }
}
fun setSummaryProvider(provider: ((Context, Int) -> CharSequence?)?) {
summaryProviderIdx = provider
scheduleViewUpdate()
}
fun setVisibleProvider(provider: ((Context, Int) -> Boolean)?) {
isVisibleProviderIdx = provider
}
fun setEnabledProvider(provider: ((Context, Int) -> Boolean)?) {
isEnabledProviderIdx = provider
}
/**
* Enable saving preference as Enum (internally saved with enum name rather than index)
*/
fun <E : Enum<E>> asEnum(enumClass: Class<E>): Typed<E> = asList(enumClass.enumConstants!!.asList()) { it.name }
/**
* Create a Typed preference backed by a dynamic list with string-key persistence.
* @param items the list of items to display
* @param keyProvider maps each item to a unique persistence key
*/
fun <T> asList(items: Collection<T>, keyProvider: (T) -> String): Typed<T> {
val arr = @Suppress("UNCHECKED_CAST") (items.toTypedArray<Any?>() as Array<T>)
entryCount = items.size
keyEntries = items.map(keyProvider).toTypedArray()
return Typed(this, arr)
}
private fun persistValue(index: Int) {
if (!isPersistent) return
val stringKey = keyEntries?.getOrNull(index)
try {
if (stringKey == null) persistInt(index)
else persistString(stringKey)
} catch (_: ClassCastException) {
val prefs = preferenceManager?.sharedPreferences ?: return
val key = key ?: return
prefs.edit().remove(key).apply()
if (stringKey == null) persistInt(index)
else persistString(stringKey)
}
}
@Suppress("EmptyRange")
private fun getPersistedIndex(): Int? {
if (!isPersistent) return null
val prefs = preferenceManager?.sharedPreferences ?: return null
val key = key ?: return null
if (!prefs.contains(key)) return null
val value = try {
prefs.getString(key, null)
} catch (e: ClassCastException) {
try {
prefs.getInt(key, Int.MIN_VALUE)
} catch (e: ClassCastException) {
null
}
}
val ke = keyEntries
return when (value) {
is Int -> value.takeIf { it in if (ke != null) ke.indices else 0 until entryCount }
is String -> {
if (ke == null) value.toIntOrNull()?.takeIf { it in 0 until entryCount }
else {
val idx = ke.indexOf(value)
if (idx >= 0) idx else null
}
}
else -> null
}
}
internal fun resolveCurrentIndex(): Int {
return currentIdxProvider?.invoke() ?: getPersistedIndex() ?: resolveDefaultIndex()
}
override fun onGetDefaultValue(a: TypedArray, index: Int): Any? {
val raw = a.getString(index)
defaultValueRaw = raw
return raw
}
override fun onSetInitialValue(defaultValue: Any?) {
if (defaultValue is String) defaultValueRaw = defaultValue
}
override fun setDefaultValue(defaultValue: Any?) {
super.setDefaultValue(defaultValue)
defaultValueRaw = "$defaultValue"
}
private fun resolveDefaultIndex(): Int {
val raw = defaultValueRaw ?: return 0
val asInt = raw.toIntOrNull()
if (asInt != null) return asInt.coerceIn(0, (entryCount - 1).coerceAtLeast(0))
val ke = keyEntries ?: return 0
val idx = ke.indexOf(raw)
return if (idx >= 0) idx else 0
}
private fun resolveFragmentManager(): FragmentManager? {
fragmentManager?.let { return it }
var ctx = context
while (ctx is ContextWrapper) {
if (ctx is FragmentActivity) return ctx.supportFragmentManager
ctx = ctx.baseContext
}
return null
}
private fun resolveIconTint(): Int = iconTintColor ?: defaultIconTint
private fun applyTint(drawable: Drawable) {
drawable.colorFilter = PorterDuffColorFilter(resolveIconTint(), PorterDuff.Mode.SRC_IN)
}
private fun scheduleViewUpdate() {
if (isAttached) {
updatePreferenceView()
} else {
needsViewUpdate = true
}
}
private val isAttached: Boolean
get() = preferenceManager != null
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
super.onAttachedToHierarchy(preferenceManager)
if (needsViewUpdate || (showPreviewIcon && iconProviderIdx != null)) {
needsViewUpdate = false
updatePreferenceView()
}
}
fun updatePreferenceView() {
val ctx = context
val idx = resolveCurrentIndex()
summary = summaryProviderIdx?.invoke(ctx, idx) ?: textProviderIdx?.invoke(ctx, idx)
if (showPreviewIcon) {
icon = iconProviderIdx?.invoke(ctx, idx)?.also { if (tintPreviewIcon) applyTint(it) }
}
}
/**
* Shows the selection bottom sheet programmatically.
* Requires the context chain to contain a [FragmentActivity].
*/
fun showSheet() {
onClick()
}
override fun onClick() {
val fm = resolveFragmentManager() ?: return
val ctx = context
val tp = textProviderIdx ?: { _: Context, i: Int -> i.toString() }
val currentIdx = resolveCurrentIndex()
// Remove any stale/restored fragment with the same tag before showing a new one
fm.findFragmentByTag(RadioGroupBottomSheet.TAG)?.let {
fm.beginTransaction().remove(it).commitAllowingStateLoss()
fm.executePendingTransactions()
}
val sheet = RadioGroupBottomSheet()
sheet.setStyle(DialogFragment.STYLE_NORMAL, com.android.settingslib.widget.theme.R.style.Theme_SettingsLib_BottomSheetDialog)
sheet.configure(
title = getSheetTitle(),
entryCount = entryCount,
iconPosition = iconPosition,
currentIndex = currentIdx,
textProvider = { i -> tp(ctx, i) },
iconProvider = iconProviderIdx?.let { p -> { i -> p(ctx, i) } },
iconTint = if (tintSheetIcons) resolveIconTint() else null,
isVisibleProvider = isVisibleProviderIdx?.let { p -> { i -> p(ctx, i) } },
isEnabledProvider = isEnabledProviderIdx?.let { p -> { i -> p(ctx, i) } },
extraActionProvider = extraActionProviderIdx?.let { p -> { i -> p(ctx, i) } },
listener = RadioGroupBottomSheet.OnItemSelectedListener { index ->
if (!callChangeListener(index)) return@OnItemSelectedListener
persistValue(index)
onSelectedIdx?.invoke(index)
updatePreferenceView()
}
)
sheet.show(fm, RadioGroupBottomSheet.TAG)
}
class Typed<T> internal constructor(
val preference: RadioGroupPreference,
private val entries: Array<T>
) {
private fun toIndex(value: T): Int = if (value is Enum<*>) value.ordinal else entries.indexOf(value)
fun setDefaultValue(value: T) = preference.setDefaultValue(value)
fun updatePreferenceView() = preference.updatePreferenceView()
fun setTextProvider(provider: (Context, T) -> CharSequence) {
preference.textProviderIdx = { ctx, i -> provider(ctx, entries[i]) }
preference.scheduleViewUpdate()
}
fun setIconProvider(provider: ((Context, T) -> Drawable?)?) {
preference.iconProviderIdx = provider?.let { p -> { ctx, i -> p(ctx, entries[i]) } }
preference.scheduleViewUpdate()
}
fun setCurrentValue(provider: () -> T) {
preference.currentIdxProvider = { toIndex(provider()).coerceAtLeast(0) }
preference.scheduleViewUpdate()
}
fun setOnSelected(listener: (T) -> Unit) {
preference.onSelectedIdx = { i -> listener(entries[i]) }
}
fun setSummaryProvider(provider: ((Context, T) -> CharSequence?)?) {
preference.summaryProviderIdx = provider?.let { p -> { ctx, i -> p(ctx, entries[i]) } }
preference.scheduleViewUpdate()
}
fun setVisibleProvider(provider: ((Context, T) -> Boolean)?) {
preference.isVisibleProviderIdx = provider?.let { p -> { ctx, i -> p(ctx, entries[i]) } }
}
fun setEnabledProvider(provider: ((Context, T) -> Boolean)?) {
preference.isEnabledProviderIdx = provider?.let { p -> { ctx, i -> p(ctx, entries[i]) } }
}
/** A button on the right of an entry (null for none), e.g. to edit it */
fun setExtraActionProvider(provider: ((Context, T) -> android.view.View.OnClickListener?)?) {
preference.extraActionProviderIdx = provider?.let { p -> { ctx, i -> p(ctx, entries[i]) } }
}
fun setOnPreferenceChangeListener(listener: ((T?) -> Boolean)?) {
preference.onPreferenceChangeListener = listener?.let { l -> OnPreferenceChangeListener {
_, i -> l((i as? Int)?.let { entries[i] }) }
}
}
}
}