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:
DialView Class :
public abstract class DialView extends View {
private float centerX;
private float centerY;
private float minCircle;
private float maxCircle;
private float stepAngle;
public DialView(Context context) {
super(context);
stepAngle = 1;
setOnTouchListener(new OnTouchListener() {
private float startAngle;
private boolean isDragging;
@Override
public boolean onTouch(View v, MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
startAngle = touchAngle(touchX, touchY);
isDragging = isInDiscArea(touchX, touchY);
break;
case MotionEvent.ACTION_MOVE:
if (isDragging) {
float touchAngle = touchAngle(touchX, touchY);
float deltaAngle = (360 + touchAngle - startAngle + 180) % 360 - 180;
if (Math.abs(deltaAngle) > stepAngle) {
int offset = (int) deltaAngle / (int) stepAngle;
startAngle = touchAngle;
onRotate(offset);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
isDragging = false;
break;
}
return true;
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
centerX = getMeasuredWidth() / 2f;
centerY = getMeasuredHeight() / 2f;
super.onLayout(changed, l, t, r, b);
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
float radius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 2f;
Paint paint = new Paint();
paint.setDither(true);
paint.setAntiAlias(true);
paint.setStyle(Style.FILL);
paint.setColor(0xFFFFFFFF);
paint.setXfermode(null);
LinearGradient linearGradient = new LinearGradient(
radius, 0, radius, radius, 0xFFFFFFFF, 0xFFEAEAEA, Shader.TileMode.CLAMP);
paint.setShader(linearGradient);
canvas.drawCircle(centerX, centerY, maxCircle * radius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawCircle(centerX, centerY, minCircle * radius, paint);
paint.setXfermode(null);
paint.setShader(null);
paint.setColor(0x15000000);
for (int i = 0, n = 360 / (int) stepAngle; i < n; i++) {
double rad = Math.toRadians((int) stepAngle * i);
int startX = (int) (centerX + minCircle * radius * Math.cos(rad));
int startY = (int) (centerY + minCircle * radius * Math.sin(rad));
int stopX = (int) (centerX + maxCircle * radius * Math.cos(rad));
int stopY = (int) (centerY + maxCircle * radius * Math.sin(rad));
canvas.drawLine(startX, startY, stopX, stopY, paint);
}
super.onDraw(canvas);
}
/**
* Define the step angle in degrees for which the
* dial will call {@link #onRotate(int)} event
* @param angle : angle between each position
*/
public void setStepAngle(float angle) {
stepAngle = Math.abs(angle % 360);
}
/**
* Define the draggable disc area with relative circle radius
* based on min(width, height) dimension (0 = center, 1 = border)
* @param radius1 : internal or external circle radius
* @param radius2 : internal or external circle radius
*/
public void setDiscArea(float radius1, float radius2) {
radius1 = Math.max(0, Math.min(1, radius1));
radius2 = Math.max(0, Math.min(1, radius2));
minCircle = Math.min(radius1, radius2);
maxCircle = Math.max(radius1, radius2);
}
/**
* Check if touch event is located in disc area
* @param touchX : X position of the finger in this view
* @param touchY : Y position of the finger in this view
*/
private boolean isInDiscArea(float touchX, float touchY) {
float dX2 = (float) Math.pow(centerX - touchX, 2);
float dY2 = (float) Math.pow(centerY - touchY, 2);
float distToCenter = (float) Math.sqrt(dX2 + dY2);
float baseDist = Math.min(centerX, centerY);
float minDistToCenter = minCircle * baseDist;
float maxDistToCenter = maxCircle * baseDist;
return distToCenter >= minDistToCenter && distToCenter <= maxDistToCenter;
}
/**
* Compute a touch angle in degrees from center
* North = 0, East = 90, West = -90, South = +/-180
* @param touchX : X position of the finger in this view
* @param touchY : Y position of the finger in this view
* @return angle
*/
private float touchAngle(float touchX, float touchY) {
float dX = touchX - centerX;
float dY = centerY - touchY;
return (float) (270 - Math.toDegrees(Math.atan2(dY, dX))) % 360 - 180;
}
protected abstract void onRotate(int offset);
}
Use it :
public class DialActivity extends Activity {
@Override
protected void onCreate(Bundle state) {
setContentView(new RelativeLayout(this) {
private int value = 0;
private TextView textView;
{
addView(new DialView(getContext()) {
{
// a step every 20°
setStepAngle(20f);
// area from 30% to 90%
setDiscArea(.30f, .90f);
}
@Override
protected void onRotate(int offset) {
textView.setText(String.valueOf(value += offset));
}
}, new RelativeLayout.LayoutParams(0, 0) {
{
width = MATCH_PARENT;
height = MATCH_PARENT;
addRule(RelativeLayout.CENTER_IN_PARENT);
}
});
addView(textView = new TextView(getContext()) {
{
setText(Integer.toString(value));
setTextColor(Color.WHITE);
setTextSize(30);
}
}, new RelativeLayout.LayoutParams(0, 0) {
{
width = WRAP_CONTENT;
height = WRAP_CONTENT;
addRule(RelativeLayout.CENTER_IN_PARENT);
}
});
}
});
super.onCreate(state);
}
}
Result :