Introduction
In Android, we have many way of "grouping" layouts together to make them reusable. There is an <include>
Tag (and the lesser known <merge>
) in our XML designs which simply includes another layout into the current one, there are Fragments
, and, of course, we can derive at any time from any of the base classes, like View
or LinearLayout
.
This article will dive a bit deeper into the latter of these:
- How can I write a control that inflates its own layout?
- How can I create custom attributes for my control?
- How is that all mixed together with styles and themes?
The scope of this article are controls that inflate their own layout, so we will derive from a matching base class, either a LinearLayout
or a RelativeLayout
, depending on how our custom layout is built.
I will use one of my own controls as an example here, to show you how it is done. This control is a simple toolbar with four buttons that support some standard functions for my software label, like sending me an email, opening my G+ page, opening my developer page on Google Play and to Rate the currently running app.
It is very simple and therefore a good candidate to be analyzed in an article.
At runtime, my control looks like this:
This is taken from a screenshot of one of my apps which uses a dark theme.
The declaration in the XML design:
<mbar.ui.controls.MbarBar
android:id="@+id/contact_mbar_button_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/contact_mbar_credit_text"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/mbar_default_control_distance"
mbar:barSize="small"/>
There would be different possible approaches to achieve this:
- Just
<include>
the pre-drawn layout from a library and assign the button listeners in the code - Draw it by hand in every app (just kidding... do not even think about that! :))
- Create a control and do it in the way shown above
Of course, we will take the third approach in this article. What you can see in this XML snippet is that the control uses at least one custom attribute: barSize
. It's an enum
type property, knowing the values "small
" (0) and "large
" (1). We will create this shortly, together with the second custom attribute showTitle
, a simple boolean value.
We assume for this article that your library/project is set up with minAPI 17.
Step 1: Create your control class
The best thing to start with: Create your class and decide what will be your base class. In our case, it is a simple LinearLayout
.
What you need to know
There are several constructors available, not all of them need to be supplied, but I always try to cover a base class as completely as possible.
So, when you start your class that extends LinearLayout
, there are 4 constructors available:
public class MbarBar extends LinearLayout {
private @MbarBarSize int barSize = MbarBarSize.SMALL;
private boolean showTitle = true;
public MbarBar(Context context) {
super(context);
init(null, 0, 0);
}
public MbarBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs, 0, 0);
}
public MbarBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr, 0);
}
@TargetApi(21)
public MbarBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs, defStyleAttr, defStyleRes);
}
You can see, I redirect them all to a single init(...)
method. We will cover that a bit later, let's concentrate on the constructors for now.
There is a @TargetApi(21)
annotation around the 4th constructor because this one is only available at 21+.
What are the values supplied to the constructor by the system?
First rule: Pass them to the super-class unless you have a very good reason, not to do so!
Second: For us devs, the second parameter, the AttributeSet
is of greatest interest, because this one contains the specified attributes from XML, including our custom attributes barSize
and showTitle
. So this already uncovers the first mystery: How do we get the values from XML to our control? The answer is: through the AttributeSet
. How we get our values out of it is covered when we discuss the init(...)
method.
Map your xml-enum values to code values
I do like those @interfaces
very much, so I created one for the barSize
. In case you do not know what that is: It is, more or less, an alternative way to group integers or strings together. Android SDK does that at many points, and you have come across them. Just as a simple example: whenever you set something to View.VISIBLE
or View.GONE
, you are touching one of them.
At the top of the class, you see the declaration:
private @MbarBarSize int barSize = MbarBarSize.SMALL;
which correlates to this line in the XML design:
mbar:barSize="small"
When we discuss the declaration of this custom attribute a bit later, you will see that the terms "small
" and "large
" represent the values "0
" and "1
". And now, we want of course, to treat those values in Java with the same names (SMALL and LARGE
) and we do not want to work with 0
and 1
.
So what is this @MbarBarSize
and what does it do? The @annotation
tells that this int
variable can hold only values defined in @MbarBarSize
. You get a lint warning if you try to assign something else.
To declare such restrictions for members, an @interface
declaration comes into play. The MbarBarSize
is defined as:
@Retention(RetentionPolicy.CLASS)
public @interface MbarBarSize {
int SMALL = 0;
int LARGE = 1;
}
With such a declaration, you can assign something I call a value-restriction
or value-constraint
on a data type that would normally allow other values too.
RetentionPolicy
It is important that you understand when to use which Policy. There are three policies available:
- CLASS: (the default). This policy can be used everywhere, but I mostly use it in libraries.
Class
means, that this @interface
will survive the compile and is still available to users of your library. They can use the values SMALL
and LARGE
as if they had them defined in their own app/lib. This is, what you want, if you need it as method parameters and when you want, that lint can warn the users of your lib, if they assign a not-allowed value. In a library, you want that your values are used with their given name. You don't want to force the users of your library to supply "0
" and "1
" as parameter values to your methods. They shall use SMALL
and LARGE
, too! - SOURCE: This policy tells the compiler to discard the definition. For human thinking, this means: you can use the SOURCE policy when you define an
@interface
that does not need to survive the compile process. As an example: in your App, when nothing outside of your App needs to access the values with their given name (SMALL
and LARGE
in the above example). Outside of your current project, SMALL
and LARGE
are unknown. Users have to supply "0
" and "1
" as parameter values. This is not what you want for your public
interfaces and methods, but you can use it for private
things in your library. - RUNTIME: This is the widest policy of all. Not only does it survive the compile process and is available to your users, it is even available when the program already runs and it can be accessed via reflection! Beside that, it behaves like CLASS.
Step 2: Defining a custom attribute
Ok, so we have seen, how we map the attribute value to our Java code, but how is that attribute defined?
You create custom attributes by adding a file called attrs.xml to your values folder of the project. Right-click the values
node in the project explorer and select New -> Values resource file. Name the file attrs.xml.
In this file, you can create custom attributes. The syntax is not surprising and easy to understand. I show you here the complete attribute set of the MbarBar
control:
<declare-styleable name="MbarBar">
<attr name="barSize" format="enum">
<enum name="small" value="0"/>
<enum name="large" value="1"/>
</attr>
<attr name="showTitle" format="boolean"/>
</declare-styleable>
You declare a styleable
resource here, which will make it available to the XML designer.
There are several formats available, it would go too far out of the scope of this article to cover them all here, but the enum
format is one of the more interesting ones anyway, and we will look at this one:
- Most important:
<declare-styleable name="class_name_of_your_control">
You may not freely choose the "name
" attribute here! This is already the connection to your control class (and the reason why we created the class in a first step and the attributes afterwards)! Our control is named MbarBar
and this here is this exact control/class name. With this single line, everything you declare inside this styleable gets attached to the MbarBar
class.
Then, two custom attributes are declared:
- defined as
format="enum"
, we can then add a list of as many <enum value
entries we like. We give them a name and a value. You see, the "0
" and "1
" here correspond to the 0
and 1
of the integer representation of our @interface
. The user of your control can assign the values as "small
" and "large
" in the XML layout. - defined as
format="boolean"
we add a simple switch to show/hide the title text "More mbar Software
" to get a bit more customization into the control.
Step 3: Putting the pieces together: The init(...) method
So, now you have your custom attribute defined, you have seen how it looks in XML layout, let's put it together and take a look at the AttributeSet
to get the values out that have been entered by the developer in the XML.
Method first, explanation afterwards:
private void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
if (attrs != null) {
TypedArray array = getContext().getTheme().obtainStyledAttributes
(attrs, R.styleable.MbarBar, defStyleAttr, defStyleRes);
barSize = array.getInt(R.styleable.MbarBar_barSize, 0);
showTitle = array.getBoolean(R.styleable.MbarBar_showTitle, true);
}
switch (barSize) {
case MbarBarSize.SMALL:
View smallMe = inflate(getContext(), R.layout.mbar_button_frame_small, this);
break;
case MbarBarSize.LARGE:
View largeMe = inflate(getContext(), R.layout.mbar_button_frame, this);
break;
}
setTitleBarVisibility();
connectListeners();
}
One of the constructors (the first one) will send null
as the attrs value to this method, so we need a null
-check. In this case, the control will work with all values at default: barSize=SMALL
and showTitle=true
(see first code block in this article - the constructors, this is how the class members are set up).
The most important part here is obtaining the attributes from XML. This is done with the method obtainStyledAttributes
, which is tied to the current theme of our context
. As we derive from LinearLayout
, we have a getContext()
method available, so we do not need a parameter or other way to get a valid context. We already have one.
The parameters are:
attrs
. This is the AttributeSet
where we want to get the values. It's the one that has been supplied in the constructor. R.styleable.MbarBar
. I am sure, by now you know what that is. The custom attribute(s) we defined in attrs.xml. We want to get exactly these values. defStyleAttr
and Res
. Everything is themed and styled. Android will apply any modifications of the current theme and style and take them into account for the values we will get.
After this call, we can access our attributes in the same easy way as we would access the Extras
of a Bundle
. With getInt
, getBool
, getWhatYouNeed
. Very easy interface. The second parameter in these calls is the default-if-not-found.
Followed by this, there is a switch
statement, inflating one of two predefined layouts (the small and the large button frame). These layouts are nothing special, the are designed as any other layout too. Just a bunch of images and text views. Standard. You can inflate it like any other layout you have inflated.
Then some other supporting methods, like hiding the title bar and connecting the click listeners are called, but they are not the scope of this article. We wanted to create a custom control with custom attributes :).
Cool thing: This is already visible at design time! If you have your Preview window open, you see the design inflated while working on your layout!
If you change any of the custom attributes in the XML designer (like set "showTitle
" to "false
"), it is immediately reflected in the layout, as you would expect.
What we have created
- We defined a new control class derived from
LinearLayout
- We created two custom attributes in a styleable resource
- We created an
@interface
to reflect custom attribute's enum
values in code with the same name - We have accessed the custom attributes in Java code through the
AttributeSet
- We have inflated a custom layout in our control
One last thing
Don't get confused, when you use your custom control for the first time and you get no intelliSense in XML when typing the name of your custom attribute!
You need to know that you will not find your attribute in the android:
namespace and also not in the app:
namespace. You will prefix this with any namespace name you like (in case of this control, as you can see in the XML on top, I used mbar:
).
When you start to type a new namespace name, Android Studio offers you to insert...
xmlns:mbar="http://schemas.android.com/apk/res-auto"
...with Alt+Enter. Accept that (= press Alt+Enter). Then your custom attributes are available with this prefix.
So... here we are!
I hope this article could help you with your first steps to custom controls and de-mystify that part a bit.
Comments are welcome as always, I will do my best to answer any questions!
History
- 2017-11-12 - Initial publication
- 2017-11-20 - Typos