Tuesday, February 28, 2012

Howto: A Rotation Knob Widget on Android

One of my projects includes a rotary knob which is missing in the
Android Api. This could be useful as a volume control.

This example demonstrates
  • how to extend ImageView for custom views
  • how to work with a listener to notify the Activity
  • how to calculate the angle theta of a unit vector w.r.t. the origin in a 2D coordinate system using spherical coordinates
First we need a picture for the view. I made a 300x300 png which is good enough for demonstration:




This is not a very nice knob, I know ... make your own.
Save it in your res/drawable folder. I called it "jog.png".

Next, add some stuff to res/layout/main.xml:

 <myprojects.android.RotaryKnobView 
   android:id="@+id/jogView"
   android:layout_width="300px"
   android:layout_height="300px"
   android:layout_gravity="center"
 />

This is the java code for the rotary knob widget:
Note that instead of the actual angle delta the argument to onKnobChanged is +1, when the knob is rotated right, and -1 otherwise. You could change that of course.

Finally, we create a listener in the main activity:

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
    
  RotaryKnobView jogView = (RotaryKnobView)findViewById(R.id.jogView);
  jogView.setKnobListener(new RotaryKnobView.RotaryKnobListener()
  {
     @Override
     public void onKnobChanged(int arg) {
  
      if (arg > 0)
        ; // rotate right 
      else
        ; // rotate left 
    
   });
    
}


That's it.

