Custom Control – Extending Existing ViewGroups

In the previous post we saw how we can extend an existing View to enhance its properties. In the following post, we go a step ahead and extend one of the existing layouts, put in a few controls and turn it into a widget.

Introduction


Here’s an use case, where you need a reusable component called the ProgressTracker, which basically consists of a TextView, an ImageView, a ProgressBar and a Button.The function of the ProgressTracker is to show a Text saying what it is that you are doing, an Image to denote the state of the task and a button that the user can use to perform any action by attaching an OnClickListener() to it.

Requirement

  1. We should be able to rearrange the inner components through xml based upon our need.
  2. The imageview and textview are optional but the rest must be provided.
  3. The ProgressTracker must be inherited from RelativeLayout which will give us more flexibility in arranging the child components.
  4. It should be generic and reusable.
  5. The user should be able to decide what happens when the button is clicked.
  6. The user should be able to know what the state of the ProgressTracker is, ie, if the task has not started, in progress, successful or failed.

Implementation

To stay within the scope of the tutorial, I’ll focus only on the concerned code snippets. You can however go through the complete code to get an idea about how the ProgressTracker can be used using the sample project given at the end of the post.

We begin by extending the RelativeLayout and implementing the constructors. In it, we extract the styleable attributes which we define in the attrs.xml. As the progress bar and the button are not optional elements we extract their ids and check their validity. Incase, either of the component is missing we throw an exception. I am not going through the extraction details as I have already explained it in Custom Controls – Extending Existing Views.

attrs.xml
Based upon our requirements we have four attributes that specify each of the components in the widget.

activity_main.xml
The ProgressTracker can be used in the following manner. Each of the components are defined inside the ProgressTracker and also assigned to their respective roles within the widget.

Constructor of ProgressTracker.java
Do not worry about the resetProgressTracker() method. You can see the complete code to see its implementation.

Next, override the protected onFinishInflate() method which is called once the view and all of its children have been inflated from the xml. Now find the views by their ids.

Once we have the views, we now must delegate a few methods from the child components to the ProgressTracker so that they can be set up from outside using the ProgressTracker object. Lets override the following methods

  • setText(String text)
  • void setMax(int max)
  • int getMax()
  • void setProgress(int progress)
  • int getProgress()
  • void incrementProgressBy(int diff)
  • void setButtontext(String str)
  • void setActionButtonOnClickListener(OnClickListener l)

Apart from these we also add a few new methods to support error notification and status management of the progress tracker. Implementation of all the methods can be found in the sample project code.

As I have mentioned earlier, the imageview and the textview are optional. The textview is standard, so there is not much to explain. However, you need to pay attention to the ImageView.

The ImageView is used to show a separate image based upon the status of the ProgressTracker and there are 4 status as mentioned above. To display the 4 states, we use a level-list drawable that has 4 drawables corresponding to each state. Here’s how it looks. Make sure to set a level-list drawable to the ImageView src for proper functioning. Here’s what a level-list drawable looks like

task_status.xml

We will see the level-list drawable in details in another post.

The corresponding code handling the ImageView in the ProgressTracker is shown below

Here’s a little snippet from the MainActivity which shows you how the ProgressTracker can be used. See MainActivity.java for various combinations and complete implementation.

That is pretty much it. The magic is mainly in the attrs.xml, the constructor and the onFinishInflate() method. If you understand these 3, you will soon be extending the different layouts to make your own reusable components. The rest of the code is to give the ProgressTracker it’s functionality.

In the sample project, I have used 3 different ways the same ProgressTracker can be used. That is the beauty of it. The user is absolutely free in arranging the components in anyway they want. I have unfortunately been unable to make the UI too jazzy for now :P

You can view and download the complete working code here.

Custom Control – Extending Existing Views

