Android Custom View Tutorial (Part 1) – Combining Existing Views
This post is part of a series written by our developers that documents their journey in a cross-platform mobile app development project. They set out to improve an internal process, create educational content for the developer community, and explore new craft beers and a vintage arcade. Sound interesting? Read more about this series and their efforts in our introductory series post.
Introduction
The Android framework has a large variety of View classes that cover many of the needs of a typical Android application. These views, along with the multitude of options for using drawable resources, make it possible to create complex user interfaces for Android applications. Nevertheless, as with just about any framework, sometimes existing classes do not fit your needs and you need to create your own.
When we set out to create a mobile solution for our time entry system, we decided that we wanted to create something that was not merely convenient and functional, but also pleasant and enjoyable to use. More so than any other platform that I have developed for, mobile applications are judged based on UI and UX (User Interface and User Experience). Have you ever deleted a mobile app simply because it was too confusing or just plain ugly? You would not be alone if you answered yes.
Learning how to create Android custom views is a valuable skill for creating great Android applications. With this tool under your belt, you have nearly unlimited flexibility in the UI/UX design of your app, helping you create apps that users will enjoy. As you will see in this tutorial, there is definitely a learning curve, but I think you will find it worth the effort.
Custom Views in Android
There are generally two approaches to creating your own view: extend an existing view or create one from scratch (well, not totally from scratch since you will still be extending the View class). This article focuses on extending an existing view. More specifically, we will be extending an existing layout and combining existing views to form a new Android custom view.
This post is part of an Android Custom View Tutorial series for Creating Custom Views in Android. All of the code for this tutorial is available in the example application, which demonstrates the custom views.
This Android Custom View Tutorial series assumes you are familiar with working with views in Android. It does not cover topics such defining views in xml, referencing values in resource files, etc.
Combining Existing Views
Why would you want you combine existing views? What is the advantage over simply placing those views individually in the layout? There are a couple of reasons. First, if you have several views that are needed to represent a single model object, it is convenient to have a custom view that handles populating those individual child views. That way you can simply pass the model object to the custom view, instead of having to populate each of those views individually every time. Second, if you have a collection of views that need to work together (e.g. actions performed on one view affect others), a custom view makes a nice container for the logic that ties them together. Additionally, if these UI portions are reused throughout your application, then an Android custom view is even more practical.
To illustrate, we are going to create the following view:
This is a simple layout: an ImageView for both the plus and minus button, and two TextViews: one for the value, and one for the label beneath it. The value can be updated by clicking one of buttons, while pressing and holding one of the buttons will update the value repeatedly.
Creating the Android Custom View Class
To start, create a class that extends RelativeLayout and add the desired constructors. If you look at the documentation for the constructors of the View class you will see multiple options. The first constructor (the one that takes a Context as the only parameter) is used to create an instance of the view programatically. The second constructor (with parameters Context and AttributeSet) is used to inflate the view from xml. The remaining constructors provide ways for subclasses of the view to specify base styles, a topic that will not be covered here. Since there are multiple constructors, it is common to have a separate method that does most of the work to initialize the View, and that is what we will do here by defining an init method. Note however, that in our example the second constructor is the only one that we will be using.
The new class also contains references to all of the child views in the layout. Since those views will be interacting with each other, it is convenient to store those as instance variables.
public class ValueSelector extends RelativeLayout { View rootView; TextView valueTextView; View minusButton; View plusButton; public ValueSelector(Context context) { super(context); init(context); } public ValueSelector(Context context, AttributeSet attrs) { super(context, attrs); init(context); } private void init(Context context) { //do setup work here }
The init method will inflate the layout, get references to all of the child views, and setup the buttons:
private void init(Context context) { rootView = inflate(context, R.layout.value_selector, this); valueTextView = (TextView) rootView.findViewById(R.id.valueTextView); minusButton = rootView.findViewById(R.id.minusButton); plusButton = rootView.findViewById(R.id.plusButton); minusButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { decrementValue(); //we'll define this method later } }); plusButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { incrementValue(); //we'll define this method later } }); }
Calling the inflate method and passing this as the root ViewGroup (the final parameter) is all we need to do to establish the layout for our View. We save a reference to the returned view for convenience. But, before that will work we need to create the layout resource that we specified when calling inflate:
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android"> <EditText android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/valueSelector_margin_top" android:textSize="@dimen/valueSelector_text" android:text="0" android:enabled="false" android:background="@null" android:textColor="#000000" android:layout_centerHorizontal="true" android:id="@+id/valueTextView" /> <TextView android:id="@+id/valueLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="@dimen/valueSelector_label_text" android:text="@string/valueSelector_label" android:layout_below="@+id/valueTextView" android:layout_centerHorizontal="true" /> <ImageView android:src="@drawable/valueselect_minus_state" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/valueSelector_button_hMargin" android:layout_marginBottom="@dimen/valueSelector_button_hMargin" android:layout_marginRight="@dimen/valueSelector_button_insideMargin" android:id="@+id/minusButton" android:clickable="true" android:layout_centerVertical="true" android:layout_alignRight="@+id/valueLabel" /> <ImageView android:src="@drawable/valueselect_plus_state" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/valueSelector_button_hMargin" android:layout_marginBottom="@dimen/valueSelector_button_hMargin" android:layout_marginLeft="@dimen/valueSelector_button_insideMargin" android:id="@+id/plusButton" android:clickable="true" android:layout_centerVertical="true" android:layout_alignLeft="@+id/valueLabel" /> </merge>
You’ll notice that the layout references several resources. You can find the resource files in the example project if you are interested in what those values are.
You also might notice that the root node in the xml is merge. What does that do? Since ValueSelector extends RelativeLayout, if we inflated a RelativeLayout, we would end up with a view that contains a RelativeLayout as its only child. Here is what that looks like in Hierarchy Viewer:
By using merge, we can avoid that unnecessary RelativeLayout:
To make our View even remotely useful, there will need to be a way to access the value, as well as set the value programatically. It also might be useful to limit the value that can be selected. So, let’s update our code to accomplish all of this.
private int minValue = Integer.MIN_VALUE; private int maxValue = Integer.MAX_VALUE; public int getMinValue() { return minValue; } public void setMinValue(int minValue) { this.minValue = minValue; } public int getMaxValue() { return maxValue; } public void setMaxValue(int maxValue) { this.maxValue = maxValue; } public int getValue() { return Integer.valueOf(valueTextView.getText().toString()); } public void setValue(int newValue) { int value = newValue; if(newValue < minValue) { value = minValue; } else if (newValue > maxValue) { value = maxValue; } valueTextView.setText(String.valueOf(value)); }
Finally, we need to add the methods for incrementing and decrementing the value, which are called by the click listeners that we set on the buttons. Here is what the increment methods looks like:
private void incrementValue() { int currentVal = Integer.valueOf(valueTextView.getText().toString()); if(currentVal < maxValue) { valueTextView.setText(String.valueOf(currentVal + 1)); } }
We now have a functioning view which we can use in our layouts. The user can update the value using the plus and minus buttons, and we can retrieve the current value when we need to. Here is how we can add the view to a layout. Notice that for Android custom views, the full name is required:
<com.intertech.customviews.ValueSelector android:id="@+id/valueSelector" android:layout_width="match_parent" android:layout_height="@dimen/valueSelector_height"> </com.intertech.customviews.ValueSelector>
Adding Press and Hold
Our view is usable at this point, but there is one thing that may frustrate users: if the value needs to be updated by a large amount, that would require many button clicks. Let’s fix that by allowing the value to be updated repeatedly when a button is held down.
Android has events for click and long click, but the events will only fire once, regardless of how long the button is pressed. We can use the long click event to determine when to begin incrementing/decrementing the value, but repeatedly updating the value will take some work on our part. There are a couple of ways we could accomplish this. We could use a ScheduledThreadPoolExecutor to schedule a Runnable to be run at a regular interval, and then cancel it when the button is released. Since we need to update the UI, our Runnable would need to communicate with the UI thread, which is accomplished by using a Handler.
A more common approach in Android is to use a Handler to do the UI update, and then check if the action should be repeated. If yes, then post another task to the Handler to be executed at the specified interval. We will use this approach for the ValueSelector.
Here is how we will handle events on our buttons:
- When a button is tapped, the click event is fired and the value is incremented/decremented.
- If a button is long-pressed, a long click event is fired. In this case, set the repeat flag to true, and use a Handler to execute a task that updates the value.
- The task continues repeating itself as long as the flag is true.
- The flag is set to false when the button is released, causing the task to stop repeating.
If you are not familiar with the Android Handler class, for now you just need to know that it provides a way to execute Runnable objects on the UI thread of your Android application. Creating a Handler for the UI thread could not be simpler:
Handler handler = new Handler();
Now create Runnables for incrementing and decrementing. Note that we are checking boolean values (plusButtonIsPressed and minusButtonIsPressed) to determine if the repeated updating should continue. These flags are set and unset when a button is long-clicked and released.
private class AutoIncrementer implements Runnable { @Override public void run() { if(plusButtonIsPressed){ incrementValue(); handler.postDelayed( new AutoIncrementer(), REPEAT_INTERVAL_MS); } } } private class AutoDecrementer implements Runnable { @Override public void run() { if(minusButtonIsPressed){ decrementValue(); handler.postDelayed(new AutoDecrementer(), REPEAT_INTERVAL_MS); } } }
We have already added OnClickListeners to our buttons that are responsible for updating the value by a single integer. We need to add a couple more listeners in order for the repeat updating to work. The View class has an interface for handling long clicks, which is not surprisingly called OnLongClickListener. Implementing this listener will allow us to instantiate an AutoIncrementor or AutoDecremeter and post it to the UI thread using the Handler. The OnLongClickListener will also set the appropriate flag to indicate that the button is pressed and that the value should continue to be updated. Since the run method in these classes creates and posts another instance of the same class, the value will be updated repeatedly.
The final step is to stop the updates once the button is released. There is no specific listener on the View class for when a long click is released, so instead we need to implement the more generic OnTouchListener interface and check for specific actions. Here is what the complete event handling logic looks like for the plus button:
plusButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { incrementValue(); } }); plusButton.setOnLongClickListener( new View.OnLongClickListener() { @Override public boolean onLongClick(View arg0) { plusButtonIsPressed = true; handler.post(new AutoIncrementer()); return false; } } ); plusButton.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if ((event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL)) { plusButtonIsPressed = false; } return false; } });
Wrap Up
This post in our Android Custom View Tutorial series covered the basics of creating a custom View in Android by extending an existing layout and using existing views. The next post will discuss how to create a fully custom view by extending the View class and drawing directly to the Canvas.
More Android Custom View tutorials in this series:
[/et_bloom_locked]
can i access a layout inflated in base class in the derived class. In the base class init() method i will inflate a layout. I want to get access to that layout.. Can you help me on how to do that ???
When you inflate the layout in the base class, store a reference to the returned View. Then you just have to make sure it is accessible to the derived class (e.g. using the protected modifier).
my base class code
————————–
public class LxlContentTitleCardSmallView extends RelativeLayout {
private View rootView;
public LxlContentTitleCardSmallView(Context context) {
super(context);
init(context);
}
public LxlContentTitleCardSmallView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void init(Context context){
rootView = inflate(context, R.layout.lxl_content_title_card_small_view, this);
}
———————————————————————————————-
My derived class code
—————————–
public class LxlContentTitleCardLargeView extends LxlContentTitleCardSmallView {
private View rootView;
public LxlContentTitleCardLargeView(Context context) {
super(context);
init(context);
}
public LxlContentTitleCardLargeView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void init(Context context){
rootView = inflate(context, R.layout.lxl_content_title_card_small_view, this);
}
—————————————————————————
In the derived class, if i remove the inflate method, layout is not getting inflated. According to my understanding, when i create a object of derived class, base class constructor will also be called and the layout should be inflated there. But it is not happening like that. Do you have any idea why ???
In your example code, the derived class constructors are calling the base class constructors via the super keyword. However, when the base class calls the init() method, it will invoke the method on the derived class, since it is overriding the init() method of the super class. Therefore, if you remove the call to inflate in the derived class, the layout is never inflated. One possible solution is to call super.init() from the derived class init method.
yeah i figured out the problem myself today morning. Thank you so much for replying.
nice tutorial. simple and reusable