Android UIs with Custom Views

In this article by Raimon Rafols Montane, the author of the book Building Android UIs with Custom Views, we will see the very basics steps we'll need to get ourselves started building Android custom views, where we should use them and where we should simply rely on the Android standard widgets.

(For more resources related to this topic, see here.)

Why do we need custom views:

There are lovely Android applications on Google Play and in other markets such as Amazon, built only using the standard Android UI widgets and layouts. There are also many other applications which have that small additional feature that makes our interaction with them easier or simply more pleasing. There is no magic formula, but maybe by just adding something different, something that the user feels like "hey it's not just another app for" might increase our user retention. It might not be the deal breaker but can definitely make the difference sometimes.

Few years ago, author was working on the pre-sales department of a mobile app development agency and when the app Path was launched to the market, we got many and many requests from our customers to build menus like the Path app menu. It wasn't a critical feature for the applications they were building, but at that point it was a cool thing and many other applications and products wanted to have something similar. Even if it was a simple detail, it made the Path application special at that time.

It wasn't the case at that time, but nowadays there are many implementations of that menu published in GitHub as open source, although they're not really maintained anymore.

https://github.com/daCapricorn/ArcMenu

https://github.com/oguzbilgener/CircularFloatingActionMenu.

One of the main reasons to create our own custom views to our mobile application is, precisely, to have something special. It might be a menu, a component, a screen, something that might be really needed or even the main functionality for our application or just as an additional feature.

In addition, by creating our custom view we can actually optimize the performance of our application. We can create a specific way of layouting widgets that otherwise will need many hierarchy layers by just using standard Android layouts or a custom view that simplifies rendering or user interaction.

On the other hand, we can easily fall in the error of trying to build everything custom. Android provides an awesome list of widget and layout components that manages a lot of things for ourselves. If we ignore the basic Android framework and try to build everything by ourselves it would be a lot of work, potentially struggling with a lot of issues and errors that the Android OS developers already faced or, at least, very similar ones and, to put it up in one sentence, we would be reinventing the wheel.

Examples in the market

We all probably use great apps that are built only using the standard Android UI widgets and layouts, but there are many others that have some custom views that we don't know or we haven't really noticed. The custom views or layouts can sometimes be very subtle and hard to spot.

We'd not be the first ones to have a custom view or layout in our application. In fact, many popular apps have some custom elements on them.

For example, the Etsy application had a custom layout called StaggeredGridView. It was even published as open source in GitHub. It has been deprecated since 2015 in favor of Google's own StaggeredGridLayoutManager used together with RecyclerView.

More information refer to:

https://github.com/etsy/AndroidStaggeredGrid

https://developer.android.com/reference/android/support/v7/widget/StaggeredGridLayoutManager.html.

Parameterizing our custom view

We have our custom view that adapts to multiple sizes now, that's good, but, what happens if we need another custom view that paints the background blue instead of red? And yellow? we shouldn't have to copy the custom view class for each customization. Luckily, we can set parameters on the XML layout and read them from our custom view.

First, we need to define the type of parameters we will use on our custom view. We've to create a file called attrs.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="OwnCustomView">
        <attr name="fillColor" format="color"/>
    </declare-styleable>
</resources>

Then, we have to add a new different namespace on our layout file where we want to use following new parameter:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="@dimen/activity_vertical_margin">

        <com.packt.rrafols.customview.OwnCustomView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:fillColor="@android:color/holo_blue_dark"/>

    </LinearLayout>
</ScrollView>

Now that we have this defined, let's see how we can read it from our custom view class.

int fillColor;

TypedArray ta = context.getTheme().obtainStyledAttributes(attributeSet, R.styleable.OwnCustomView, 0, 0);
try {
    fillColor = ta.getColor(R.styleable.OwnCustomView_fillColor, DEFAULT_FILL_COLOR);
} finally {
    ta.recycle();
}

By getting a TypedArray using the styled attribute ID Android tools created for us after saving the attrs.xml file, we'll be able to query for the value of those parameters set on the XML layout file.

More information about how to obtain a TypedArray can be found on the Android documentation:

https://developer.android.com/reference/android/content/res/Resources.Theme.html#obtainStyledAttributes(android.util.AttributeSet, int[], int, int).

For more information about TypedArray refer to:

https://developer.android.com/reference/android/content/res/TypedArray.html.

In this example, we created an attribute named fillColor which will be a formatted as a color. This format, or basically, the type of the attribute is very important to limit the kind of values we can set, and how these values can be retrieved afterwards from our custom view.

Also, for each parameter we define, we'll get a R.styleable.<name>_<parameter_name> index in the TypedArray. In the code above, we're querying for the fillColor using the R.styleable.OwnCustomView_fillColor index.

We shouldn't forget to recycle the TypedArray after using it so it can be reused, but once recycled, we can't use it again.

