afollestad
afollestad

Reputation: 2934

How to get the raw, unresolved value of an attribute from an AttributeSet?

I'm trying to find a way to get the raw unresolved value of an attribute. By that, I mean pretty much the exact text in the layout file, without having to parse the file directly.

Say there was a TextView like this:

<TextView
    ...
    android:textColor="?colorAccent"
    />

I want to be able to pull out "?colorAccent" as a string, or the entry name of the value (like package:attr/colorAccent).

Is this possible without an XMLPullParser?

Upvotes: 2

Views: 634

Answers (2)

afollestad
afollestad

Reputation: 2934

Going on what @Ben P. provided, I was able to come up with something that doesn't use reflection:

class Attributes(
  private val context: Context,
  private val attrs: AttributeSet
) {
  fun getRawValue(@AttrRes attrId: Int): String {
    val res = context.resources
    val attrName = res.getResourceName(attrId)
    val attrIndex = attrs.indexOfAttr(context) { it == attrName }
    if (attrIndex == -1) return ""
    val attrValue = attrs.getAttributeValue(attrIndex)

    return when {
      attrValue.startsWith('@') || attrValue.startsWith('?') -> {
        val id = attrValue.substring(1)
            .toInt()
        res.getResourceName(id)
      }
      else -> attrValue
    }
  }
}

private fun AttributeSet.indexOfAttr(
  context: Context,
  matcher: (String) -> (Boolean)
): Int {
  for (i in 0 until attributeCount) {
    val nameResource = getAttributeNameResource(i)
    val literalName = if (nameResource != 0) context.resources.getResourceName(nameResource) else ""
    if (matcher(literalName)) {
      return i
    }
  }
  return -1
}

Here's a usage example:

class MyTextView(
  context: Context, 
  attrs: AttributeSet? = null
) : AppCompatTextView() {

  init {
     if (attrs != null) {
         val attributes = Attributes(context, attrs)
         val rawTextColor = attributes.getRawValue(android.R.attr.textColor)
         setText(rawTextColor)
     }
  }
}

The text view's text would be set to the "raw value" of Android's text color attribute which is provided in your layout XML.

If your text view's text was set to AndroidX's ?colorAccent, the above code would populate the text view with the text [your-package]:attr/colorAccent. It would do the same for a resource, returning the whole entry name including the package. If the attribute was set to literal text, it would just return that.

Upvotes: 0

Ben P.
Ben P.

Reputation: 54214

I'm using a view tag that looks like this for these examples:

<EditText 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:hint="this is the hint"
    android:text="@string/dummy_content"
    android:textColor="?colorAccent" />

What's notable about this is that android:hint is plain text, android:text is a resource attribute, and android:textColor is a style attribute.

For all three, I start with AttributeSet.getAttributeValue(). For plain-text attributes, this will give you the actual value (e.g. for android:hint this returns "this is the hint").

Resource attributes instead return a string that starts with @ and is then a number (e.g. for android:text this returns "@2131689506"). You can then parse the numeric portion of this string and use Resources.getResourceName() to get the resolved name ("com.example.stackoverflow:string/dummy_content").

Style attributes return a string that starts with ? and is then a number (e.g. for android:textColor this returns "?2130903135"). However, I do not know of any way to convert this number into a textual representation with supported APIs. Hopefully, though, this is enough to help someone else on the way to the full answer.

Using reflection

If you're willing to go off the rails, though, you can use reflection to find the text value of the style attribute. Because the string starts with ?, you know it is either in R.attr or android.R.attr. You can scan these for a matching field with code like this:

private static String scan(Class<?> classToSearch, int target) {
    for (Field field : classToSearch.getDeclaredFields()) {
        try {
            int fieldValue = (int) field.get(null);

            if (fieldValue == target) {
                return field.getName();
            }
        } catch (IllegalAccessException e) {
           // TODO
        }
    }

    return null;
}
int id = Integer.parseInt(attributeValue.substring(1));
String attrName = scan(R.attr.class, id);
String androidAttrName = scan(android.R.attr.class, id);

For me, this will output

colorAccent
null

If the value of android:textColor were ?android:colorAccent instead of just ?colorAccent, the output would instead be:

null
colorAccent

Upvotes: 1

Related Questions