Building on our previous example of how we can extend existing controls and customize them for our requirements, we improve the “CustomFontTextView”. In the last post, we saw that by extending a TextView and then setting it’s font in it’s constructor we can achieve a TextView with custom fonts. But you will soon see that it rather restricts our development. If we were dealing with a number of different fonts, we would have to replicate them in multiple classes and then use each of them.

What we need is a more generic approach to the problem. As a developer, one should always build his own toolkit of reusable components over time which speeds up development. We could make it more generic if there was a way in which we could specify the font type at the time of defining the xml. This is possible by declaring an attribute through styleable which could be accessed through the xml.

So the basic steps are as follows

  1. Declare an attribute in a styleable which will be used to specify the font type.
  2. Add your custom fonts in the assets/fonts folder.
  3. Use the specified font type in the constructor of your extended TextView to access the custom fonts.

Let’s start with the styleable. In the “res/values” folder, we add a resource file called attrs.xml. This is the file where all our attributes of our custom controls go. In it we add the following piece of code.

[sourcecode language="xml"]
<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="CustomFontTextView">
<attr name="fontName" format="enum">
<enum name="AirMole" value="0" />
<enum name="AirMoleAntique" value="1" />
<enum name="AirMoleShaded" value="2" />
<enum name="AirMoleStripe" value="3" />
</attr>
</declare-styleable>
</resources>
[/sourcecode]

We declare a styleable name “CustomFontTextView” and within it declare an attribute called “fontName” of type “enum”. In our example, we have four different font types which we have specified using name/value format.The attributes can be of different types such as integer, string, boolean, etc. We will discuss styleables in more details in another post.

The next thing we need to do is to access the attribute in our extended class. Check out the updated code of the CustomFontTextView below.

[sourcecode language="java"]
public class CustomFontTextView extends TextView {

public CustomFontTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if( !isInEditMode() ){
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomFontTextView,
defStyle, 0);

int fontId = a.getInteger(R.styleable.CustomFontTextView_fontName, -1);
if( fontId == -1 ){
throw new IllegalArgumentException("The font_name attribute is required and must refer "
+ "to a valid child.");
}
a.recycle();
initialize(fontId);
}

}

public CustomFontTextView(Context context, AttributeSet attrs) {
//call the constructor which has the complete definition
this(context, attrs, 0);
}

public CustomFontTextView(Context context) {
super(context);
//This fallbacks to the default TextView without applying any custom fonts
}

public void initialize(int fontId){

Typeface tf = null;
switch(fontId){
case 0:
tf = Typeface.createFromAsset(getContext().getAssets(), "fonts/airmole.ttf");
break;
case 1:
tf = Typeface.createFromAsset(getContext().getAssets(), "fonts/AirmoleAntique.ttf");
break;
case 2:
tf = Typeface.createFromAsset(getContext().getAssets(), "fonts/AirmoleShaded.ttf");
break;
case 3:
tf = Typeface.createFromAsset(getContext().getAssets(), "fonts/AirmoleStripe.ttf");
break;
}

setTypeface(tf);
}
}
[/sourcecode]

We declare a TypedArray which points to an array of values retrieved with obtainStyledAttributes(). We will look into it in more details at a later time. For now, we need to extract the fontName using the getInteger() of the TypedArray. We also pass a default value (-1 here) which is returned in case we don’t find a valid value in the array.

Note that it is important to call recycle() on the TypedArray so that the array can be cached for later use. A lot of people ask about the purpose of recycle() and why it is recommended. If you look at the code of TypedArray, you will realize that it is basically a caching mechanism which stores the currently retreived StyledAttributes for later re-use.

Once we retreive the fontId which is nothing but an enum value for the font name, we load the appropriate font type.

Finally, we need to use this in our xml files and specify the font. The code below does just that

[sourcecode language="xml"]
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.iwannalearn.customcontrols"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >

<com.iwannalearn.customcontrols.CustomFontTextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
app:fontName="AirMole"
android:text="AirMole" android:textSize="22sp"/>

