Skip to content

Commit 8eeb433

Browse files
authored
Handle color selector in DeferredColor.resolve (#32)
* Improve attribute resolution error messages * Add KDoc warnings about attribute color selectors
1 parent 57a43d9 commit 8eeb433

File tree

7 files changed

+110
-22
lines changed

7 files changed

+110
-22
lines changed
Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,73 @@
11
package com.backbase.deferredresources
22

33
import android.graphics.Color
4+
import androidx.test.filters.SdkSuppress
45
import com.backbase.deferredresources.test.R
56
import com.google.common.truth.Truth.assertThat
67
import org.junit.Test
78

89
class DeferredColorTest {
910

10-
@Test fun constant_withIntValue_returnsSameValue() {
11+
//region Constant
12+
@Test fun constantResolve_withIntValue_returnsSameValue() {
1113
val deferred = DeferredColor.Constant(Color.MAGENTA)
1214
assertThat(deferred.resolve(context)).isEqualTo(Color.MAGENTA)
1315
}
1416

15-
@Test fun constant_withStringValue_returnsParsedValue() {
17+
@Test fun constantResolve_withStringValue_returnsParsedValue() {
1618
val deferred = DeferredColor.Constant("#00FF00")
1719
assertThat(deferred.resolve(context)).isEqualTo(Color.GREEN)
1820
}
21+
//endregion
1922

20-
@Test fun resource_resolvesWithContext() {
23+
//region Resource
24+
@Test fun resourceResolve_withStandardColor_resolvesColor() {
2125
val deferred = DeferredColor.Resource(R.color.blue)
2226
assertThat(deferred.resolve(context)).isEqualTo(Color.BLUE)
2327
}
2428

25-
@Test fun attribute_resolvesWithContext() {
29+
@Test fun resourceResolve_withSelectorColor_resolvesDefaultColor() {
30+
val deferred = DeferredColor.Resource(R.color.stateful_color_without_attr)
31+
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#aaaaaa"))
32+
}
33+
34+
@SdkSuppress(minSdkVersion = 23)
35+
@Test fun resourceResolve_withSelectorColorWithAttribute_resolvesDefaultColor() {
36+
val deferred = DeferredColor.Resource(R.color.stateful_color_with_attr)
37+
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#987654"))
38+
}
39+
//endregion
40+
41+
//region Attribute
42+
@Test fun attributeResolve_withStandardColor_resolvesColor() {
2643
val deferred = DeferredColor.Attribute(R.attr.colorPrimary)
27-
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#212121"))
44+
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#987654"))
45+
}
46+
47+
@Test fun attributeResolve_withSelectorColor_resolvesDefaultColor() {
48+
val deferred = DeferredColor.Attribute(R.attr.subtitleTextColor)
49+
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#aaaaaa"))
50+
}
51+
52+
@SdkSuppress(minSdkVersion = 23)
53+
@Test fun attributeResolve_withSelectorColorWithAttributeDefault_resolvesDefaultColor() {
54+
val deferred = DeferredColor.Attribute(R.attr.titleTextColor)
55+
assertThat(deferred.resolve(AppCompatContext())).isEqualTo(Color.parseColor("#987654"))
2856
}
2957

3058
@Test(expected = IllegalArgumentException::class)
31-
fun attribute_withUnknownAttribute_throwsException() {
59+
fun attributeResolve_withUnknownAttribute_throwsException() {
3260
val deferred = DeferredColor.Attribute(R.attr.colorPrimary)
3361

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

3866
@Test(expected = IllegalArgumentException::class)
39-
fun attribute_withWrongAttributeType_throwsException() {
67+
fun attributeResolve_withWrongAttributeType_throwsException() {
4068
val deferred = DeferredColor.Attribute(R.attr.isLightTheme)
4169

4270
deferred.resolve(AppCompatContext())
4371
}
72+
//endregion
4473
}

deferred-resources/src/androidTest/java/com/backbase/deferredresources/TestContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal fun AppCompatContext(
2525
light: Boolean = false
2626
): Context = ContextThemeWrapper(
2727
context,
28-
if (light) R.style.Theme_AppCompat_Light else R.style.Theme_AppCompat
28+
if (light) R.style.TestTheme_Light else R.style.TestTheme
2929
)
3030

3131
//region Configuration
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<item android:color="#dbdbdb" android:state_enabled="false" />
5+
6+
<item android:color="@android:color/darker_gray" android:state_checked="true" />
7+
8+
<item android:color="?colorPrimary" />
9+
10+
</selector>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<item android:color="#dbdbdb" android:state_enabled="false" />
5+
6+
<item android:color="@android:color/darker_gray" />
7+
8+
</selector>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<style name="TestTheme" parent="Theme.AppCompat">
4+
<item name="colorPrimary">#987654</item>
5+
<item name="titleTextColor">@color/stateful_color_with_attr</item>
6+
<item name="subtitleTextColor">@color/stateful_color_without_attr</item>
7+
</style>
8+
9+
<style name="TestTheme.Light" parent="Theme.AppCompat.Light">
10+
<item name="colorPrimary">#987654</item>
11+
<item name="titleTextColor">@color/stateful_color_with_attr</item>
12+
<item name="subtitleTextColor">@color/stateful_color_without_attr</item>
13+
</style>
14+
</resources>

deferred-resources/src/main/java/com/backbase/deferredresources/DeferredColor.kt

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.backbase.deferredresources
22

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

6369
/**
64-
* Resolve [resId] to a [ColorInt] with the given [context]'s theme.
70+
* Resolve [resId] to a [ColorInt] with the given [context]'s theme. If [resId] would resolve a color selector,
71+
* resolves to the default color of that selector.
72+
*
73+
* Warning: On API < 23, resolving a color selector with [context]'s theme is unsupported. Thus, a color
74+
* selector with an attribute reference as its default color will not resolve to the correct color on API 22 and
75+
* below. A color selector with a resource reference as its default color will resolve correctly.
6576
*
6677
* @throws IllegalArgumentException if [resId] cannot be resolved to a color.
6778
*/
68-
@ColorInt override fun resolve(context: Context): Int = context.resolveColorAttribute(resId)
69-
70-
@ColorInt private fun Context.resolveColorAttribute(@AttrRes resId: Int): Int =
71-
resolveAttribute(
72-
resId, "color", reusedTypedValue,
73-
TypedValue.TYPE_INT_COLOR_RGB8, TypedValue.TYPE_INT_COLOR_ARGB8,
74-
TypedValue.TYPE_INT_COLOR_RGB4, TypedValue.TYPE_INT_COLOR_ARGB4
75-
) {
79+
@ColorInt override fun resolve(context: Context): Int = context.resolveColorAttribute {
80+
if (type == TypedValue.TYPE_STRING)
81+
context.resolveColorStateList().defaultColor
82+
else
7683
data
77-
}
84+
}
85+
86+
private inline fun <T> Context.resolveColorAttribute(
87+
toTypeSafeResult: TypedValue.() -> T
88+
): T = resolveAttribute(
89+
resId, "color", reusedTypedValue,
90+
TypedValue.TYPE_INT_COLOR_RGB8, TypedValue.TYPE_INT_COLOR_ARGB8,
91+
TypedValue.TYPE_INT_COLOR_RGB4, TypedValue.TYPE_INT_COLOR_ARGB4,
92+
TypedValue.TYPE_STRING,
93+
toTypeSafeResult = toTypeSafeResult
94+
)
95+
96+
private fun Context.resolveColorStateList(): ColorStateList = resolveAttribute(
97+
resId, "reference", reusedTypedValue,
98+
TypedValue.TYPE_REFERENCE,
99+
resolveRefs = false
100+
) {
101+
val colorSelectorResId = data
102+
ContextCompat.getColorStateList(this@resolveColorStateList, colorSelectorResId)!!
103+
}
78104
}
79105
}

deferred-resources/src/main/java/com/backbase/deferredresources/internal/Context.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ internal inline fun <T> Context.resolveAttribute(
2828
attributeTypeName: String,
2929
reusedTypedValue: TypedValue,
3030
vararg expectedTypes: Int,
31+
resolveRefs: Boolean = true,
3132
toTypeSafeResult: TypedValue.() -> T
3233
): T {
3334
try {
34-
val isResolved = theme.resolveAttribute(resId, reusedTypedValue, true)
35+
val isResolved = theme.resolveAttribute(resId, reusedTypedValue, resolveRefs)
3536
if (isResolved && expectedTypes.contains(reusedTypedValue.type))
3637
return reusedTypedValue.toTypeSafeResult()
3738
else
@@ -53,13 +54,13 @@ private fun Context.createErrorMessage(
5354
) = try {
5455
val name = resources.getResourceEntryName(resId)
5556
val couldNotResolve = "Could not resolve attribute <$name>"
56-
val withContext = "with <$this>"
57+
val withContext = "with context <$this>"
5758
if (isResolved)
5859
"$couldNotResolve to a $attributeTypeName $withContext"
5960
else
6061
"$couldNotResolve $withContext"
6162
} catch (notFoundException: Resources.NotFoundException) {
62-
"Attribute <$resId> could not be found with <$this>"
63+
"Attribute <$resId> could not be found with context <$this>"
6364
}
6465

6566
/**

0 commit comments

Comments
 (0)