Custom Rotary Knob Control for Android

Here’s a new dynamic control for Android: a Rotary Knob. It can be used to adjust a value similar to a slider, but it is circular and has a certain advantage when it comes to aesthetics. Additionally it can be made to accept clicks, so besides working as a slider it can also function as a button. Either a momentary push button or a switch that can be toggled on or off.

volume-knobs-sw-s
And having said that, I’m sure you all know the Audio equipment volume buttons, that control both the volume and the on/off status of the particular device. Aslo in the software world, there are many audio/music applications that come with knobs that you can turn to set the volume.
volume-knobs-hw-s

Android implementation

The plan is to create a custom control, have it extend RelativeLayout and inside show two ImageViews, one would be the stator, a fixed image while the other one, the rotor, should be able to rotate over a given angle, while keeping the pivot at the center of our custom control. The control would also have two states, so when pressing the knob we would toggle from one state to another. To mark the two different positions, I will actually use two rotor images, one with a led turned on (green) and another one with it turned off (red/brown).
To intercept the user screen touches, we set a Touch listener. To make things easier (but not required), I have used a GestureDetector, exposing several callbacks of which I used three:

public boolean onDown(MotionEvent event) ...
public boolean onSingleTapUp(MotionEvent e) ...
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) ...

The first two are used for the toggle. If we press down and then press up at a location not far from the press down, then this is a simple click on the button.

public boolean onDown(MotionEvent event) {
	float x = event.getX() / ((float) getWidth());
	float y = event.getY() / ((float) getHeight());
	mAngleDown = cartesianToPolar(1 - x, 1 - y);// 1- to correct our custom axis direction
	return true;
}

public boolean onSingleTapUp(MotionEvent e) {
	float x = e.getX() / ((float) getWidth());
	float y = e.getY() / ((float) getHeight());
	mAngleUp = cartesianToPolar(1 - x, 1 - y);// 1- to correct our custom axis direction
	
	// if we click up the same place where we clicked down, it's just a button press
	if (! Float.isNaN(mAngleDown) && ! Float.isNaN(mAngleUp) && Math.abs(mAngleUp-mAngleDown) < 10) {
		SetState(!mState);
		if (m_listener != null) m_listener.onStateChange(mState);
	}
	return true;
}

If instead, we slide over our control, than we catch the movement in onScroll and we use it to rotate our rotor.

public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
	float x = e2.getX() / ((float) getWidth());
	float y = e2.getY() / ((float) getHeight());
	float rotDegrees = cartesianToPolar(1 - x, 1 - y);// 1- to correct our custom axis direction
	
	if (! Float.isNaN(rotDegrees)) {
		// instead of getting 0-> 180, -180 0 , we go for 0 -> 360
		float posDegrees = rotDegrees;
		if (rotDegrees < 0) posDegrees = 360 + rotDegrees;
		
		// deny full rotation, start start and stop point, and get a linear scale
		if (posDegrees > 210 || posDegrees < 150) {
			// rotate our imageview
			setRotorPosAngle(posDegrees);
			// get a linear scale
			float scaleDegrees = rotDegrees + 150; // given the current parameters, we go from 0 to 300
			// get position percent
			int percent = (int) (scaleDegrees / 3);
			if (m_listener != null) m_listener.onRotate(percent);
			return true; //consumed
		} else
			return false;
	} else
		return false; // not consumed
}

The angular interval is given to limit the rotation and give a start and stop point. For a volume knob, we will have a volume min and a volume max position corresponding to the limits of the permitted circular trajectory. But as pictures as better than words to illustrate a concept, here how this control looks like:
rotary_knob_android_custom_control

The math behind this control is simple. The user taps the control, so we get the touch coordinates as a (x,y) Cartesian pair. From it we compute the polar coordinates, that are used to know the rotation angle.
rotation_diagram_ex

private float cartesianToPolar(float x, float y) {
	return (float) -Math.toDegrees(Math.atan2(x - 0.5f, y - 0.5f));
}

