Click here to Skip to main content
13,708,005 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

7.1K views
8 bookmarked
Posted 12 Nov 2017
Licenced CPOL

Create Your Own Controls for Android

, 19 Nov 2017
Rate this:
Please Sign up or sign in to vote.
Create custom controls with custom attributes for Android in Java and XML.

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;
   
   // <editor-fold desc="Constructors">
   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);
   }
   // </editor-fold>

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mike Barthold
Software Developer (Senior)
Austria Austria
Software Developer since the late 80's, grew up in the good old DOS-Era, switched to windows with Win95 and now doing .net since early 2002 (beta).
Long year c# experience in entertainment software, game programming, directX and XNA as well as SQLServer (DBA, Modelling, Optimizing, Replication, etc) and Oracle Databases in Enterprise environments. Started with Android development in 2014.

My Android Label (mbar Software) on G+: Take a look and follow me!
My Android Apps in Play Store: Take a look!

You may also be interested in...

Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05-2016 | 2.8.180920.1 | Last Updated 20 Nov 2017
Article Copyright 2017 by Mike Barthold
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid