Understanding Android Custom Attributes: An Article
Being able to modify behavior of a run time component through configruation is good architecture. Take a look at a text view declaration in an Android layout file
<TextView
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Debut Text Appears here"
/>
In this example the string "android:text" is called an attribute of the class android.widget.TextView. The string "android" in "android:text" defines the XML namespace to separate your own attributes from those as defined in the android SDK. This is an example of how configuration can be used to change the behavior of a component (TextView in this example) at run time.
TextView happens to be a class that comes with the Android SDK. Android SDK has architected this class in such a way that we can change its behavior at tun time. Won't it be nice if I can define my own view class that can allow its own customization based on custom attributes. Here is an example
<com.ai.android.book.apptemplate2.MyTextView
android:id="@+id/custom_text_id"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Debut Text Appears here"
apptemplate:custom_text="Custom Hello"
/>
The attribute "custom_text" is a new attribute that is understood only by MyTextView, the class that I wrote and inherited from TextView. You may ask what is "apptempate" in "apptemplate:custom_text"? This is again a convention in XML to avoid attribute name conflicts. The name space become clear when you see the parent root XML node definition in the full xml layout file. Here is an example of that layout file presenting a few lines at the top and bottom of that file.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:apptemplate="http://schemas.android.com/apk/res/com.ai.android.book.apptemplate2"
....
>
<TextView .... />
<com.ai.android.book.apptemplate2.MyTextView .... />
</LinearLayout>
Notice that the two name spaces "android" and "apptemplate" at the begining of the XML root node LinearLayout. The name spaces have to be uniquie. It is a mere convention that they point to an "http" like resource identifier. It is also a convention that we distinguish the new custom name space using a structure similar to android spec and end it with our own root package name where the custom classes reside. In this example the root java package name is
com.ai.android.book.apptemplate2
Even if there are sub packages under this package, it is sufficient to namespace them with the root package (in the layout file) unless you feel there are too many custom classes and you want to avoid name conflicts. Then you are free to have multiple name spaces for your sub packages. All I am trying to say is that you are free to choose what ever pattern that meet your needs and don't have to follow the EXACT class package structure for the namespace. It may be good, but not a rule!!
MyTextView Implementation
Let's see now what MyTextView implementation may look like to read this custom attribute
public class MyTextView extends TextView
{
public MyTextView(Context context) {
super(context);
setMyText(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
setMyText(context,attrs);
}
public MyTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setMyText(context,attrs);
}
//Implementation of setMyText
private void setMyText(Context context)
{
this.setText("Hello there");
}
private void setMyText(Context context, AttributeSet attrs)
{
this.setText(getFromAttrs(context,attrs));
}
//Function to read the custom attribute
//Stubbed out for now
private String getFromAttrs(Context ctx, AttributeSet attrs)
{
String myText ="Custom Text";
return mytext;
}
}
Notice how this custom class is trying to read from the custom attribtue in the method getFromAttrs(). For now I have stubbed this method.
First problem: Attribute Resource IDs and attrs.xml
So far we have a java class MyTextView. We have a layout file where we indicated a custom attribute called "custom_text".
Now if you place the layout file that has this custom attribute in your project and the compiler will tell you that the following attribute
apptemplate:custom_text="Custom Hello"
is in error. The error says that there is no "resource id" available for 'custom_text'. How do you get a resource id for this? why is it important that I get a resource id?
In android when you declare resources such as strings, images, menus, and layouts in xml files Android stores them away and gives them unique integer constants called resource ids.
So to get a resource id for "custom_text", like other resources such as strings, we have to define this in an XML file. One could argue that "custom_text" is already in the XML file and why not have android generate this id from here.
May be, I speculate, because "custom_text" is an attribute that is going to be used again and again (like an instance). So, perhaps, we need to define it somewhere else first and possibly tell android more about this "custom_text" attribute such as, is it a string or an integer, or a boolean etc.
This definition of a custom attribute happens in an XML file sitting under the
/res/values
sub directory. Typically this file is caleld
attrs.xml
Here is an example
<resources>
<attr name="custom_text" format="string"></attr>
</resources>
The custom attribute definitions don't have to be in attrs.xml. You can put them in strings.xml if you want, as long as this file is under the values sub directory. However, the convention is this file is generally called attrs.xml. Important thing is the XML node "attr".
The custom attribute definition in the attrs.xml will create a resource constant like the following
R.attr.custom_text
Here is an implication of this. Because every native view/class in the android SDK may also have attributes that are defined this way there must be an android.R.attr.* namespace that will tell you all possible attributes defined in the Android SDK!!
It is not wildly useful but if you want to know what is the "sum total" of the attributes defined by all the controls of Android you can look at the API reference for
android.R.attr
The http link for this is at
http://developer.android.com/reference/android/R.attr.html
Turning attention to the "format" property of the custom attribute, in my example above I have defined the custom_text as a string. This is indicated by the "format" attribute. Here are all the possible formats possible for XML defined object attribtues
reference
string
color
dimension
boolean
integer
float
fraction
enum
flag
To know how these format types are used you can see the Android API examples project which comes with the android SDK. Look at the /res/values/attrs.xml file.
Also, if you are able to get access to the source code of Core Android jar you can take a look at the "attrs.xml" in the android core project to see most of the formats used.
Here are a handful of examples of custom attributes taken from these android attrs.xml files
<attr name="text" format="string" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<attr name="textAppearance" format="reference" />
<attr name="textColorPrimary" format="reference|color" />
<attr name="anr">
<enum name="none" value="0" />
<enum name="thumbnail" value="1" />
<enum name="drop" value="2" />
</attr>
<attr name="windowSoftInputMode">
<flag name="stateUnspecified" value="0" />
<flag name="stateUnchanged" value="1" />
</attr>
<!-- absolute dimension or fraction of the screen size -->
<attr name="windowMinWidthMajor" format="dimension|fraction" />
Most attribute formats listed above are easy to interpret. Not so obvious in this listing is where formats can be combined as in "dimension|fraction" indicating that the value for the attribute can be either an absolute dimension or a percentage fraction.
Exploring the AttributeSet: Can I read my attribute now??
So far you have done the following
1. Define a custom class
2. Define a custom attribute for your class/application in attrs.xml
3. Provide a namespace for your attribute
4. Provide a value for your custom attribute in the file
With all the 4 in place, you ask, should I not be able to read that custom attribute?
Well, let's give it a try. Let's locate the method where we are expecting to read the custom attribute
//Function to read the custom attribute
private String getFromAttrs(Context ctx, AttributeSet attrs)
{
String myText ="Custom Text";
return mytext;
}
In this method, we got in our hand the object "AttributeSet". Let's see what kind of methods this object offers by looking at its API docs
http://developer.android.com/reference/android/util/AttributeSet.html
As per the documentation, an AttributeSet object corresponds to the set of attributes that are specified at that specific XML view object tag. For example in the following attribute specification for MyTextView
<com.ai.android.book.apptemplate2.MyTextView
android:id="@+id/custom_text_id"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Debut Text Appears here"
apptemplate:custom_text="Custom Hello"
/>
The passed in AttributeSet object is a set of attributes that is a collection of id, layout_width, layout_height, text, and custom_text and NO MORE.
The AttributeSet class has methods like
getAttributeCount();
getAttributeValue(String namespace, String name);
getAttributeName(int index);
getAttributeValue(int index);
For our example the first method, getAttributeCount() returns 5. Then using the index we can get the attribute value for our index for which our custom attribute name matches. I have also wondered what goes into the "namespace" argument of the method getAttributeValue() from the following namespace spec
xmlns:apptemplate="http://schemas.android.com/apk/res/com.ai.android.book.apptemplate2"
I have tried "apptemplate" but that doesn't work, what works is the following code
String namespace=
"http://schemas.android.com/apk/res/com.ai.android.book.apptemplate2";
String mytext =
attrs.getAttributeValue(namespace, "custom_text");
However there are drawbacks to reading custom attributes this way. First it is not recommended. And you will know the reasons by the end of this article.
To start with if value of the custom attribute has a reference to another resource such as "@string/mystring" as opposed to "mystring stuff", then we need to untangle that referecne ourselves. The recommended approach is to use a structure called "TypedArray". However we need another detour to understand "TypedArray" and the APIs available to get a TypedArray and work with it.
An arbitrary grouping: The Styleable Wrinkle
According to the recommendation we need to use the following appoach
//Make an array of integers
//where integers are the resource ids of the attributes
int attrsResourceIdArray[] = {R.attr.custom_text};
//At what offset is the custom attribute's resource id
int custom_text_offset = 0;
//Pass the array of attribute resource ids and the
//attribute set to get an ARRAY of values
TypedArray t = context.obtainStyledAttributes(attributeSet,attrsResourceIdArray);
//Use the offset to get the fully typed value
mytext = t.getString(custom_text_offset);
Notice how the method obtainStyledAttributes() takes an array of integers to return a typed array. The object TypedArray will have methods to extract the right values. The API docs can be found at
http://developer.android.com/reference/android/content/res/TypedArray.html
As you see we have manually constructed an integer array (attribute resource ids) to get values from the AttributeSet. This can get tedious if you have a number of custom attributes. Android ADK provides a quicker way to do this by packaging a number of attributes together in the attrs.xml itself through an XML tag called styleable.
Say we are interested in reading the following custom attributes
a1
a2
custom_text
Then we can define these as follows in our res/values/attrs.xml
<resources>
<attr name="a1" format="integer"></attr>
<declare-styleable name="SomeArbitraryGroupName">
<attr name="a1"/>
<attr name="a2" format="string" />
<attr name="custom_text" format="string"/>
</declare-styleable>
</resources>
Notice how we have grouped the definitions for a1, a2, and custom_text. This will create an integer array constant as follows
public static final class R {
...
public static final class styleable {
...
public static final int[] SomeArbitraryGroupName = {
0x7f010000, 0x7f010001, 0x7f010002
};
...
};//end of class styleable
...
}//end of class R
Usually every resource in Android generates a single integer constant in the yourpackage.R namespace. Here is an exampel where Android actually generates an entire ARRAY of resource ids.
So this approach gives us the constant
R.styleable.SomeArbitraryGroupName
allowing us to rewrite
TypedArray t = context.obtainStyledAttributes(attributeSet,attrsResourceIdArray);
as
TypedArray t = context.obtainStyledAttributes(attrs
,R.styleable.SomeArbitraryGroupName);
So this saves us from constructing our own attribute resource Id arrays.
The styleable tag is merely an aggregation of attributes. This means the attribute names will conflict if they are inside or outside of the styleable grouping. This means you cannot take an attribute like "a1" and define it once inside a styleable and once outside a styleable. That would be considered duplicate. So the resource ids generated for attributes are independent of what styleable underwhich the attributes are defined. The styleable merely gathers the attribute resource ids into an integer array that can be readily referenced in your java code.
So again, the attributes can be defined either outside or inside the declare-styleable tag. The declare-styleable is merely a convenient grouping of attributes.
You can choose to take an attribute and include it in many declare-styleable groupings. In such a case just don't specify the "format" tag of the attribute. Such an attribute then is considered reused. In our example the attribute "a1" is defined outside first and then for convenience included in our grouping the second time, but omitting the foramt.
Well there is a bit more to styleable. The compiler also generates a number of constants for the attribute offsets such as (for our example)
R.styleable.SomeArbitraryGroupName_a1 (value 0)
R.styleable.SomeArbitraryGroupName_a2 (value 1)
R.styleable.SomeArbitraryGroupName_custom_text (value 2)
Using these constants we can now rewrite the following code int attrsResourceIdArray[] = {R.attr.custom_text}; int custom_text_offset = 0; TypedArray t = context.obtainStyledAttributes(attrs,attrsResourceIdArray); mytext = t.getString(custom_text_offset);
as
//You don't need these any more. Generated by R
//int attrsResourceIdArray[] = {R.attr.custom_text};
//int custom_text_offset = 0;
TypedArray t = context.obtainStyledAttributes(attrs,R.styleable.SomeArbitraryGroupName);
mytext = t.getString(R.styleable.SomeArbitraryGroupName_custom_text);
//you have to recycle the typed array
t.recycle();
You should be able to answer the following questions about custom attributes?
- What are custom attributes?
- Why use custom attributes?
- How do you code a custom class?
- How do you specify a custom attribute?
- Do you have to define a custom attribute to Android?
- Where do you have to define a custom attribute to Android?
- What is AttributeSet?
- What is TypedArray?
- How do you read a custom attribute in your java code?
- What is the styleable tag?
- What is attrs.xml?
- Does the name of a styleable tag need to match your java custom view name?
- How do you declare a namespace for your custom attribute?
- Do you need to declare your namespace differently if you have a sub package?
- Do you need to specify package/sub-package name for your custom view class in a styleable?
- Does an attribute name has to be unique in your entire package?
- Can a styleable grouping avoid attribute name uniqueness constraint?
References
AttributeSet API You rarely use this API directly. But it is there if you are curious as this is the object that gets passed into class initialization.
TypedArray API This is the object you read your typed custom attributes from.
R.attr If you want to see the total universe Android SDK attributes.
R.styleable If you want to see the grouping of attributes as seen by various views in Android SDK.
R.style To contrast the difference between attributes, styles, and styleables.
Resources.Theme API to read/work with the current theme that is in play. A theme is a named style that is applied to a whole application.
Custom Components Presentation by Chiu-Ki Chan Custom components use custom attributes. This is a good presentation into the basics of creating custom views.
Resource related references My research notes on Android resources in general, of which "attr" is just another value resource.
How to get the Android Core Source code If you want to get access to the root attrs.xml.
Understand Styles and Themes My research on styles and themes. This is a good read once you understand what custom attributes are.
My Research notes on Custom attributes. This entire article is based on the research I have done on this page.
A number of Android core SDK files: attrs.xml, styles.xml etc. If you can't get the source code you can use this link to look at these files. I have copied them to my site to support this article
Custom Attribute Predicates
- Objects have attributes (ex: TextView)
- You can specify attributes (values) declaratively for an object in xml layout files (ex: layouts)
- Attributes need to be declared first through "attr" tag in the /res/values sub directory.
- Attributes are usually declared in /res/values/attrs.xml
- An attribute definition has a name and a format (like boolean, string etc)
- Attribute formats can be combined (Ex: reference|color)
- One uses TypedArray object to read custom attributes
- An XML tag called "styleable" enables TypedArray usage
- Styleable tags are also found in attrs.xml
- Styleables are mere groupings of, otherwise independent, attributes
- To repeat an attribute in a styleable group, avoid the format tag and use the same name
- The value of an attribute can use a "?" to de-reference another value's attribute (ex: attrib1="?attrib2" Use the value of attrib2 as the value of attrib1)
- Styleables don't play a big role in XML layout definitions
- Styleables are useful in Java code and used by objects to read a set of attributes that those objects care about
- Context.obtainStyledAttribues() is used in conjunction with TypedArray and styleables
- Attributes are represented by R.attr.*
- Styles and themes are represented by R.style.*
- Styleable grouping are represented by R.styleable.* and R.styleable.group_attribute1, R.styleable.group_attribute2 etc.