The article explains designing the custom view in Android along with some useful backgrounds in this regard. Then, a circular progress bar is developed that can be used in different apps as an example custom view.
Introduction
The Android framework provides several default views that are responsible for layouting, measuring, and drawing themselves and their related children. The views can also be responsible to save their states and manipulating their UI properties. Android UI elements are all based on the View
class that is a base class for these objects. Some of them are built-in views involving "Button
" and "TextView
".
Sometimes, it is highly needed to create a custom view (your own View
) based on the app's requirements. Customizing a view needs some related backgrounds that are presented in this article. Customizing a view deals with some primary different aspects that start from drawing and ends at storing. These different aspects should be simultaneously exploited to develop a well-behavior custom view with appropriate interactions. These aspects can be briefly listed as given below:
- Drawing
- Interactions
- Measurement
- Layouting
- Attributing
The above aspects are fully presented in the Background chapter of this article. Understanding the concepts of these aspects can answer a lot of questions that may arise through developing a custom view such as a circular progress bar.
Figure 1: Two instances of Circular Progress Bar
A circular progress bar is a type of progress bar with a circular shape that can be used in some specific apps. Two implemented versions of these progress bars are shown in Figure 1.
The developed progress bar can be easily implemented in other apps that is fully explained in this article.
Background
As stated earlier in the Introduction section, customizing a view requires some primary aspects that are respectively mentioned in this section. Before presenting these aspects, the following points are highly needed to be mentioned.
First of all, showing a simple view on the screen is hierarchy such as the root node is firstly shown, then its children are traversed, in-orderly. It means view parents are showed before their corresponding view children on the screen. It should be noted that this screen visualization is enough fast such that you cannot detect this in-order traversing.
All views should know how to draw, measure, and layout themselves for both built-in and custom ones. These methods are individually and independently called to show a view on the screen. Drawing, measuring, and layouting are the most important parts of designing a custom view that are totally defined and explained in this section.
A custom view class can be easily defined as given below:
public class myCustomView extends View {
public myCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
The above code defines an empty custom view whose parts should be defined respectively. In the sequel, the primary aspects and their implementation details are presented one after another.
Drawing
The rendering of the view on the screen is controlled in this part. Overriding the onDraw()
method enables us to depict the whole visual parts of the view. This method exploits a Canvas
object to paint the visual shape of the view such as an instance custom view. For example, you can draw a circle, line, and arc by this method which is the visual parts of your own custom view by this method. It should be noted that this method is similarly evaluated for all views including the custom or built-in ones.
Canvas
object is a simple 2D rendering tool for drawing shapes that can be used as given in the following code block to draw a simple circle:
@Override
protected void onDraw(Canvas canvas){
Paint paint=new Paint();
paint.setColor(Color.BLACK);
canvas.drawCircle(centerX,centerY,radius,paint);
}
The above code draws a black circle around the specific center and radius. The paint object depicts some styles of the drawing procedure involving the color, font size and so on. The canvas
object is able to draw a set of simple shapes whose list can be found by a simple net searching.
Measuring
Measuring handles the dimensions of the view involving its width
and height
. This part is evaluated by the onMeasure()
method that should be overridden. This method determines the width
and height
(the dimension) of a view based on its contents and parent's constraints. This method has two integer input arguments which are the measured specific width
and height
, respectively.
The overriding method makes a trade-off between the desired and specific (forced) dimensions. The desired dimension is the one that is given in the design procedure (manually designed in the XML files) while the specific (forced) dimension is given by the input arguments of the method. It must be emphasized that different modes exist that are presented in the following:
- Exact Mode: In this mode, the view is forced to have a specific dimension. The view must have the forced dimension that may happen through considering the
match_parent
attribute. - At_Most Mode: This mode happens when the maximum size is needed to be considered. In this mode, the view cannot be larger than the forced dimension.
- Unspecified Mode: The view is free to have the desired dimension and the specific width and heights values are not set in this mode. This mode happens when both
width
and height
are set to be wrap_content
.
For better understanding, a simple form of this method is given in the following:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width, height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthMeasureSpec;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthMeasureSpec);
} else {
width = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightMeasureSpec;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightMeasureSpec);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
As can be seen, the width
and height
of the view are manipulated based on the desired and forced dimensions and the pre-defined modes. The method onMeasuredDimension()
finally is called with the final dimensions. Note that, this method must be called at the end of the method to determine the dimensions of the view.
It is worth mentioning that there is another method resovelSizeAndState()
that simplifies the above code which is given in the following code block:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = resolveSizeAndState(desiredWidth, widthMeasureSpec);
int height= resolveSizeAndState(desiredHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
About the above code, it should be noted that the padding distances must be considered for correctly resizing. Up to now, the measuring aspect is totally clear by the above-mentioned notes.
Layouting
This part determines the exact location of the view in the screen that is evaluated by overriding the onLayout()
method. This method has five input arguments that are the changed (boolean), left, top, right, and bottom (integer) values, respectively. Though layouting, the position of the view can be manipulated to exactly locate at the best position. The first argument (changed) determines any change in the containing layout of this view that is a boolean variable.
Assume a custom view is developed that is around a center with specific values. Let centerX
and centerY
be the positions of the custom view. The following code can be used for this special case (such as used in the developed circular progress bar):
@Override
protected void onLayout(Boolean changed, int left, int top, int right, int bottom) {
if(changed){
centerX=(left+right)/2;
centerY=(top+bottom)/2;
}
}
Clearly, the center of the custom view locates at the center of the layout.
Attributing
A custom view surely contains a set of style and format attributes that should be accurately implemented. These XML attributes are optimally determined to follow the custom view configurations. To define these XML attributes, the following resources must be inserted within res/values/attrs.xml such as given below. In this example code, the custom view is named by myCustomView
whose required attributes are considered.
<code>="1.0"="utf-8"
<resources>
<declare-styleable name="myCustumView">
<attr name="backColor" format="Color />
<attr name="textSize format="Integer" />
...
</declare-styleable>
</resources></code>
The above XML enables us to define some required attributes such as colors, sizes, and so on. These attributes can be modified in both static and dynamic cases. In the static case, the values of these attributes can be initialized via setting their values in the layout
XML as given in the following code:
="1.0"="utf-8"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.mycustomview.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal|center_vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1">
<com.example.mycustomview
android:layout_width="400dp"
android:layout_height="400dp"
android:id="@+id/pb1"
app:backColor="#aaaaaa"
app:textSize="20"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
Indeed, the pre-defined attributes can be forced to have some initial values as defined in the above code.
In the dynamic case, the attributes of the custom view are modified by the code as presented in the circular progress bar that is attached in this article.
Interactions
A standard view reacts to its programmed events such as Touch, Click, and so on. For each definable event, the custom view can be programmed to interact appropriately in which some of its attributes may change. For this purpose, one instant of the custom view is defined in the main activity and its event listeners are implemented to perform some specific tasks.
For example, the developed circular progress bar is supposed to interact to the click event. The progress bar's value is increased by 5 after each click event. This example is implemented as follows:
public class MainActivity extends AppCompatActivity {
public ProgressBar progressBar1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar1=(ProgressBar)findViewById(R.id.pb1);
progressBar2=(ProgressBar)findViewById(R.id.pb2);
progressBar1.setValue(80);
progressBar2.setValue(30);
progressBar1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (progressBar1.value<100) {
progressBar1.value+=5;
progressBar1.Invalidate();
}else{
progressBar1.value=0;
}
}
});
}
}
As can be seen, the progress bar interacts to the click event and its value increases. Therefore, the amount of the progress value that is visually shown must be accurately modified and the view should be repaint (reshown). For this purpose, the Invalidate()
method is called that tells the view that some of its attributes are changed.
In fact, the view or its parent's attributes may be required to be reshown in this situation. Some methods are employed to tell the Android framework about this situation which are listed below that their goal is to awake view about its invalidation:
Invalidate()
: This method is called when the view is needed to be redraw. It will result in onDraw
being called instantly. This method must be called on an UI-thread. Postinvalidate()
: This method is the same of the Invalidate()
method except it is called on a background thread. requestLayout()
: The changes that may affect the size should be followed by calling this method. This method eventually trigger both onMeasure()
and onLayout()
methods not only for the mentioned view but also for the whole children in the parent views.
It is very important to properly call one of the above methods according to the occur event (change). It is mainly because these methods have different complexities that are not needed to be evaluated in some special cases. Typically, one of the above methods is surely called in the interaction part.
Circular Progress Bar
Based on the defined aspects, the circular progress bar is developed through the following codes. First of all, the view is attributed as given below:
package com.example.mycustomview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;
public class ProgressBar extends View {
private int backColor=0;
private int frontColor=0;
private int lineColor=0;
private boolean displayValue;
private int outerCirlceRadius=200;
private int innerCirlceRadius=150;
private int displayTextSize=20;
private int value=90;
private int centerX=200;
private int centerY=200;
private int width=400;
private int height=400;
private String name="";
public ProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
setupAttrs(attrs);
}
private void setupAttrs(AttributeSet attrs){
try {
for(int i=0;i<attrs.getAttributeCount();i++) {
if(attrs.getAttributeName(i).contains("backColor")) {
backColor = attrs.getAttributeIntValue(i, backColor);
}else if(attrs.getAttributeName(i).contains("frontColor")){
frontColor = attrs.getAttributeIntValue(i, frontColor);
}else if(attrs.getAttributeName(i).contains("lineColor")){
lineColor = attrs.getAttributeIntValue(i, lineColor);
}else if(attrs.getAttributeName(i).contains("displayValue")){
displayValue = attrs.getAttributeBooleanValue(i, false);
}else if(attrs.getAttributeName(i).contains("name")){
name = attrs.getAttributeValue(i);
}else if(attrs.getAttributeName(i).contains("layout_width")) {
String text=attrs.getAttributeValue(i);
text=text.substring(0,text.length()-5);
width = Integer.parseInt(text);
}else if(attrs.getAttributeName(i).contains("layout_height")) {
String text=attrs.getAttributeValue(i);
text=text.substring(0,text.length()-5);
height = Integer.parseInt(text);
}
}
}catch(Exception ex) {
Toast.makeText(getContext(),ex.getMessage(), Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onLayout(boolean changed,int left,int top,int right,int bottom){
if(changed){
centerX=(left+right)/2;
centerY=(top+bottom)/2;
}
}
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
width = Math.min(width, widthMeasureSpec);
height = Math.min(height, heightMeasureSpec);
width=Math.min(width,height);
height=Math.min(width,height);
innerCirlceRadius=(35*width)/100;
outerCirlceRadius=width/2;
displayTextSize=width/10;
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas){
Paint paint=new Paint();
paint.setColor(backColor);
canvas.drawCircle(centerX,centerY,outerCirlceRadius,paint);
paint.setColor(frontColor);
RectF rectf=new RectF();
rectf.left=centerX-outerCirlceRadius;rectf.top=centerY-outerCirlceRadius;
rectf.right=centerX+outerCirlceRadius;
rectf.bottom=centerY+outerCirlceRadius;
if(value<=50) {
canvas.drawArc(rectf, 180f, ((float)value)*3.6f, true, paint);
}else{
canvas.drawArc(rectf, 180f, 180f, true, paint);
canvas.drawArc(rectf, 0f, (value-50)*3.6f, true, paint);
}
paint.setColor(Color.WHITE);
canvas.drawCircle(centerX,centerY,innerCirlceRadius,paint);
if(displayValue) {
paint.setColor(lineColor);
paint.setTextSize(displayTextSize);
String displayText=name + " = "+ value;
int len=displayText.length()*displayTextSize/4;
canvas.drawText(displayText, centerX-len, centerY, paint);
}
}
public void setValue(int value){
this.value=value;
invalidate();
}
public int getValue(){
return this.value;
}
}
Different parts of circular progress bar such as measuring, layouting, and drawing are specially described in the above code. Then, the main activity code is given here:
package com.example.mycustomview;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
public ProgressBar progressBar1;
public ProgressBar progressBar2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar1=(ProgressBar)findViewById(R.id.pb1);
progressBar2=(ProgressBar)findViewById(R.id.pb2);
progressBar1.setValue(80);
progressBar2.setValue(30);
progressBar1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (progressBar1.getValue()<100) {
progressBar1.setValue(progressBar1.getValue() + 5);
}else{
progressBar1.setValue(0);
}
}
});
progressBar2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (progressBar2.getValue()<100) {
progressBar2.setValue(progressBar2.getValue() + 5);
}else{
progressBar2.setValue(0);
}
}
});
}
}
The main activity's layout of the project is given below:
="1.0"="utf-8"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.mycustomview.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal|center_vertical"
android:weightSum="2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1">
<com.example.mycustomview.ProgressBar
android:layout_width="400dp"
android:layout_height="400dp"
android:id="@+id/pb1"
app:backColor="#aaaaaa"
app:frontColor="#444444"
app:lineColor="#000000"
app:displayValue="true"
app:name="PB1"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1">
<com.example.mycustomview.ProgressBar
android:layout_width="400dp"
android:layout_height="400dp"
android:id="@+id/pb2"
app:backColor="#aa00aa"
app:frontColor="#440044"
app:lineColor="#000000"
app:displayValue="true"
app:name="PB2"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
Finally, the XML attributes of the circular progress bar is presented in the following:
="1.0"="utf-8"
<resources>
<declare-styleable name="ProgressBar">
<attr name="backColor" format="color"/>
<attr name="frontColor" format="color"/>
<attr name="lineColor" format="color"/>
<attr name="displayValue" format="boolean"/>
<attr name="name" format="string"/>
</declare-styleable>
</resources>
The result is shown in Figure 2:
Figure 2: Two implemented instances of Circular Progress Bar
The developed progress bar can be simply used in the other projects that are explained in the next section.
Using the Code
To use the developed circular progress bar, it suffices to insert the circular progress bar to your XML main activity's layout.
Points of Interest
This article discuses about developing a custom view and its related aspects and also provides a circular progress bar for better understanding of the mentioned concepts.
History
- 19th October, 2020: Initial version