Today I’m going to introduce several fundamental concepts of Android custom view. I will create an Android custom view to implement circular SeekBar
like the mockup below, make it as a widget library, open source at Github, and publish it to JCenter so that other developers can download and use it.

In this series of posts, I will cover two major topics:
- Fundamental concepts about Android custom view. (Part 1)
- The steps to publish this custom view as a library. (Part 2)
https://github.com/enginebai/SwagPoints
(It would be nice to give my project a star, thank you.)
Before you start…
Try to understand the requirement or design specifications, take a look at this widget first. Here is some requirements:
- The user can touch the indicator icon through this arc to set the current progress level.
- The center text display the current progress in integer.
- The indicator icon will stop dragging when exceeding max or min progress.
It works like an extension of circular SeekBar
with customized appearances and motion event.
Get Started:
View Lifecycle
All the android view widgets are based on View
, to implement custom view, you will start a subclass of View
and override several lifecycle callback methods of view, so you need to understand the view lifecycle at first. The following diagram shows some important methods to override:

Create view
To get started, the first thing we have to do is to create a class that extends View
, and provide two default constructors which allows us to create the view programmatically (1st constructor) or in XML layout (2nd constructor). The SwagPoint(Context context, AttributeSet attrs)
constructor is more important here because it is used when Android inflates the view from XML layout file, otherwise you will get exception.
public class SwagPoints extends View { | |
// used in view creation programmatically | |
public SwagPoints(Context context) { | |
super(context); | |
} | |
// used in XML layout file | |
public SwagPoints(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
} | |
} |
Then, there are several things we have to control or modify in our custom view:
- Attributes: What things are customizable in your view? Determine custom attributes that allow developer to change its appearance and behavior in XML layout file according to their design.
- Size: Determine the dimensions of the view and every components in this custom view on the screen.
- Drawing: Determine how the view and components to render on the screen which contains the shape, location, appearance.
- Touch: Determine the way the user can interact with the view by touching.
Customization
1. Attributes:
Here we provide several customizable attributes for developer: the initial progress points
, the range of progress max/min
, the interval when user change the progress step
, the color and size of progress/arc/text. To define custom attributes, we create res/values/attrs.xml
file and define custom attributes for your view in a <declare-styleable>
resource element as below.
<resources> | |
<declare-styleable name="SwagPoints"> | |
<attr name="points" format="integer" /> | |
<attr name="max" format="integer" /> | |
<attr name="min" format="integer"/> | |
<attr name="step" format="integer"/> | |
<attr name="indicatorIcon" format="reference" /> | |
<attr name="progressWidth" format="dimension" /> | |
<attr name="progressColor" format="color" /> | |
<attr name="arcWidth" format="dimension" /> | |
<attr name="arcColor" format="color" /> | |
<attr name="textSize" format="dimension"/> | |
<attr name="textColor" format="color"/> | |
<attr name="clockwise" format="boolean" /> | |
<attr name="enabled" format="boolean" /> | |
</declare-styleable> | |
</resources> |
After adding res/values/attrs.xml
file, we use TypedArray
to retrieve attribute value in Java class and define instance variables (the following variables with m
as prefix name) to store. Here we add a init() method to put inside the constructor:
private void init(Context context, AttributeSet attrs) { | |
float density = getResources().getDisplayMetrics().density; | |
// Defaults, may need to link this into theme settings | |
int arcColor = ContextCompat.getColor(context, R.color.color_arc); | |
int progressColor = ContextCompat.getColor(context, R.color.color_progress); | |
int textColor = ContextCompat.getColor(context, R.color.color_text); | |
mProgressWidth = (int) (mProgressWidth * density); | |
mArcWidth = (int) (mArcWidth * density); | |
mTextSize = (int) (mTextSize * density); | |
mIndicatorIcon = ContextCompat.getDrawable(context, R.drawable.indicator); | |
if (attrs != null) { | |
// Attribute initialization | |
final TypedArray a = context.obtainStyledAttributes(attrs, | |
R.styleable.SwagPoints, 0, 0); | |
Drawable indicatorIcon = a.getDrawable(R.styleable.SwagPoints_indicatorIcon); | |
if (indicatorIcon != null) | |
mIndicatorIcon = indicatorIcon; | |
int indicatorIconHalfWidth = mIndicatorIcon.getIntrinsicWidth() / 2; | |
int indicatorIconHalfHeight = mIndicatorIcon.getIntrinsicHeight() / 2; | |
mIndicatorIcon.setBounds(-indicatorIconHalfWidth, -indicatorIconHalfHeight, indicatorIconHalfWidth, | |
indicatorIconHalfHeight); | |
mPoints = a.getInteger(R.styleable.SwagPoints_points, mPoints); | |
mMin = a.getInteger(R.styleable.SwagPoints_min, mMin); | |
mMax = a.getInteger(R.styleable.SwagPoints_max, mMax); | |
mStep = a.getInteger(R.styleable.SwagPoints_step, mStep); | |
mProgressWidth = (int) a.getDimension(R.styleable.SwagPoints_progressWidth, mProgressWidth); | |
progressColor = a.getColor(R.styleable.SwagPoints_progressColor, progressColor); | |
mArcWidth = (int) a.getDimension(R.styleable.SwagPoints_arcWidth, mArcWidth); | |
arcColor = a.getColor(R.styleable.SwagPoints_arcColor, arcColor); | |
mTextSize = (int) a.getDimension(R.styleable.SwagPoints_textSize, mTextSize); | |
mTextColor = a.getColor(R.styleable.SwagPoints_textColor, mTextColor); | |
mClockwise = a.getBoolean(R.styleable.SwagPoints_clockwise, | |
mClockwise); | |
mEnabled = a.getBoolean(R.styleable.SwagPoints_enabled, mEnabled); | |
a.recycle(); | |
} | |
} |
2. Size:
In order to control the view dimension, we have to override View.onMeasure()
method and calculate the size of each components. Here we have to define the arc radius according to the width/height of our view.
@Override | |
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | |
final int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec); | |
final int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec); | |
final int min = Math.min(width, height); | |
mTranslateX = (int) (width * 0.5f); | |
mTranslateY = (int) (height * 0.5f); | |
int arcDiameter = min - getPaddingLeft(); | |
mArcRadius = arcDiameter / 2; | |
float top = height / 2 - (arcDiameter / 2); | |
float left = width / 2 - (arcDiameter / 2); | |
mArcRect.set(left, top, left + arcDiameter, top + arcDiameter); | |
updateIndicatorIconPosition(); | |
super.onMeasure(widthMeasureSpec, heightMeasureSpec); | |
} |
3. Drawings:
Give you a pen and a paper, you can draw what you want. To draw the view, you have to override the onDraw(Canvas canvas)
method.
Before that, we have to know what to draw and how to draw. Android provides two classes to do this job:
- What to draw, handled by
Canvas
which is provided as parameter inonDraw()
method. - How to draw, handled by
Paint
.
Before you use Canvas
to draw anything, it’s necessary to createPaint
objects. For performance optimization, creating the Paint
objects ahead is quite important, because the onDraw()
method is called when redrawn is needed at anytime, we won’t create the Paint
objects inside the onDraw()
method, we should avoid any allocation in onDraw()
method. Here we define these objects (one for arc, another for progress and the other for text) as instance variables and initialize at init()
method:
private void init(Context context, AttributeSet attrs) { | |
// ... | |
mArcPaint = new Paint(); | |
mArcPaint.setColor(arcColor); | |
mArcPaint.setAntiAlias(true); | |
mArcPaint.setStyle(Paint.Style.STROKE); | |
mArcPaint.setStrokeWidth(mArcWidth); | |
mProgressPaint = new Paint(); | |
mProgressPaint.setColor(progressColor); | |
mProgressPaint.setAntiAlias(true); | |
mProgressPaint.setStyle(Paint.Style.STROKE); | |
mProgressPaint.setStrokeWidth(mProgressWidth); | |
mTextPaint = new Paint(); | |
mTextPaint.setColor(textColor); | |
mTextPaint.setAntiAlias(true); | |
mTextPaint.setStyle(Paint.Style.FILL); | |
mTextPaint.setTextSize(mTextSize); | |
} |
Once we have Paint
objects defined, we can start to implement the onDraw(Canvas canvas)
method, here we’re going to draw the text to display current progress number, the arc and current progress:
@Override | |
protected void onDraw(Canvas canvas) { | |
if (!mClockwise) { | |
canvas.scale(-1, 1, mArcRect.centerX(), mArcRect.centerY()); | |
} | |
// draw the text | |
String textPoint = String.valueOf(mPoints); | |
mTextPaint.getTextBounds(textPoint, 0, textPoint.length(), mTextRect); | |
// center the text | |
int xPos = canvas.getWidth() / 2 - mTextRect.width() / 2; | |
int yPos = (int)((mArcRect.centerY()) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)); | |
canvas.drawText(String.valueOf(mPoints), xPos, yPos, mTextPaint); | |
// draw the arc and progress | |
canvas.drawArc(mArcRect, ANGLE_OFFSET, 360, false, mArcPaint); | |
canvas.drawArc(mArcRect, ANGLE_OFFSET, mProgressSweep, false, mProgressPaint); | |
if (mEnabled) { | |
// draw the indicator icon | |
canvas.translate(mTranslateX - mIndicatorIconX, mTranslateY - mIndicatorIconY); | |
mIndicatorIcon.draw(canvas); | |
} | |
} |
After overriding onDraw()
method, there is one more important method about drawing to introduce: invalidate()
, this method is used when any redrawing is needed, we won’t call onDraw()
directly, we just call this method instead, you can use this method anywhere inside your custom view, however, for performance optimization, make sure it’s called as infrequently as possible.
4. Touching:
When the user touch the screen, Android calls the onTouchEvent()
method, so we override View.onTouchEvent()
to handle the user input gestures:
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
if (mEnabled) { | |
this.getParent().requestDisallowInterceptTouchEvent(true); | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
if (mOnSwagPointsChangeListener != null) | |
mOnSwagPointsChangeListener.onStartTrackingTouch(this); | |
updateOnTouch(event); | |
break; | |
case MotionEvent.ACTION_MOVE: | |
updateOnTouch(event); | |
break; | |
case MotionEvent.ACTION_UP: | |
if (mOnSwagPointsChangeListener != null) | |
mOnSwagPointsChangeListener.onStopTrackingTouch(this); | |
setPressed(false); | |
this.getParent().requestDisallowInterceptTouchEvent(false); | |
break; | |
case MotionEvent.ACTION_CANCEL: | |
if (mOnSwagPointsChangeListener != null) | |
mOnSwagPointsChangeListener.onStopTrackingTouch(this); | |
setPressed(false); | |
this.getParent().requestDisallowInterceptTouchEvent(false); | |
break; | |
} | |
return true; | |
} | |
return false; | |
} |
There are several things we have to control when user touches the indicator or other place of view:
- Update the indicator icon position and progress text.
- Draw the current progress on the arc.
- Stop when reaching max or min.
To know the indicator icon position and the current progress to draw, we have to convert the touch coordinate on the screen to the angle of arc.
In our custom view, we consider the center of arc as origin (0, 0)
, and use trigonometric functions to transform the touch coordinate into the angle of the arc in degree (0, 360)
, and map to the current progress value of given range (min, max)
.

