I am working on an Android application, and I have a TextView where I display a price (for example 50$).
I would like to have a circular control similar to this picture:
I wrote this custom FrameLayout to detect circular movement around its center point. I'm using orientation of three points on a plane and the angle between them to determine when the user has made half a circle in one direction and then completes it in the same.
public class CircularDialView extends FrameLayout implements OnTouchListener {
private TextView counter;
private int count = 50;
private PointF startTouch;
private PointF currentTouch;
private PointF center;
private boolean turning;
private boolean switched = false;
public enum RotationOrientation {
CW, CCW, LINEAR;
}
private RotationOrientation lastRotatationDirection;
public CircularDialView(Context context) {
super(context);
init();
}
public CircularDialView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CircularDialView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
this.startTouch = new PointF();
this.currentTouch = new PointF();
this.center = new PointF();
this.turning = false;
this.setBackgroundResource(R.drawable.dial);
this.counter = new TextView(getContext());
this.counter.setTextSize(20);
FrameLayout.LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
addView(this.counter, params);
updateCounter();
this.setOnTouchListener(this);
}
private void updateCounter() {
this.counter.setText(Integer.toString(count));
}
// need to keep the view square
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
center.set(getWidth()/2, getWidth()/2);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
startTouch.set(event.getX(), event.getY());
turning = true;
return true;
}
case MotionEvent.ACTION_MOVE: {
if(turning) {
currentTouch.set(event.getX(), event.getY());
RotationOrientation turningDirection = getOrientation(center, startTouch, currentTouch);
if (lastRotatationDirection != turningDirection) {
double angle = getRotationAngle(center, startTouch, currentTouch);
Log.d ("Angle", Double.toString(angle));
// the touch event has switched its orientation
// and the current touch point is close to the start point
// a full cycle has been made
if (switched && angle < 10) {
if (turningDirection == RotationOrientation.CCW) {
count--;
updateCounter();
switched = false;
}
else if (turningDirection == RotationOrientation.CW) {
count++;
updateCounter();
switched = false;
}
}
// checking if the angle is big enough is needed to prevent
// the user from switching from the start point only
else if (!switched && angle > 170) {
switched = true;
}
}
lastRotatationDirection = turningDirection;
return true;
}
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
turning = false;
return true;
}
}
return false;
}
// checks the orientation of three points on a plane
private RotationOrientation getOrientation(PointF a, PointF b, PointF c){
double face = a.x * b.y + b.x * c.y + c.x * a.y - (c.x * b.y + b.x * a.y + a.x * c.y);
if (face > 0)
return RotationOrientation.CW;
else if (face < 0)
return RotationOrientation.CCW;
else return RotationOrientation.LINEAR;
}
// using dot product to calculate the angle between the vectors ab and ac
public double getRotationAngle(PointF a, PointF b, PointF c){
double len1 = dist (a, b);
double len2 = dist (a, c);
double product = (b.x - a.x) * (c.x - a.x) + (b.y - a.y) * (c.y - a.y);
return Math.toDegrees(Math.acos(product / (len1 * len2)));
}
// calculates the distance between two points on a plane
public double dist (PointF a, PointF b) {
return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
}