Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I program a rotating circular animation such as this one (picture attached)? Android

I am developing an app & just built its logical part. Now I want to design this app like in famous timer apps.for examples:

http://goo.gl/N3qS1
enter image description here

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??

like image 216
Aexyn Avatar asked Jan 25 '13 19:01

Aexyn


2 Answers

The solution of Antimonit has two significant problems:

  1. Memory leak occure when you destroy activity/fragment with circular clock view, and show clock again.
  2. All parameters are hardcoded in java class, and circular clock view class in not reusable.

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();
}
like image 51
wapn Avatar answered Nov 11 '22 18:11

wapn


Will this do?

Update: Now also correctly handles real world time.

Sample Screenshot: 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);
        }

    }

}
like image 31
Antimonit Avatar answered Nov 11 '22 19:11

Antimonit