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 did some research but couldn't find a working implementation of something to do this.
How could you create such a circular control driven by swipes?
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 :
I've modified the source of circularseekbar to work as you want.
You can get the mofidied class from modified cirucularseekbar
First Include the control in your layout and set your dial as a background
<com.yourapp.CircularSeekBar
android:id="@+id/circularSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/amount_wheel_bg" />
Then, in your activity (it should implement OnCircularSeekBarChangeListener) add the following:
//This is a reference to the layout control
private CircularSeekBar circularSeekBar;
//This is a reference to the textbox where you want to display the amount
private EditText amountEditText;
private int previousProgress = -1;
And add the following callback methods:
@Override
public void onProgressChanged(CircularSeekBar circularSeekBar,
int progress, boolean fromUser) {
if(previousProgress == -1)
{
//This is the first user touch we take it as a reference
previousProgress = progress;
}
else
{
//The user is holding his finger down
if(progress == previousProgress)
{
//he is still in the same position, we don't do anything
}
else
{
//The user is moving his finger we need to get the differences
int difference = progress - previousProgress;
if(Math.abs(difference) > CircularSeekBar.DEFAULT_MAX/2)
{
//The user is at the top of the wheel he is either moving from the 0 -> MAx or Max -> 0
//We have to consider this as 1 step
//to force it to be 1 unit and reverse sign;
difference /= Math.abs(difference);
difference -= difference;
}
//update the amount
selectedAmount += difference;
previousProgress= progress;
updateAmountText();
}
}
}
@Override
public void onStopTrackingTouch(CircularSeekBar seekBar) {
//reset the tracking progress
previousProgress = -1;
}
@Override
public void onStartTrackingTouch(CircularSeekBar seekBar) {
}
private void updateAmountText()
{
amountEditText.setText(String.format("%.2f", selectedAmount));
}
selectedAmount is a double property to store the amount selected.
I hope this can help you.
I've just written the following code and only tested it theoretically.
private final double stepSizeAngle = Math.PI / 10f; //Angle diff to increase/decrease dial by 1$
private final double dialStartValue = 50.0;
//Center of your dial
private float dialCenterX = 500;
private float dialCenterY = 500;
private float fingerStartDiffX;
private float fingerStartDiffY;
private double currentDialValueExact = dialStartValue;
public boolean onTouchEvent(MotionEvent event) {
int eventaction = event.getAction();
switch (eventaction) {
case MotionEvent.ACTION_DOWN:
//Vector between startpoint and center
fingerStartDiffX = event.getX() - dialCenterX;
fingerStartDiffY = event.getY() - dialCenterY;
break;
case MotionEvent.ACTION_MOVE:
//Vector between current point and center
float xDiff = event.getX() - dialCenterX;
float yDiff = event.getY() - dialCenterY;
//Range from -PI to +PI
double alpha = Math.atan2(fingerStartDiffY, yDiff) - Math.atan2(fingerStartDiffX, xDiff);
//calculate exact difference between last move and current move.
//This will take positive and negative direction into account.
double dialIncrease = alpha / stepSizeAngle;
currentDialValueExact += dialIncrease;
//Round down if we're above the start value and up if we are below
setDialValue((int)(currentDialValueExact > dialStartValue ? Math.floor(currentDialValueExact) : Math.ceil(currentDialValueExact));
//set fingerStartDiff to the current position to allow multiple rounds on the dial
fingerStartDiffX = xDiff;
fingerStartDiffY = yDiff;
break;
}
// tell the system that we handled the event and no further processing is required
return true;
}
private void setDialValue(int value) {
//assign value
}
If you would like to change the direction, simply do alpha = -alpha
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With