<com.iwannalearn.customcontrols.CustomFontTextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
app:fontName="AirMoleStripe"
android:text="AirMoleStripe" android:textSize="22sp"/>

</LinearLayout>
[/sourcecode]

The implementation is pretty straight forward. You need to add the xml namespace of your custom control.

[sourcecode language="xml"]
xmlns:app="http://schemas.android.com/apk/res/com.iwannalearn.customcontrols"
[/sourcecode]

where “app” is the name we have assigned here to represent the namespace.

It can either be added at the top level viewgroup affecting the complete xml or just within your custom control. To specify the CustomFontTextView, you must specify the complete qualifying name. In this example, it is com.iwannalearn.customcontrols. CustomFontTextView. See what happens when you just specify CustomFontTextView. Check out the error that occurs.

To specify the font name, you must use the namespace:attributeName format. Ex, app:fontName = “AirMole”.

And that’s it! You are done. Compile and run it. If you have done things correctly you will see a screen similar to the following, with fonts specified by you.

Custom Fonts ScreenShot

Note: If you want to make it more generic. You could use a “string” format instead of “enum” and pass the name of the font to the CustomFontTextView. You can then use this name to open the appropriate file. Another thing that you can do to make it more efficient, is to create a spearate file which inflates the font file from the assets folder and keeps a reference to it. In that case, you can save re-inflation time, specially if you are using the CustomFontTextView in numerous places in your application.

You can view and download the complete working code here.

ListView with ArrayAdapter and customized items

ListView is one of the most common features implemented in any Android project. There are quite a few ways to implement ListView. The one we will be looking in today is a little on the advanced side and will help you make some zazzy Custom ListViews.

Before we begin, lets define a scenario and make sure we are all on the same page as far as the requirements are concerned.

We will have a data source (lets say some service like twitter), an array of objects (comments from the service) which we need to display in a list. The data source object would have

  • User’s Image
  • Name
  • Comment
  • A clickable link

There will be a single item type in the list. We will not include headers or footers in the list.

Before we begin, here are the components

  1. UI
    • comment_list_row.xml: This is the layout of each of your List items.
    • comment_list.xml: This is the Page containing your ListView.
    • CommentListActivity.java: An activity derived from the Android Activity.
  2. Adapter
    • CommentAdapter.java: An adapter derived from the Android ArrayAdapter.

Let us start with the UI components.

comment_list_row.xml

Make a simple UI containing all the fields mentioned above in the requirements. I generally prefer using RelativeLayout for basically 2 reasons. It gives me a lot more flexibility in laying out my components and at the same time does it efficiently.

comment_list.xml

Next we make the main layout which will house the ListView and will be set as the main content view in the CommentListActivity.java. Its a very simple xml with just a ListView item in a RelativeLayout. I have added 2 more TextViews, one to be displayed when the list is empty and the other is passed to the adapter. It is an empty textview which the adapter uses to display the data in case there is no valid view defined for the row.

CommentListActivity.java

This is our main activity or rather our only activity. We derive this from the Android Activity and override the onCreate() function. We get a reference to the ListView object using the findViewById() function and then set it’s adapter.

The adapter basically forms a bridge between the list and the underlying data. There are different types of adapters that can be used to supply data to the list but for now we will use a derivation of the ArrayAdapter. More about it later in the tutorial.

Tip!

To check that the provided view is shown when there is no data, comment out the prepareDataList() function.

We use the setEmptyView() function to set a view to display when there is no data to display in the list. The prepareDataList() function is used to generate the data source.

Set the ListView’s OnItemClickListener to handle the process that you may need to accomplish when your user clicks on an item.

[sourcecode language="java"]
public class CommentListActivity extends Activity {

private ArrayList<CommentObject> commentList = null;
private ListView commentListView = null;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.comment_list);

//Prepare the Dataset
prepareDataList();

