결론부터 말하자면 View의 Background를 리소스로 설정시에 color 리소스가 아닌 drawable 리소스만을 사용해야 합니다.
(물론 대다수의 경우들은 drawable만 사용합니다....)
Android UI 작업을 하다보면 View의 Background를 설정하는 경우는 매우 흔한 일입니다.
이때 pressed 효과를 위해 StateListDrawable 형태의 drawable 리소스를 쓰는 경우도 많이 생깁니다.
그리고 Android OS 10에서부터는 ColorStateList 형태의 color 리소스도 View의 Background에서 사용이 가능해졌습니다.
(흔히 말하는 selector 리소스 형태들입니다.)
- ColorStateList 리소스 : https://developer.android.com/guide/topics/resources/color-list-resource
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="<http://schemas.android.com/apk/res/android>">
<item android:color="@color/white" android:state_selected="true" />
<item android:color="@color/white" android:state_pressed="true" />
<item android:color="@color/black" />
</selector>
color/selector_rect_color.xml
- Drawable 리소스 : https://developer.android.com/guide/topics/resources/drawable-resource
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="<http://schemas.android.com/apk/res/android>">
<item android:drawable="@color/white" android:state_selected="true" />
<item android:drawable="@color/white" android:state_enabled="true" />
<item android:drawable="@color/black" />
</selector>
drawable/selector_rect.xml
즉 최신 Android OS에서 ColorStateList 리소스로 View.setBackgroundResource() 를 사용할 수 있지만, OS 9 이하에서는 Resource NotFoundException을 겪게 됩니다.
1. 문제 재현해보기
먼저 문제를 재현해 보고, 정확히 문제가 되는 부분을 찾아봅니다.
try {
val drawable = context.getDrawable(R.drawable.selector_rect)
Log.d(TAG, "ResourceTest :: 0. $drawable")
val drawable2 = context.getDrawable(R.color.selector_rect_color)
Log.d(TAG, "ResourceTest :: 1. $drawable2")
} catch(e: Exception) {
Log.d(TAG, "ResourceTest :: 2. $e")
}
앱에서 테스트할 부분에 위 코드를 추가 후 OS 10과 OS 9 기기에 올려서 확인해봅니다.
(기기가 없어도 에뮬레이터로 확인 가능합니다.)
각가 아래와 같은 로그를 얻을 수 있습니다.
ResourceTest :: 0. android.graphics.drawable.StateListDrawable@ba98d70
ResourceTest :: 1. android.graphics.drawable.ColorStateListDrawable@c5c796e
ResourceTest :: 0. android.graphics.drawable.StateListDrawable@e71743d
ResourceTest :: 2. android.content.res.Resources$NotFoundException: Drawable com.test.app:color/selector_rect_color with resource ID #0x7f0601c3
2. 문제 범위 확인
문제 발생시 CallStack을 확인하여 비교할 코드 범위를 좁혀봅니다.
android.content.res.Resources$NotFoundException: Drawable com.test.app:color/selector_rect_color with resource ID #0x7f0601c3
Caused by: android.content.res.Resources$NotFoundException: File res/color/selector_rect_color.xml from drawable resource ID #0x7f0601c3
at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:847)
at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
at android.content.res.Resources.getDrawableForDensity(Resources.java:888)
at android.content.res.Resources.getDrawable(Resources.java:827)
at android.content.Context.getDrawable(Context.java:626)
...
Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #3: <item> tag requires a 'drawable' attribute or child tag defining a drawable
at android.graphics.drawable.StateListDrawable.inflateChildElements(StateListDrawable.java:190)
at android.graphics.drawable.StateListDrawable.inflate(StateListDrawable.java:122)
at android.graphics.drawable.DrawableInflater.inflateFromXmlForDensity(DrawableInflater.java:142)
at android.graphics.drawable.Drawable.createFromXmlInnerForDensity(Drawable.java:1332)
at android.graphics.drawable.Drawable.createFromXmlForDensity(Drawable.java:1291)
at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:833)
... 20 more
위 로그로 ResourcesImpl.loadDrawableForCookie 에서 Exception을 발생함을 확인할 수 있습니다.
3. AOSP를 통한 소스 확인
이 부분은 aosp 소스로 그 차이를 확인할 수 있습니다.
@Nullable
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
...
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
LookupStack stack = mLookupStack.get();
try {
// Perform a linear search to check if we have already referenced this resource before.
if (stack.contains(id)) {
throw new Exception("Recursive reference in drawable");
}
stack.push(id);
try {
if (file.endsWith(".xml")) {
final XmlResourceParser rp = loadXmlResourceParser(
file, id, value.assetCookie, "drawable");
dr = Drawable.createFromXmlForDensity(wrapper, rp, density, null);
rp.close();
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
} finally {
stack.pop();
}
} catch (Exception | StackOverflowError e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
...
@Nullable
private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
...
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
LookupStack stack = mLookupStack.get();
try {
// Perform a linear search to check if we have already referenced this resource before.
if (stack.contains(id)) {
throw new Exception("Recursive reference in drawable");
}
stack.push(id);
try {
if (file.endsWith(".xml")) {
if (file.startsWith("res/color/")) {
dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
} else {
dr = loadXmlDrawable(wrapper, value, id, density, file);
}
} else {
final InputStream is = mAssets.openNonAsset(
value.assetCookie, file, AssetManager.ACCESS_STREAMING);
AssetInputStream ais = (AssetInputStream) is;
dr = decodeImageDrawable(ais, wrapper, value);
}
} finally {
stack.pop();
}
} catch (Exception | StackOverflowError e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
rnf.initCause(e);
throw rnf;
}
...
private Drawable loadColorOrXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density, String file) {
try {
ColorStateList csl = loadColorStateList(wrapper, value, id, null);
return new ColorStateListDrawable(csl);
} catch (NotFoundException originalException) {
// If we fail to load as color, try as normal XML drawable
try {
return loadXmlDrawable(wrapper, value, id, density, file);
} catch (Exception ignored) {
// If fallback also fails, throw the original exception
throw originalException;
}
}
}
...
private Drawable loadXmlDrawable(@NonNull Resources wrapper, @NonNull TypedValue value,
int id, int density, String file)
throws IOException, XmlPullParserException {
try (
XmlResourceParser rp =
loadXmlResourceParser(file, id, value.assetCookie, "drawable")
) {
return Drawable.createFromXmlForDensity(wrapper, rp, density, null);
}
}
위 소스 확인으로 OS 10 이후부터는 ColorStateList 리소스도 처리가 가능하지만 그 미만 버전에서 문제가 되는 경우가 발생하는 부분을 확인할 수 있습니다.
컴파일시 에러가 나거나 버전 분기가 필요한 부분으로 체크 되지도 않기 때문에 생각없이 지나치기 쉬운 부분이 될 수도 있습니다.
만약 앱의 minSdkVersion이 29 미만이라면 항상 위 내용을 주의할 필요가 있습니다.