Many custom views will only need to draw something in a special way, that's the reason we created them as custom views, but many others will need to react to user events. For example, how our custom view will behave when the user clicks or drags on top of it?

Basic event handling

Let's start by adding some basic event handling to our custom views.

Reacting to touches

One of the first things we'd to implement is to react to touch events. Android provides us with the method onTouchEvent() that we can override in our custom view. Overriding this method, we'll get any touch event happening on top of it. To see how it works, let's add it to the custom view:

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.d(TAG, "touch: " + event);
    return super.onTouchEvent(event);
}

Let's also add a log call to see what events do we receive. If we run this code and we touch on top of our view, we'll get:

D/com.packt.rrafols.customview.CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=644.3645, y[0]=596.55804, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=30656461, downTime=30656461, deviceId=9, source=0x1002 }

As we can see there is a lot of information on the event, the coordinates, the action type, the time, but even if we perform more actions on it, we'll only get ACTION_DOWN events. That's because the default implementation of view is not clickable. If we don't set the clickable flag on the view, onTouchEvent() will return false and ignore further events.

Method onTouchEvent() has to return true if the event has been processed or false if it hasn't. If we receive an event in our custom view and we don't know what to do it or we're not interested in that kind of events, we should return false so it can be processed by our view's parent or by any other component or the system.

To receive more type of events, we can do two things:

  • Set the view as clickable by using setClickable(true)
  • Implement our own logic and process the events ourselves in our custom class.

As, we'll implement more complex events, we'll go for the second option.

Let's do a quick test, change the method to return simply true instead of calling the parent method:

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.d(TAG, "touch: " + event);
    return true;
}

Now we should receive many other types of events as follows:

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_UP,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_MOVE,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_MOVE,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_MOVE,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_UP,

...CircularActivityIndicator: touch: MotionEvent { action=ACTION_DOWN,

Using the Paint class

We've been drawing some primitives until now, but Canvas provides us with many more primitive rendering methods. We'll briefly cover some of them, but before let's first talk about the Paint class as we haven't introduced it properly.

According to the official definition the Paint class hold the style and color information about how to draw primitives, text and bitmaps. If we check the examples we've been building, we created a Paint object on our class constructor, or on the onCreate method, and we used it to draw primitives later on our onDraw method. As, for instance, we set our background Paint instance Style to Paint.Style.FILL, it'll fill the primitive, but we can change it to Paint.Style.STROKE if we only want to draw the border or the strokes of the silhouette. We can draw both using Paint.Style.FILL_AND_STROKE.

To see the Paint.Style.STROKE in action, we'll draw a black border on top of our selected colored bar in our custom view. Let's start by defining a new Paint object called indicatorBorderPaint and initialize it on our class constructor:

indicatorBorderPaint = new Paint();
indicatorBorderPaint.setAntiAlias(false);
indicatorBorderPaint.setColor(BLACK_COLOR);
indicatorBorderPaint.setStyle(Paint.Style.STROKE);
indicatorBorderPaint.setStrokeWidth(BORDER_SIZE);
indicatorBorderPaint.setStrokeCap(Paint.Cap.BUTT);

We also defined a constant with the size of the border line and set the stroke width to this size. If we set the width to 0, Android guaranties it'll use a single pixel to draw the line. As we want to draw a thick black border, this is not our case right now. In addition, we set the stroke cap to Paint.Cap.BUTT to avoid the stroke overflowing its path. There are two more Caps we can use, Paint.Cap.SQUARE and Paint.Cap.ROUND. These last two will end the stroke respectively with a circle, rounding the stroke, or a square.

Let's see the differences between the three Caps and also introduce the drawLine primitive.

First of all, we create an array with all three Cap, so we can easily iterate between them and create a more compact code:

private Paint.Cap[] caps = new Paint.Cap[] {
        Paint.Cap.BUTT,
        Paint.Cap.ROUND,
        Paint.Cap.SQUARE
};

Now, on our onDraw method, let's draw a line using each of the Cap using the drawLine(float startX, float startY, float stopX, float stopY, Paint paint) method.

int xPos = (getWidth() - 100) / 2;
int yPos = getHeight() / 2 - BORDER_SIZE * CAPS.length / 2;
for(int i = 0; i < CAPS.length; i++) {
    indicatorBorderPaint.setStrokeCap(CAPS[i]);
    canvas.drawLine(xPos, yPos, xPos + 100, yPos, indicatorBorderPaint);
    yPos += BORDER_SIZE * 2;
}
indicatorBorderPaint.setStrokeCap(Paint.Cap.BUTT);

Summary

In this article we have seen the reasoning behind why we might want to build custom views and layouts. Android provides a great basic framework for creating UIs and not using it would be a mistake. Not every component, button or widget has to be developed completely custom but, by doing it so in the right spot, we can add an extra feature that might make our application remembered. We have also seen basics of event handling.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Building Android UIs with Custom Views

Explore Title