commentListView = (ListView)findViewById(R.id.listComment);
TextView emptyList = (TextView)findViewById(R.id.txtEmptyList);
emptyList.setText("No comments to display!");
commentListView.setEmptyView(emptyList);
CommentArrayAdapter adapter = new CommentArrayAdapter(this, R.id.txtLoadingList, commentList);
commentListView.setAdapter(adapter);

//Manage the onItemClick method
commentListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
CommentObject currComment = (CommentObject)parent.getItemAtPosition(position);
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(CommentListActivity.this,
"You clicked on a comment by "+currComment.senderName, duration);
toast.show();

}
});
}

private void prepareDataList(){
/*
* Data for the list is loaded here
*/
}
}
[/sourcecode]

CommentAdapter.java

This is the heart of our list and needs close attention. If you master the adapter, you can customize your ListView as much as you want. In this tutorial, we will look into the ArrayAdapter. We start by overriding the following functions,

getCount() : Override this function to return the number of items present in the data set in the adapter. Here we return the size of the ArrayList containing the comment objects.
[sourcecode language="java" light="true"]
@Override
public int getCount() {
if( m_comments != null ){
return m_comments.size();
}
return 0;
}
[/sourcecode]

getView(int position, View convertView, ViewGroup parent) : This function is used to prepare the view that presents the data at the specified position in the data set. Here we inflate the view from the comment_list_row.xml and then bind the data in the ArrayList at the index corresponding to the position value. You will observe that we do a null check for the convertView and create only if it is null. This helps in making it more efficient as the xml inflation is not done for every row.

You need to implement the onClickListener of your link’s TextView to handle it.
[sourcecode language="java" light="true"]
@Override
public View getView(int position, View convertView, ViewGroup parent) {

CommentViewHolder holder = null;

//form the view
if( convertView == null ){
holder = new CommentViewHolder();
convertView = m_inflater.inflate(R.layout.comment_list_row, parent, false);
holder.senderImage = (ImageView)convertView.findViewById(R.id.imgUserImage);
holder.senderName = (TextView)convertView.findViewById(R.id.txtUserName);
holder.commentText = (TextView)convertView.findViewById(R.id.txtUserComment);
holder.link = (TextView)convertView.findViewById(R.id.txtClickableLink);

convertView.setTag(holder);

}else{

holder = (CommentViewHolder)convertView.getTag();
}

//bind the data to the view
CommentObject comment = m_comments.get(position);
if( comment.senderPhoto.contentEquals("calvin.png") )
holder.senderImage.setImageResource(R.drawable.calvin);
else if( comment.senderPhoto.contentEquals("hobbes.png") )
holder.senderImage.setImageResource(R.drawable.hobbes);
else if( comment.senderPhoto.contentEquals("susie.png") )
holder.senderImage.setImageResource(R.drawable.susie);

holder.senderName.setText(comment.senderName);
holder.commentText.setText(comment.commentText);
final String clickableText = comment.link;
holder.link.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(m_context, clickableText, duration);
toast.show();
}
});

return convertView;
}
[/sourcecode]

ViewHolder Class : The ViewHolder class is used to make the View creation optimization that we just saw above. This class helps us create the View once and then stores it so that it can be re-used. In more advanced tutorials, you’ll observe how this class can be used to update the ListView dynamically.
[sourcecode language="java" light="true"]
private class CommentViewHolder{
ImageView senderImage;
TextView senderName;
TextView commentText;
TextView link;
}
[/sourcecode]

Screenshots


ListView with an empty dataset

ListView without data

ListView with data populated by the ArrayAdapter

ListView with data

Final Review


In this tutorial you have learnt,

  • Make a ListView and attach an ArrayAdapter to provide data to it.
  • Handle both item clicks and also any clickable link that may be present in the item.
  • Customize getCount() and getView() to make your own list item view.
  • Use a ViewHolder to speed up your list creation.

You can find the source code attached Here.