Skip to content

Commit

Permalink
Handle color selector in DeferredColor.resolve (#32)
Browse files Browse the repository at this point in the history
* Improve attribute resolution error messages
* Add KDoc warnings about attribute color selectors
  • Loading branch information
drewhamilton committed Jul 22, 2020
1 parent 57a43d9 commit 8eeb433
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -1,44 +1,73 @@
package com.backbase.deferredresources

import android.graphics.Color
import androidx.test.filters.SdkSuppress
import com.backbase.deferredresources.test.R
import com.google.common.truth.Truth.assertThat
import org.junit.Test

class DeferredColorTest {

@Test fun constant_withIntValue_returnsSameValue() {
//region Constant
@Test fun constantResolve_withIntValue_returnsSameValue() {
val deferred = DeferredColor.Constant(Color.MAGENTA)
assertThat(deferred.resolve(context)).isEqualTo(Color.MAGENTA)
}

@Test fun constant_withStringValue_returnsParsedValue() {
@Test fun constantResolve_withStringValue_returnsParsedValue() {
val deferred = DeferredColor.Constant("#00FF00")
assertThat(deferred.resolve(context)).isEqualTo(Color.GREEN)
}
//endregion

@Test fun resource_resolvesWithContext() {
//region Resource
@Test fun resourceResolve_withStandardColor_resolvesColor() {
val deferred = DeferredColor.Resource(R.color.blue)
assertThat(deferred.resolve(context)).isEqualTo(Color.BLUE)
}

@Test fun attribute_resolvesWithContext() {
@Test fun resourceResolve_withSelectorColor_resolvesDefaultColor() {
val deferred = DeferredColor.Resource(R.color.stateful_color_without_attr)
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#aaaaaa"))
}

@SdkSuppress(minSdkVersion = 23)
@Test fun resourceResolve_withSelectorColorWithAttribute_resolvesDefaultColor() {
val deferred = DeferredColor.Resource(R.color.stateful_color_with_attr)
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#987654"))
}
//endregion

//region Attribute
@Test fun attributeResolve_withStandardColor_resolvesColor() {
val deferred = DeferredColor.Attribute(R.attr.colorPrimary)
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#212121"))
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#987654"))
}

@Test fun attributeResolve_withSelectorColor_resolvesDefaultColor() {
val deferred = DeferredColor.Attribute(R.attr.subtitleTextColor)
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#aaaaaa"))
}

@SdkSuppress(minSdkVersion = 23)
@Test fun attributeResolve_withSelectorColorWithAttributeDefault_resolvesDefaultColor() {
val deferred = DeferredColor.Attribute(R.attr.titleTextColor)
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#987654"))
}

@Test(expected = IllegalArgumentException::class)
fun attribute_withUnknownAttribute_throwsException() {
fun attributeResolve_withUnknownAttribute_throwsException() {
val deferred = DeferredColor.Attribute(R.attr.colorPrimary)

// Default-theme context does not have <colorPrimary> attribute:
deferred.resolve(context)
}

@Test(expected = IllegalArgumentException::class)
fun attribute_withWrongAttributeType_throwsException() {
fun attributeResolve_withWrongAttributeType_throwsException() {
val deferred = DeferredColor.Attribute(R.attr.isLightTheme)

deferred.resolve(AppCompatContext())
}
//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal fun AppCompatContext(
light: Boolean = false
): Context = ContextThemeWrapper(
context,
if (light) R.style.Theme_AppCompat_Light else R.style.Theme_AppCompat
if (light) R.style.TestTheme_Light else R.style.TestTheme
)

//region Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

<item android:color="#dbdbdb" android:state_enabled="false" />

<item android:color="@android:color/darker_gray" android:state_checked="true" />

<item android:color="?colorPrimary" />

</selector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

<item android:color="#dbdbdb" android:state_enabled="false" />

<item android:color="@android:color/darker_gray" />

</selector>
14 changes: 14 additions & 0 deletions deferred-resources/src/androidTest/res/values/test_theme.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TestTheme" parent="Theme.AppCompat">
<item name="colorPrimary">#987654</item>
<item name="titleTextColor">@color/stateful_color_with_attr</item>
<item name="subtitleTextColor">@color/stateful_color_without_attr</item>
</style>

<style name="TestTheme.Light" parent="Theme.AppCompat.Light">
<item name="colorPrimary">#987654</item>
<item name="titleTextColor">@color/stateful_color_with_attr</item>
<item name="subtitleTextColor">@color/stateful_color_without_attr</item>
</style>
</resources>
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.backbase.deferredresources

import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.TypedValue
import androidx.annotation.AttrRes
Expand Down Expand Up @@ -45,7 +46,12 @@ interface DeferredColor {
@ColorRes private val resId: Int
) : DeferredColor {
/**
* Resolve [resId] to a [ColorInt] with the given [context].
* Resolve [resId] to a [ColorInt] with the given [context]. If [resId] resolves to a color selector resource,
* resolves the default color of that selector.
*
* Warning: On API < 23, resolving a color selector with [context]'s theme is unsupported. Thus, a color
* selector with an attribute reference as its default color will not resolve to the correct color on API 22 and
* below. A color selector with a resource reference as its default color will resolve correctly.
*/
@ColorInt override fun resolve(context: Context): Int = ContextCompat.getColor(context, resId)
}
Expand All @@ -61,19 +67,39 @@ interface DeferredColor {
private val reusedTypedValue = TypedValue()

/**
* Resolve [resId] to a [ColorInt] with the given [context]'s theme.
* Resolve [resId] to a [ColorInt] with the given [context]'s theme. If [resId] would resolve a color selector,
* resolves to the default color of that selector.
*
* Warning: On API < 23, resolving a color selector with [context]'s theme is unsupported. Thus, a color
* selector with an attribute reference as its default color will not resolve to the correct color on API 22 and
* below. A color selector with a resource reference as its default color will resolve correctly.
*
* @throws IllegalArgumentException if [resId] cannot be resolved to a color.
*/
@ColorInt override fun resolve(context: Context): Int = context.resolveColorAttribute(resId)

@ColorInt private fun Context.resolveColorAttribute(@AttrRes resId: Int): Int =
resolveAttribute(
resId, "color", reusedTypedValue,
TypedValue.TYPE_INT_COLOR_RGB8, TypedValue.TYPE_INT_COLOR_ARGB8,
TypedValue.TYPE_INT_COLOR_RGB4, TypedValue.TYPE_INT_COLOR_ARGB4
) {
@ColorInt override fun resolve(context: Context): Int = context.resolveColorAttribute {
if (type == TypedValue.TYPE_STRING)
context.resolveColorStateList().defaultColor
else
data
}
}

private inline fun <T> Context.resolveColorAttribute(
toTypeSafeResult: TypedValue.() -> T
): T = resolveAttribute(
resId, "color", reusedTypedValue,
TypedValue.TYPE_INT_COLOR_RGB8, TypedValue.TYPE_INT_COLOR_ARGB8,
TypedValue.TYPE_INT_COLOR_RGB4, TypedValue.TYPE_INT_COLOR_ARGB4,
TypedValue.TYPE_STRING,
toTypeSafeResult = toTypeSafeResult
)

private fun Context.resolveColorStateList(): ColorStateList = resolveAttribute(
resId, "reference", reusedTypedValue,
TypedValue.TYPE_REFERENCE,
resolveRefs = false
) {
val colorSelectorResId = data
ContextCompat.getColorStateList(this@resolveColorStateList, colorSelectorResId)!!
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ internal inline fun <T> Context.resolveAttribute(
attributeTypeName: String,
reusedTypedValue: TypedValue,
vararg expectedTypes: Int,
resolveRefs: Boolean = true,
toTypeSafeResult: TypedValue.() -> T
): T {
try {
val isResolved = theme.resolveAttribute(resId, reusedTypedValue, true)
val isResolved = theme.resolveAttribute(resId, reusedTypedValue, resolveRefs)
if (isResolved && expectedTypes.contains(reusedTypedValue.type))
return reusedTypedValue.toTypeSafeResult()
else
Expand All @@ -53,13 +54,13 @@ private fun Context.createErrorMessage(
) = try {
val name = resources.getResourceEntryName(resId)
val couldNotResolve = "Could not resolve attribute <$name>"
val withContext = "with <$this>"
val withContext = "with context <$this>"
if (isResolved)
"$couldNotResolve to a $attributeTypeName $withContext"
else
"$couldNotResolve $withContext"
} catch (notFoundException: Resources.NotFoundException) {
"Attribute <$resId> could not be found with <$this>"
"Attribute <$resId> could not be found with context <$this>"
}

/**
Expand Down

0 comments on commit 8eeb433

Please sign in to comment.