Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Attempting to get attribute values in code returns incorrect values

I wish to extract multiple attributes from a style resource (only interested in attributes that fall in the TextAppearance Group)

Style defined as so

<style name="Label" parent="@android:style/TextAppearance.Small">
    <item name="android:textColor">@color/floatlabel_text</item>
    <item name="android:textSize">8dp</item>
    <item name="android:textStyle">bold</item>
</style>

First try

First I tried how TextView(lines 663-731) has it implemented, but then I found out we don't have access to com.android.internal.R

Partial Solution

Which is why I switched to this solution: https://stackoverflow.com/a/7913610/3922891

So I created textAppearanceAttr to replace com.android.internal.R.styleable.TextAppearance (only contains 10/13 TextAppearance attributes I am interested in)

int[] textAppearanceAttr = new int[]{    
        android.R.attr.textColor,
        android.R.attr.textSize,
        android.R.attr.typeface,
        android.R.attr.fontFamily,
        android.R.attr.textStyle,
        android.R.attr.textAllCaps,
        android.R.attr.shadowColor,
        android.R.attr.shadowDx,
        android.R.attr.shadowDy,
        android.R.attr.shadowRadius};

Here is how I used it. I get the style's resource id (resource is referenced by a clTextAppearance attribute)

   int ap = a.getResourceId(R.styleable.CustomLabelLayout_clTextAppearance, android.R.style.TextAppearance_Small);
   TypedArray appearance = mContext.obtainStyledAttributes(ap, textAppearanceAttr);

And here is how I get the attributes (still following the answer at the above link):

    mLabelTextColor = appearance.getColorStateList(0);
    mLabelTextSize = appearance.getDimensionPixelSize(1, 15);
    mLabelTypeface = appearance.getInt(2, -1);
    mLabelFontFamily = appearance.getString(3);
    mLabelTextStyle = appearance.getInt(4, -1);
    (5 more...)

Current Issue

It seems that only the first attribute gets set, every other either sets with the default or null.

A hack that seems to work

Individual arrays:

int[] textSizeAttr = new int[] { android.R.attr.textSize};
int[] textStyleAttr = new int[] { android.R.attr.textStyle};

And get attributes like so

    appearance.recycle();
    appearance = mContext.obtainStyledAttributes(ap, textSizeAttr);
    mLabelTextSize = appearance.getDimensionPixelSize(0, 15);
    appearance.recycle();
    appearance = mContext.obtainStyledAttributes(ap, textStyleAttr);
    mLabelTextStyle = appearance.getInt(0, -1);
    appearance.recycle();

Now doing this is such a waste.

Questions

  1. I would like to know why getting all the attributes at once doesn't work.
  2. Is there a solution (where all the extra work is not necessary)?

EDIT 1

I found something similar here: https://stackoverflow.com/a/13952929/3922891 And for some reason it works. Until I add more attributes to the array then everything becomes kerfuffle.

Example:

 int[] attrs = {android.R.attr.textColor,
            android.R.attr.textSize,
            android.R.attr.background,
            android.R.attr.textStyle,
            android.R.attr.textAppearance,
            android.R.attr.textColorLink,
            android.R.attr.orientation,
            android.R.attr.text};

If I get text using the above array it works.

String text = ta.getString(7);

But if I change the array to the below it fails (replaced android.R.attr.orientation with android.R.attr.shadowColor)

int[] attrs = {android.R.attr.textColor,
            android.R.attr.textSize,
            android.R.attr.background,
            android.R.attr.textStyle,
            android.R.attr.textAppearance,
            android.R.attr.textColorLink,
            android.R.attr.shadowColor,
            android.R.attr.text};

Why is this happening? (Question #1)

like image 637
PrivatMamtora Avatar asked Aug 20 '14 10:08

PrivatMamtora


3 Answers

I think I have an idea why it's happening. Looks like if IDs are not sorted, you get issues. textColor for example has the lowest int value, that's why it starts working being placed on the first position in the array.

If you take a look at R.java with your stylable, you'll see that android resource compiler has sorted the IDs for you. That's why it always works if you declare stylable in attrs.xml and may not work if you manually created the arrays of IDs.

I believe there is a performance reason for IDs to be sorted. If they are sorted then attributes can be read from the AttributeSet using one traversal instead of N traversals in case of N ids.

UPDATE: I took a look at the source code and it proves my idea. Context.obtainStyledAttributes() calls JNI method AssetManager.applyStyle(). You can find source here:

https://android.googlesource.com/platform/frameworks/base.git/+/android-4.3_r2.1/core/jni/android_util_AssetManager.cpp

On line 1001 you'll find a while loop where ix (index in the extracted XML attributes array) is always incremented and never reset to 0. This means if textColor is the last index in the array (variable "src" in the code) then we'll never get to that attribute.

like image 154
igret Avatar answered Nov 15 '22 14:11

igret


Thanks to @PrivatMamtora and @igret for investigating this! If the issue is that the ids must be ordered, this should be ok.

private static final int ATTR_PADDING = android.R.attr.padding;
private static final int ATTR_TEXT_COLOR = android.R.attr.textColor;
private static final int ATTR_TEXT_SIZE = android.R.attr.textSize;

private void loadAttributes(Context context, AttributeSet attrs) {
    int[] ids = { ATTR_PADDING, ATTR_TEXT_COLOR, ATTR_TEXT_SIZE};
    Arrays.sort(ids); // just sort the array

    TypedArray a = context.obtainStyledAttributes(attrs, ids);
    try {
        padding = a.getDimensionPixelSize(indexOf(ATTR_PADDING, ids), padding);
        textColor = a.getColor(indexOf(ATTR_TEXT_COLOR, ids), textColor);
        textSize = a.getDimensionPixelSize(indexOf(ATTR_TEXT_SIZE, ids), textSize);
    } finally {
        a.recycle();
    }
}

private int indexOf(int id, int[] ids) {
    for (int i = 0; i < ids.length; i++) {
        if (ids[i] == id) {
            return i;
        }
    }
    throw new RuntimeException("id " + id +  " not in ids"); 
}
like image 45
Plato Avatar answered Nov 15 '22 14:11

Plato


Get it working like this : i defined a new styleable :

<?xml version="1.0" encoding="utf-8"?>
<resources>     
    <declare-styleable name="Label" >
        <attr name="android:textColor" />
        <attr name="android:textSize" />
        <attr name="android:textStyle" />
        <attr name="android:typeface" />
    </declare-styleable>
</resources>

Then here's my styles.xml :

<resources xmlns:android="http://schemas.android.com/apk/res/android">
    <style name="Label" parent="@android:style/TextAppearance.Small">
        <item name="android:textColor">#12345678</item>
        <item name="android:textSize">8dp</item>
        <item name="android:textStyle">bold</item>
        <item name="android:typeface">serif</item>
    </style>    
</resources>

And finally the test :

public class TextAppearanceTest extends AndroidTestCase {

    public void test() {
        TypedArray a = getContext().obtainStyledAttributes(R.style.Label, R.styleable.Label);
        assertTrue(a.getColor(R.styleable.Label_android_textColor, -1) != -1);
        assertTrue(a.getDimensionPixelSize(R.styleable.Label_android_textSize, -1) != -1);
        assertTrue(a.getInt(R.styleable.Label_android_typeface, -1) != -1);
    }
}
like image 41
Antoine Marques Avatar answered Nov 15 '22 14:11

Antoine Marques