Knowing that the tangent of the angle is the length of the opposite side divided to the length of the adjacent side, we can compute the arctangent to get the Angle in radians. Then, with toDegrees, we get it in degrees, but working with radians directly is also ok.
As the direction of rotation that we consider is not the same with the trigonometric direction of rotation, a series of sign changing and complementary calculations are required. This - sign in the function above is also for this purpose, and so is the calling the function with 1 - value: cartesianToPolar(1 - x, 1 - y). But if you get a pen and a piece of paper like I did when making this, you'll easily understand the logic.

To rotate the imageview we use a matrix and postRotate:

public void setRotorPosAngle(float deg) {
	if (deg >= 210 || deg <= 150) {
		if (deg > 180) deg = deg - 360;
		Matrix matrix=new Matrix();
		ivRotor.setScaleType(ScaleType.MATRIX);   
		matrix.postRotate((float) deg, m_nWidth/2, m_nHeight/2);//getWidth()/2, getHeight()/2);
		ivRotor.setImageMatrix(matrix);
	}
}
public void setRotorPercentage(int percentage) {
	int posDegree = percentage * 3 - 150;
	if (posDegree < 0) posDegree = 360 + posDegree;
	setRotorPosAngle(posDegree);
}

And finally , here is the result:
rotary_knob_android_custom_control 02

The source code

Code available under GPL v2, on Github or attached to this post, here.

This article has 22 Comments

  1. This is very useful. Thank you.
    I see three issues:

    1 – I cannot get the 100% value (max is 99%)
    2 – The knob should continue from its current position when scrolled, and not start from where it is touched.
    3 – As a consequence of the previous point, when the max value is reached and I go on scrolling, it starts again from 0.

    But again, thank you!

  2. 1- To get the 100% value, try to change:
    int percent = (int) (scaleDegrees / 3);
    in
    int percent = (int) (Math.round(scaleDegrees / 3));

  3. Like gb said, 1-how to continue from its current position when scrolled, but not start from where it is touched.
    2-how to improve when the max/min value is reached and I go on scrolling, it won’t starts again from 0 or 100.

  4. Nice job.
    But how can I use it in my project?
    I have a XML main_layout but I don’t know how should I add this knob to it.
    Please help me.

  5. Thank you dear Radu.
    I want to use the knob in a scrollview but the knob doesn’t work fine while rotating.
    How should I fix the issue?
    regards

  6. Hi, great work, thanks a lot!
    Do you have also a tutorial how to highlight the dots around the knob like in the first screenshots so that they match to the position of the knob and show the currently set value?
    Thanks and best regards!

  7. Hi,

    I read and tested your custom rotary knob i found here:
    http://www.pocketmagic.net/2013/11/custom-rotary-knob-control-for-android/#.VKMCTl4AR

    However, i’m interested in expanding it’s features.

    As i see in the post, there’s an image with four different variations of this button.
    I’m interested in the third or fourth one, which have a circular bar which fills up. (one with stripes and one solid fill)
    Is it possible to send me that code? Or to get me on the way.

    The feature i want to add afterwards is that this circular loadbar drags along with your fingertip as you fill it up with x-y movement .
    A bit like the MIDI Designer application on Iphone.

    I’ve created some pictures for my analysis to illustrate this behaviour:

    https://www.dropbox.com/s/s5k7l3547xdp21i/rotaryfingermovement.bmp?dl=0

    I’t’s my first android program task.

    Thanks in advance for your feedback.

  8. How can I implement the RKB in my XML layout? Java layouting works, but unfortunately the XML results in an inflating error…

  9. I need to rotate the image from 0 to 100 and 100 to 0. If I am reaching at 100 the scroll to down it will move to 0 or other position. I require two if I am reaching at 100 position I don’t want to move down I need to work only 100 to 0 like fan knob. Is it possible?

  10. Change this row then the rotation works again:
    matrix.postRotate((float) deg, getWidth()/2, getHeight()/2); // 210/2,210/2);//

  11. I made a custom control from this that can be snapped into a fragment. I have 3 of them for bass, mid, and treble. My problem with my implementation at the moment is that the roforon image wobbles a bit as you turn it. I’m basically using the code you had in your sample with one in the center of your activity, Yours doesn’t wobble, mine does !! Great job with the graphics and trig explanation. I’m also concerned on how this is going to scale against lanscape/portrait, and differing device form factors.

Leave a Reply