I am developing an app & just built its logical part. Now I want to design this app like in famous timer apps.for examples:
The thing I want is the outer Circle that fills with every trigger of some event or with increment of number. I actually don't know that is it animation part (like to be built in flash or what) or just possible by coding in android itself using its inbuilt properties and features. So anybody if tell explain me what tools are used or any reference tutorial that can explain things from bottom. I really don't know any thing of designing . Any code for this??
The solution of Antimonit
has two significant problems:
circular clock view
, and show clock again.Based on Antimonit
code (thank You!), I create more reusable and memory safe solution. Now, almost all parameters can be set from XML
file. At the end in the activity/fragment class we need invoke startCount
method. I strongly recoment invoke removeCallbacks
method when activity/fragment are going to be destroyed to avoid memory leaks.
KakaCircularCounter.java class:
public class KakaCircularCounter extends View {
public static final int DEF_VALUE_RADIUS = 250;
public static final int DEF_VALUE_EDGE_WIDTH = 15;
public static final int DEF_VALUE_TEXT_SIZE = 18;
private Paint backgroundPaint;
private Paint progressPaint;
private Paint textPaint;
private RectF circleBounds;
private long startTime;
private long currentTime;
private long maxTime;
private long progressMillisecond;
private double progress;
private float radius;
private float edgeHeadRadius;
private float textInsideOffset;
private KakaDirectionCount countDirection;
private Handler viewHandler;
private Runnable updateView;
public KakaCircularCounter(Context context) {
super(context);
init(null);
}
public KakaCircularCounter(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public KakaCircularCounter(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
public KakaCircularCounter(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrSet) {
if (attrSet == null) {
return;
}
TypedArray typedArray = getContext().obtainStyledAttributes(attrSet, R.styleable.KakaCircularCounter);
circleBounds = new RectF();
backgroundPaint = setupBackground(typedArray);
progressPaint = setupProgress(typedArray);
textPaint = setupText(typedArray);
textInsideOffset = (textPaint.descent() - textPaint.ascent() / 2) - textPaint.descent();
radius = typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_clockRadius, DEF_VALUE_RADIUS);
edgeHeadRadius = typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_edgeHeadRadius, DEF_VALUE_EDGE_WIDTH);
countDirection = KakaDirectionCount.values()[typedArray.getInt(R.styleable.KakaCircularCounter_countFrom,
KakaDirectionCount.MAXIMUM.ordinal())];
typedArray.recycle();
}
private Paint setupText(TypedArray typedArray) {
Paint t = new Paint();
t.setTextSize(typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_textInsideSize, DEF_VALUE_TEXT_SIZE));
t.setColor(typedArray.getColor(R.styleable.KakaCircularCounter_textInsideColor, Color.BLACK));
t.setTextAlign(Paint.Align.CENTER);
return t;
}
private Paint setupProgress(TypedArray typedArray) {
Paint p = new Paint();
p.setStyle(Paint.Style.STROKE);
p.setAntiAlias(true);
p.setStrokeCap(Paint.Cap.SQUARE);
p.setStrokeWidth(typedArray.getDimensionPixelSize(R.styleable.KakaCircularCounter_clockWidth, DEF_VALUE_EDGE_WIDTH));
p.setColor(typedArray.getColor(R.styleable.KakaCircularCounter_edgeBackground, Color.parseColor("#4D4D4D")));
return p;
}
private Paint setupBackground(TypedArray ta) {
Paint b = new Paint();
b.setStyle(Paint.Style.STROKE);
b.setStrokeWidth(ta.getDimensionPixelSize(R.styleable.KakaCircularCounter_clockWidth, DEF_VALUE_EDGE_WIDTH));
b.setColor(ta.getColor(R.styleable.KakaCircularCounter_clockBackground, Color.parseColor("#4D4D4D")));
b.setAntiAlias(true);
b.setStrokeCap(Paint.Cap.SQUARE);
return b;
}
public void startCount(long maxTimeInMs) {
startTime = System.currentTimeMillis();
this.maxTime = maxTimeInMs;
viewHandler = new Handler();
updateView = () -> {
currentTime = System.currentTimeMillis();
progressMillisecond = (currentTime - startTime) % maxTime;
progress = (double) progressMillisecond / maxTime;
KakaCircularCounter.this.invalidate();
viewHandler.postDelayed(updateView, 1000 / 60);
};
viewHandler.post(updateView);
}
public void removeCallbacks() {
viewHandler.removeCallbacks(updateView);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float centerWidth = getWidth() / 2f;
float centerHeight = getHeight() / 2f;
circleBounds.set(centerWidth - radius,
centerHeight - radius,
centerWidth + radius,
centerHeight + radius);
canvas.drawCircle(centerWidth, centerHeight, radius, backgroundPaint);
canvas.drawArc(circleBounds, -90, (float) (progress * 360), false, progressPaint);
canvas.drawText(getTextToDraw(),
centerWidth,
centerHeight + textInsideOffset,
textPaint);
canvas.drawCircle((float) (centerWidth + (Math.sin(progress * 2 * Math.PI) * radius)),
(float) (centerHeight - (Math.cos(progress * 2 * Math.PI) * radius)),
edgeHeadRadius,
progressPaint);
}
@NonNull
private String getTextToDraw() {
if (countDirection.equals(KakaDirectionCount.ZERO)) {
return String.valueOf(progressMillisecond / 1000);
} else {
return String.valueOf((maxTime - progressMillisecond) / 1000);
}
}
}
KakaDirectionCount enum:
public enum KakaDirectionCount {
ZERO, MAXIMUM
}
atributes file in values directory (kaka_circular_counter.xml)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="KakaCircularCounter">
<attr name="clockRadius" format="dimension"/>
<attr name="clockBackground" format="color"/>
<attr name="clockWidth" format="dimension"/>
<attr name="edgeBackground" format="color"/>
<attr name="edgeWidth" format="dimension"/>
<attr name="edgeHeadRadius" format="dimension"/>
<attr name="textInsideSize" format="dimension"/>
<attr name="textInsideColor" format="color"/>
<attr name="countFrom" format="enum">
<enum name="ZERO" value="0"/>
<enum name="MAXIMUM" value="1"/>
</attr>
</declare-styleable>
</resources>
example of using in xml file:
<pl.kaka.KakaCircularCounter
android:id="@+id/circular_counter"
android:layout_width="180dp"
android:layout_height="180dp"
app:layout_constraintBottom_toBottomOf="@id/backgroundTriangle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/backgroundTriangle"
app:clockRadius="85dp"
app:clockBackground="@color/colorTransparent"
app:clockWidth="3dp"
app:edgeBackground="@color/colorAccentSecondary"
app:edgeWidth="5dp"
app:edgeHeadRadius="1dp"
app:textInsideSize="60sp"
app:textInsideColor="@color/colorWhite"
app:countFrom="MAXIMUM"/>
Example of using in activity or fragment:
//the number in parameter is the value of the counted time
binding.circularCounter.startCount(12000);
CAUTION: remember to remove callbacks when you destroy activity/fragment, because memory leak occure. For example:
@Override
public void onDestroyView() {
super.onDestroyView();
binding.circularCounter.removeCallbacks();
}
Will this do?
Update: Now also correctly handles real world time.
Sample Screenshot:
Code:
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextPaint;
import android.view.View;
import android.graphics.*;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new CircularCountdown(this));
}
private static class CircularCountdown extends View {
private final Paint backgroundPaint;
private final Paint progressPaint;
private final Paint textPaint;
private long startTime;
private long currentTime;
private long maxTime;
private long progressMillisecond;
private double progress;
private RectF circleBounds;
private float radius;
private float handleRadius;
private float textHeight;
private float textOffset;
private final Handler viewHandler;
private final Runnable updateView;
public CircularCountdown(Context context) {
super(context);
// used to fit the circle into
circleBounds = new RectF();
// size of circle and handle
radius = 200;
handleRadius = 10;
// limit the counter to go up to maxTime ms
maxTime = 5000;
// start and current time
startTime = System.currentTimeMillis();
currentTime = startTime;
// the style of the background
backgroundPaint = new Paint();
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setAntiAlias(true);
backgroundPaint.setStrokeWidth(10);
backgroundPaint.setStrokeCap(Paint.Cap.SQUARE);
backgroundPaint.setColor(Color.parseColor("#4D4D4D")); // dark gray
// the style of the 'progress'
progressPaint = new Paint();
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setAntiAlias(true);
progressPaint.setStrokeWidth(10);
progressPaint.setStrokeCap(Paint.Cap.SQUARE);
progressPaint.setColor(Color.parseColor("#00A9FF")); // light blue
// the style for the text in the middle
textPaint = new TextPaint();
textPaint.setTextSize(radius / 2);
textPaint.setColor(Color.BLACK);
textPaint.setTextAlign(Paint.Align.CENTER);
// text attributes
textHeight = textPaint.descent() - textPaint.ascent();
textOffset = (textHeight / 2) - textPaint.descent();
// This will ensure the animation will run periodically
viewHandler = new Handler();
updateView = new Runnable(){
@Override
public void run(){
// update current time
currentTime = System.currentTimeMillis();
// get elapsed time in milliseconds and clamp between <0, maxTime>
progressMillisecond = (currentTime - startTime) % maxTime;
// get current progress on a range <0, 1>
progress = (double) progressMillisecond / maxTime;
CircularCountdown.this.invalidate();
viewHandler.postDelayed(updateView, 1000/60);
}
};
viewHandler.post(updateView);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// get the center of the view
float centerWidth = canvas.getWidth() / 2;
float centerHeight = canvas.getHeight() / 2;
// set bound of our circle in the middle of the view
circleBounds.set(centerWidth - radius,
centerHeight - radius,
centerWidth + radius,
centerHeight + radius);
// draw background circle
canvas.drawCircle(centerWidth, centerHeight, radius, backgroundPaint);
// we want to start at -90°, 0° is pointing to the right
canvas.drawArc(circleBounds, -90, (float)(progress*360), false, progressPaint);
// display text inside the circle
canvas.drawText((double)(progressMillisecond/100)/10 + "s",
centerWidth,
centerHeight + textOffset,
textPaint);
// draw handle or the circle
canvas.drawCircle((float)(centerWidth + (Math.sin(progress * 2 * Math.PI) * radius)),
(float)(centerHeight - (Math.cos(progress * 2 * Math.PI) * radius)),
handleRadius,
progressPaint);
}
}
}
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