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
- We should be able to rearrange the inner components through xml based upon our need.
- The imageview and textview are optional but the rest must be provided.
- The ProgressTracker must be inherited from RelativeLayout which will give us more flexibility in arranging the child components.
- It should be generic and reusable.
- The user should be able to decide what happens when the button is clicked.
- 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.
|
1 2 3 4 5 6 7 8 9 |
<?xml version=>1.0> encoding=>utf-8>?> <resources> <declare-styleable name=>ProgressTracker>> <attr name=>progressBar> format=>integer> /> <attr name=>progressStatusImage> format=>integer>/> <attr name=>progressTaskDescription> format=>integer>/> <attr name=>progressActionButton1> format=>integer>/> </declare-styleable> </resources> |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res/com.iwannalearn.progresstracker" android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <com.iwannalearn.progresstracker.ProgressTracker android:id="@+id/progress_tracker" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#898989" android:descendantFocusability="blocksDescendants" android:padding="10dp" app:progressActionButton1="@+id/action_button" app:progressBar="@+id/progress" app:progressStatusImage="@+id/status_image" app:progressTaskDescription="@+id/description" > <TextView android:id="@+id/description" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ProgressBar android:id="@+id/progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/description" android:layout_marginTop="10dp" android:layout_toLeftOf="@id/status_image" /> <ImageView android:id="@+id/status_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_below="@id/description" android:layout_marginLeft="5dp" android:src="@drawable/task_status" /> <Button android:id="@+id/action_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_below="@id/status_image" android:layout_marginTop="10dp" /> </com.iwannalearn.progresstracker.ProgressTracker> </LinearLayout> |
Constructor of ProgressTracker.java
Do not worry about the resetProgressTracker() method. You can see the complete code to see its implementation.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public ProgressTracker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressTracker, defStyle, 0); //Check for the validity of the non-optional items int barId = a.getResourceId(R.styleable.ProgressTracker_progressBar, 0); if( barId == 0 ){ throw new IllegalArgumentException("The progressBar attribute is required and must refer " + "to a valid child."); } int button1Id = a.getResourceId(R.styleable.ProgressTracker_progressActionButton1, 0); if( button1Id == 0 ){ throw new IllegalArgumentException("The progressActionButton1 attribute is required and must refer " + "to a valid child."); } mBarId = barId; mButton1Id = button1Id; //We don't check for the description and the status image as these are optional mDescriptionId = a.getResourceId(R.styleable.ProgressTracker_progressTaskDescription, 0); mStatusId = a.getResourceId(R.styleable.ProgressTracker_progressStatusImage, 0); //Don't forget to call recycle() a.recycle(); resetProgressTracker(); } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
@Override protected void onFinishInflate() { mBar = (ProgressBar) findViewById(mBarId); if (mBar == null) { throw new IllegalArgumentException("The progressBar attribute must refer to an" + " existing child."); } mButton1 = (Button) findViewById(mButton1Id); if (mButton1 == null) { throw new IllegalArgumentException("The progressActionButton1 attribute must refer to an" + " existing child."); } //Extra bit of check to see whether the item is present in the layout at all if( mDescriptionId != 0 ){ mDescription = (TextView) findViewById(mDescriptionId); if (mDescription == null) { throw new IllegalArgumentException("The progressTaskDescription attribute must refer to an" + " existing child."); } } if( mStatusId != 0 ){ mStatus = (ImageView) findViewById(mStatusId); if (mStatus == null) { throw new IllegalArgumentException("The progressStatusImage attribute must refer to an" + " existing child."); } } } |
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
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="utf-8"?> <level-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:drawable="@drawable/icon_blank" android:maxLevel="0"/> <item android:drawable="@drawable/icon_wip" android:maxLevel="1"/> <item android:drawable="@drawable/icon_success" android:maxLevel="2"/> <item android:drawable="@drawable/icon_fail" android:maxLevel="3"/> </level-list> |
We will see the level-list drawable in details in another post.
The corresponding code handling the ImageView in the ProgressTracker is shown below
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private void setProgressStatus( ProgressStatus status ){ if( mProgressStatus == status ) return; mProgressStatus = status; if( mStatus != null ){ switch(status){ case NOTSTARTED: mStatus.setImageLevel(0); break; case INPROGRESS: mStatus.setImageLevel(1); break; case SUCCESS: mStatus.setImageLevel(2); break; case FAILED: mStatus.setImageLevel(3); break; } } if( listener != null ) listener.onProgressStopped(mProgressStatus); } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
final ProgressTracker progress_rearranged = (ProgressTracker) findViewById(R.id.progress_tracker_rearranged); progress_rearranged.setMax(100); progress_rearranged.setButtontext("Start"); progress_rearranged.setActionButtonOnClickListener(new OnClickListener() { @Override public void onClick(View v) { runProgressTracker(progress_rearranged, false); } }); progress_rearranged.setOnProgressTrackerListener(new ProgressTrackerListener() { @Override public void onProgressStopped(ProgressStatus status) { switch (status) { case SUCCESS: progress_rearranged.setText("The Task was successfull!"); incrementProgress.cancel(); timer.purge(); break; case FAILED: progress_rearranged.setText("The Task failed!"); timer.purge(); break; case INPROGRESS: progress_rearranged.setText("Calculating your Awesomeness!!!"); break; } } }); |
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
You can view and download the complete working code here.