42 comments:

  1. Awesome piece of code dude, Just what I was looking for. i'm itching to get it up and running now.

    ReplyDelete
  2. thanks buddy!
    let me know if you can get it to work.
    You might also check out

    http://mindtherobot.com/blog/534/android-ui-making-an-analog-rotary-knob/

    - thomas

    ReplyDelete
  3. hi, yeah I tried checking out that link before but my browser gives me a malware warning on access. So I've been staying clear, maybe I@ll try from work tomorrow ;)

    While I'm back here though, and now I've had a play with it, yeah I got it working, The only thing I changed is the "c.rotate(angle,150,150);" to "c.rotate(angle,(width/2),(height/2)); " makes it a bit neater when adjusting the dimensions, which I note have to be in px, other wise all kinds of fun start happening ;)

    I have a question, I have a Radio player on my Sam Galaxy, and this has a rotary knob too, but their knob appears to focus under your finger constantly, I have to admit I would prefer this kind of control, have you seen it or would you know how to adapt your code to do this?

    TR.

    ReplyDelete
  4. Yeah I noticed the malware warning. This is odd, but I ignored it and opened the site.

    Changing angle,150,150 to the right thing is good of course.

    If I got you right (I don't know the player on the Galaxy) it should be simple to adjust my code
    to your needs: when the view is touched, you need to set the current angle to theta.
    It might be enough to add

    angle = theta_old;

    behind the line

    theta_old = getTheta(x, y);

    in the POINTER_DOWN branch; maybe there's more work to do, that also depends on what you want to do. I kept it simple and just send a +1 if it turns right, -1 otherwise. You might need to adjust that too.

    ReplyDelete
  5. Thanks for getting back so quick. I did try your suggestions, but it makes the knob a big shaky. I wish I could work out your code to have a go myself, but it really is a little above my level of java and maths. Its working well at the moment anyway, I added a different job image which minimizes the misalignment. Your implementation of +1, -1 is perfect for my need so all is good.

    cheers
    T.R.

    ReplyDelete
  6. hi, I worked out the finger tracking part. I noticed the angle and theta are 90deg off, so I added this bit of code, and all is well, of course I'm sure I could have just changed my png also!

    angle = theta - 270;

    anyway, cheers its working a treat.
    T.R.

    ReplyDelete
  7. Thanks for the code man!

    I am new to android programming and i want to make two knobs in the same activity. do you know how i can switch between the two ids ?

    i get the same value for both knob.

    ReplyDelete
  8. just give them individual ids, like

    android:id="@+id/jogView1"

    ...

    android:id="@+id/jogView2"

    in your code:

    RotaryKnobView firstJogView = (RotaryKnobView)findViewById(R.id.jogView1);
    ...
    RotaryKnobView secondJogView = (RotaryKnobView)findViewById(R.id.jogView2);

    give each of them his listener and it should work.

    thomas

    ReplyDelete
  9. First, thank you Thomas for sharing :) it means a lot.

    I'm complete novice so can you please tell me how can I pass value (angle)from rotaryKnob class to a textView in main activity. I know, it's dumb, sorry :)

    ReplyDelete
  10. you could add the knob-class a public method getAngle:

    public float getAngle() { return angle; }

    In the listener in your main activity, you can set the textview's text:

    public void onKnobChanged(int arg) {
    TextView tv = getResourceById(...);
    tv.setText("angle: " + jogView.getAngle());
    ...
    }

    I guess this should work.

    - thomas

    ReplyDelete
  11. Hello Thomas, is there a way to access MainActivity variables values from RotaryKnobView class? What ever I try all I get are original declared values.

    I use this in RotaryKnobView

    MainActivity myActiv = new MainActivity();
    float currentAngle = myActiv.getCurrentAngle();

    I'm looking for a way to reset knob to original or some predefined values. Sorry for newbie questions :)

    ReplyDelete
  12. hm, sorry but this makes no sense.
    What might work, though I did not test it, would be to add a method to the KnobView-Class, something like

    public void setAngle(float angle)
    {
    this.angle = angle;
    invalidate();
    }

    -thomas

    ReplyDelete
  13. Nice work.

    I've made a few modifications to support a dynamic size (in dp, or using fill_parent...) and to have the ability to listen to delta and angle. Code is available here : https://gist.github.com/4281823

    Thx!
    Yann

    ReplyDelete
  14. great code.
    can you tell me how to make the knob go back to the starting position upon users release. i want to use it for a dialer.
    thank you.

    ReplyDelete
  15. OK, I don't know exactly how to do it, but first you'll need to know when the view is released. Maybe http://developer.android.com/reference/android/view/View.OnTouchListener.html can give you a hint.
    Then I would start a thread that sets the knob angle continuously to zero and calls notifyListener on the way.

    - thomas

    ReplyDelete
  16. This is awesome. Heads up though. It wouldn't compile for me until I added

    xmlns:android="http://schemas.android.com/apk/res/android"

    to main.xml like this:

    ReplyDelete
  17. Have you ever tried of analog sound instrument apps?
    'Coz i'm making one,i want to know about the knob contrl...can u plz help me?

    ReplyDelete
  18. I used your example code in my own project and by and large, it's working very well. My project deviates from your example a few different ways, but the most significant is that I add the knob widget and a Done button to the display programmatically (no XML).

    I like how and where the graphics appear (most of the time), but the problem is that when the knob spins, it doesn't appear to be rotating around its center, and as a result wobbles side-to-side
    (and up-and-down) as the user spins it. It's not affecting the important stuff, but it looks funny and I'd like to fix it.
    rmayo100@yahoo.com

    ReplyDelete
  19. uhoh, strange ... I don't have that wobble. My first guess is that the picture isn't rotating around it's center (yeah, you mentioned that!).
    What size is the picture you are using for the knob?
    Did I miss something in the main xml?
    Maybe adding

    android:scaleType="centerInside"

    to the XML file might help here.

    - thomas

    ReplyDelete
  20. ah ok -- in main.xml I specify the size of the knob in pixels (300x300).
    If you forgot to change that and use the code with a picture of different size, that would explain the wobble ...

    - thomas

    ReplyDelete
    Replies
    1. There is no XML in my project associated with using the Knob (I mentioned that, too.) I don't know if that qualifies as doing things right or not, but that is what I'm doing right now. I'm using the 300x300 png file from this site. I'll experiment that with that, I guess.
      rmayo100@yahoo.com

      Delete
  21. Thank you for your code mate... I am new to android and programming itself...I am currently developing an app and planning to use a knob to take userinput instead of a seekbar. I want the knob to give me values between 0 to 100. Is it possible with the above provided code..Is it also possible to show a graphical progress around the knob for the turning status??If so how to do it..Do i need to make 100 drawables for the 100 graphical state as the values are between 0 to 100???

    ReplyDelete
  22. Thank you for your code mate... I have a doubt... I am planning to use a knob instead of a seekbar and wish to get values between 0 to 100. Is it possible with a knob???
    Can I show a visual progress(lighting up until the turned position) to show the knob place or something??

    ReplyDelete
  23. flyingboy, it's certainly possible to modify the code such that it returns a value between 0 and 100.
    Look at this code:

    int direction = (delta_theta > 0) ? 1 : -1;
    angle += 3*direction;

    notifyListener(direction);

    the listener right now receives a positive value of +1 if the knob is rotated to the right, -1 otherwise. You can use the current angle / 36 or so in the argument instead.

    Visual progress is possible too, but my guess is it's significantly harder.
    keep trying!

    - thomas

    ReplyDelete
  24. Thanks a lot for posting this code

    ReplyDelete
  25. Is there any way I can modify it so that instead of spinning freely it "snaps" to the next position (like on an analogue rotary switch for a multimeter)?
    Also can I make it return a value or call a function depending on what position the switch is in?
    (I am very inexperienced with Java, sorry)

    Thanks!

    ReplyDelete
    Replies
    1. While experimenting I found a solution for your first question. Pretty naive, but it works. Say you want to split the angles into discrete values 0-10 and make the wheel snap accordingly:
      1) add a global variable to store the total angle (not only the delta of the last movement), and another for the resulting 0-10 value:
      private float absoluteAngle = 0f;
      private int absoluteAngleDecimals = 0;
      2) in onTouch(View v, MotionEvent event), after determining the angle, update the absolute angle; then convert it to an integer 0-10 value dividing by 36 and rounding the result (the rounding does the trick):
      absoluteAngle += delta_theta;
      absoluteAngleDecimals = Math.round((absoluteAngle)/36);
      3) in onDraw(Canvas c) convert again the 0-10 value to degrees for the rotation:
      angle = absoluteAngleDecimals * 36;
      and then do the rotation.

      Delete
  26. Hi, thanks for the code. I'd like to set an upper and lower limit to the rotation in order to block the animation and the counting (I added the lines needed to get an integer output from 0 to 10 and display it in a TextView) like in the real thing, any ideas? I'm an Android newbie too.

    ReplyDelete
  27. This comment has been removed by the author.

    ReplyDelete
  28. Awesome piece of code. Helped me a lot.
    Thanks

    ReplyDelete
  29. Great, Thanks for sharing!
    Uwe

    ReplyDelete
  30. The way you structure your code is terrible.

    ReplyDelete
    Replies
    1. Alright Dude, thanks for letting me know.
      I cleaned it up a bit.
      If you're still not satisfied, please define "terrible" :-)

      Delete
  31. As somebody posted before,

    you should definitely change the angle calculation to this:

    angle = theta - 270;

    After I did that. The knob worked as expected. Thank you very much!

    ReplyDelete
  32. Hello there,
    First of all thank you for all the effort you have put in your code!
    And secondly i have a problem with xml file, it says: binary xml file and shows, the problem is the line with: <myprojects.android.RotaryKnobView any suggestions?
    Thanks in advance!

    ReplyDelete
    Replies
    1. Hi Anonymous,

      you need to add the xml snippet from above to your application's layout xml file (e.g. main.xml), just as you would do with a button, image view etc.

      Delete
    2. and change "myprojects.android.RotaryKnobView" to your project's package string.

      Delete
  33. hi thomas can you tell me how to modify the code so that when a touch the knob the small ball did not set the position on the angle on my finger instead only move when a move the finger it would behave more like real knob

    ReplyDelete
  34. Hey Thomas !

    This is awessssome piece of code man..
    Really !! I was dying for this logic for months...

    Great help.. Thank you so much .... :):)

    ReplyDelete
  35. Is there a way to add inertia to the rotation after the touch ends?

    ReplyDelete
  36. Hi, Great code. Can we stop the knob at particular angle.

    ReplyDelete