Here we add a method to convert from touch coordinate to the arc angle:
private double convertTouchEventPointToAngle(float xPos, float yPos) { | |
// transform touch coordinate into component coordinate | |
float x = xPos - mTranslateX; | |
float y = yPos - mTranslateY; | |
x = (mClockwise) ? x : -x; | |
double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2)); | |
angle = (angle < 0) ? (angle + 360) : angle; | |
return angle; | |
} |
Final Demo

Happy Coding!!
https://github.com/enginebai/SwagPoints
It would be nice to star my project, thank you!!
寫的蠻清楚的,推一個!
Good job you really motivated me to write my own library and open source it….
Keep going on your awesome project and do the gread job
Very good! Excellent post.
Is this library Kotlin compatible?
No plan for this, actually, it stops maintaining through.
Good Explanation. Can you help me Drawing two arc (left + right) with thumbs?
Hi, sure, you can send message via FB or email.
do you use vi in macbook?
its really helpful to create own views.
Elaborated all the points clearly. Thank you.
Nice and very useful tutorial. Share more tutorial like this.
It’s going to be end of mine day, but before finish I am reading this enormous piece of writing to improve my experience.
Great post but I was wanting to know if you could write a litte more on this topic? I’d be very thankful if you could elaborate a little bit further. Cheers!
Hi there, You have done an excellent job. I’ll certainly
digg it and personally recommend to my friends.
I’m sure they will be benefited from this site.