Introduction
Sometimes a scenario comes where android developers need to create gauges, compasses, speedometers etc. One component of them is Needle. In this tutorial, we will explain how to draw a simple needle and how to make it rotatable. We will use android graphics and View class.
Background
Ofcourse you need to know a little bit about android graphics, its components/features/methods and View class. Although I will explain each step so that you don't need to visit android docs / Google now and then while reading this tutorial/article. You should also be familiar with how to create andorid project :)
Using the code
This article includes steps to create needle. In some steps, you may find a better approach or some hardocre values, but that is done delibrately just for the sake of understanding. And ofcourse you can code better than me. Your feedback/improvements are openly welcomed. Lets start:
Step 1:
Create a new android application project with default settings. Create a seperate package with name of your choice. I had named it in my project as com.needle.views. And now create a class in com.needle.views and name it of your choice. I named it as Needle. So, I have class Needle.java.
Step 2:
When you create an empty class in android, it is inherited from parent Object class. This is by default. You need to extend it from View.
public class Needle extends View {
Note that, View
class is located in package android.view.View
. So import this package in your code. Intellisense will do this job for you. If not, press Ctrl+Shift+O.
import android.view.View;
public class Needle extends View {
It will highlight an error on class name forcing to write contructor. It will show you three contructors. Atleast one constructor is required. I have written all three constructors.
public <a id="constructor" name="constructor">Needle</a>(Context context) {
super(context);
}
public Needle(Context context, AttributeSet attrs) {
super(context, attrs);
}
public Needle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
Step 3:
When you are writing your own control or working on any graphics in View
, the method onDraw(Canvas canvas)
is required to be overriden. According to android docs, when you want to render your View, you must override this method. You must implement this method for your drawing. So, override this method.
- Right clicke any where in code editor and select Source -> Override/Implement Methods....
- A popup window will open. Find
onDraw(Canvas)
method. Select it and click OK.
And it will place code snippet for you.
@Override
protected void <a id="onDraw" name="onDraw"></a>onDraw(Canvas canvas) {
super.onDraw(canvas);
}
Canvas is a place where you will draw different shapes. It provides plenty of methods to draw different shapes. We will use it path method that will draw path for us.
Step 4:
Now we will draw lines such that it takes the shape of speedometer needle. We need two main components to do this job. First, a Path which needs points to join lines, second, a Paint which will be used for coloring, style etc.
The Paint class holds the style and color information about how to draw gemoetries, text and bitmaps.
So, lets instantiate two objects of type Path
and Paint
.
private Paint linePaint;
private Paint needleScrewPaint;
private Path linePath;
Initialize these objects. I will recommend you to write an init()
method, and do all initializations in it. And call init()
method in constructors.
public Needle(Context context) {
super(context);
init();
}
private void init() {
linePaint = new Paint();
linePath = new Path();
needleScrewPaint = Paint();
}
Step 5:
First, set the color and style information. The object linePaint
holds the information of color and style of our needle.
linePaint = new Paint();
linePaint.setColor(Color.RED);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(5.0f);
linePaint.setShadowLayer(8.0f, 0.1f, 0.1f, Color.GRAY);
You can change these values and play with them. And I will recommend to change the values and see the results and observe how to adjust these values right according to your requirements.
Step 6:
Now initialize the linePath
. We are going to draw lines which will end up in needle shape.
linePath = new Path();
linePath.moveTo(50.0f, 50.0f);
The method moveTo(float x, float y)
is the starting point of our line. Note that I have hardcoded x,y coordinates just for the sake of simplicity and better understanding. You can adjust these values according to screen size. Just play with them.
Now we need to define our next point. As soon as we define our next point, we will be able to see line in our device. A method lineTo(float x, float y)
will do this job. Note that, here x,y are end points. Line will be drawn from starting point to this point.
linePath.moveTo(50.0f, 50.0f);
linePath.lineTo(130.0f, 40.0f);
linePath.lineTo(600.0f, 50.0f);
linePath.lineTo(130.0f, 60.0f);
linePath.lineTo(50.0f, 50.0f);
linePath.close();
Note that I have made call to same method 4 times. Every time I call this method, end points are different and path keeps drawing. you can check this on your device/emulator. Before testing it, you need to do two things. In onDraw(Canvas) method, call canvas.drawPath(Path, Paint)
method as onDraw(Canvas) will render it on device screen.
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(linePath, linePaint);
}
In your Activity's xml layout, add this control: Note the package name and class name.
<com.needle.views.Needle
android:id="@+id/myNeedle"
android:layout_marginTop="10dp"
android:layout_height="wrap_content"
android:layout_width="wrap_content"/>
When you run it on emulator/device, the output will be like this:
Step 7:
Now we will add circle. This circle will add make it look more elegant. To fill this needle shape, change linePaint.setStyle(Paint.Style.STROKE)
to linePaint.setStyle(Paint.Style.FILL_AND_STROKE)
. The object linePath
contains a method addCircle(x, y, radius, direction)
which adds the circle in the line path. The method is pretty simple. But add it before close();
method.
.....
linePath.addCircle(130.0f, 50.0f, 20.0f, Path.Direction.CW);
linePath.close();
it will look like this:
TEST: Try setting linePaint.setStyle(Paint.Style.STROKE) and see the shape of needle.
We are almost done. Just need to modify this circle. Infact we will add new circle with radius less than current and same coordinates/center circle and will apply another paint properties. This is why we had taken paint object lineScrewPaint
.
In init()
method, initialize the lineScrewPaint
and set its values.
needleScrewPaint = new Paint();
needleScrewPaint.setColor(Color.BLACK);
needleScrewPaint.setAntiAlias(true);
needleScrewPaint.setShader(new RadialGradient(130.0f, 50.0f, 10.0f,
Color.DKGRAY, Color.BLACK, Shader.TileMode.CLAMP));
It is same as linePaint
but with different values and new things, Shader and RadialGradient. Shader is used to set gradient colors on your view. Try playing with these values and you will enjoy it. Now in onDraw() method, add following method call just after canvas.drawPath(linePath, linePaint);
canvas.drawCircle(130.0f, 50.0f, 16.0f, needleScrewPaint);
It will look like this:
So, this is our needle and ready to use. It very much simple. No super technique is involve.
Step 8:
Animation help taken from Stackoverflow.
Now we want to make it rotate. Take Matrix and three variables:
private Matrix matrix;
private int framePerSeconds = 100;
private long animationDuration = 10000;
private long startTime;
Initialize them in init()
or constructors.
public Line(Context context) {
super(context);
matrix = new Matrix();
this.startTime = System.currentTimeMillis();
this.postInvalidate();
init();
}
Now, change the onDraw(Canvas) method as following:
long elapsedTime = System.currentTimeMillis() - startTime;
matrix.postRotate(1.0f, 130.0f, 50.0f);
canvas.concat(matrix);
canvas.drawPath(linePath, linePaint);
canvas.drawCircle(130.0f, 50.0f, 16.0f,
if(elapsedTime < animationDuration){
this.postInvalidateDelayed(10000 / framePerSeconds);
}
invalidate();
Note that in matrix.postRotate(... , 130.0f, 50.0f)
method, we have three parameters. First one is degree to rotate, and remaining two parameters are axis. These two parameters must have same axis values as your circle's center values that you just set while adding circle to line. e.g. linePath.addCircle(130.0f, 50.0f, ...)
.
canvas.concat(matrix)
method just attaches the matrix with canvas.
Finally, invalidate()
, this method is required.
Here is the complete code of Needle.java class.
package com.needle.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
public class Needle extends View {
private Paint linePaint;
private Path linePath;
private Paint needleScrewPaint;
private Matrix matrix;
private int framePerSeconds = 100;
private long animationDuration = 10000;
private long startTime;
public Needle(Context context) {
super(context);
matrix = new Matrix();
this.startTime = System.currentTimeMillis();
this.postInvalidate();
init();
}
public Needle(Context context, AttributeSet attrs) {
super(context, attrs);
matrix = new Matrix();
this.startTime = System.currentTimeMillis();
this.postInvalidate();
init();
}
public Needle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
matrix = new Matrix();
this.startTime = System.currentTimeMillis();
this.postInvalidate();
init();
}
private void init(){
linePaint = new Paint();
linePaint.setColor(Color.RED);
linePaint.setStyle(Paint.Style.FILL_AND_STROKE);
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(5.0f);
linePaint.setShadowLayer(8.0f, 0.1f, 0.1f, Color.GRAY);
linePath = new Path();
linePath.moveTo(50.0f, 50.0f);
linePath.lineTo(130.0f, 40.0f);
linePath.lineTo(600.0f, 50.0f);
linePath.lineTo(130.0f, 60.0f);
linePath.lineTo(50.0f, 50.0f);
linePath.addCircle(130.0f, 50.0f, 20.0f, Path.Direction.CW);
linePath.close();
needleScrewPaint = new Paint();
needleScrewPaint.setColor(Color.BLACK);
needleScrewPaint.setAntiAlias(true);
needleScrewPaint.setShader(new RadialGradient(130.0f, 50.0f, 10.0f,
Color.DKGRAY, Color.BLACK, Shader.TileMode.CLAMP));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
long elapsedTime = System.currentTimeMillis() - startTime;
matrix.postRotate(1.0f, 130.0f, 50.0f);
canvas.concat(matrix);
canvas.drawPath(linePath, linePaint);
canvas.drawCircle(130.0f, 50.0f, 16.0f, needleScrewPaint);
if(elapsedTime < animationDuration){
this.postInvalidateDelayed(10000 / framePerSeconds);
}
invalidate();
}
}
Points of Interest
As we have seen that hwo it is easy to draw things from simple and scratch. It makes learning quickly. In this tutorial, I have made the needle with hardcoded values. You should apply dynamic values. You should also consider the onSizeChanged(int, int, int, int)
method. You may find repeated code in constructo, just ignore it and put it in init()
method. :)
Thanks for reading.
Send your feedback/suggestions and improvements.