Mobile Development 13 min read

Implementation of a Custom Progress View with Particle Animation on Android

This article explains how to build a custom Android progress view featuring random particle motion, radial gradient backgrounds, sector clipping, pointer color changes, animated color transitions, and a jumping number display, with complete source code and step‑by‑step implementation details.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Implementation of a Custom Progress View with Particle Animation on Android

The article describes the design and implementation of a custom Android progress view that combines particle animation, gradient backgrounds, sector clipping, pointer color changes, and animated numeric displays.

Key visual effects include particles moving from the circle’s perimeter toward the center with random radius, speed, opacity and a ±30° angle offset, a radial gradient background, a rotating color‑changing ring in timer mode, and a pointer whose color is altered via layer blending.

Particle behavior is encapsulated in the AnimPoint class, which stores position, radius, angle, velocity, and alpha values. The class implements Cloneable to allow efficient reuse.

public class AnimPoint implements Cloneable {
    private float mX;
    private float mY;
    private float radius;
    private double anger;
    private float velocity;
    private int num = 0;
    private int alpha = 153;
    private double randomAnger = 0;
}

Particles are initialized in init() , where random values are assigned to angle, speed, radius, position, offset angle, and alpha. The updatePoint() method moves each particle each frame, reduces its radius, and resets it when a distance or frame limit is reached.

public void init(Random random, float viewRadius) {
    anger = Math.toRadians(random.nextInt(360));
    velocity = random.nextFloat() * 2F;
    radius = random.nextInt(6) + 5;
    mX = (float) (viewRadius * Math.cos(anger));
    mY = (float) (viewRadius * Math.sin(anger));
    randomAnger = Math.toRadians(30 - random.nextInt(60));
    alpha = 153 + random.nextInt(102);
}

public void updatePoint(Random random, float viewRadius) {
    float distance = 1F;
    double moveAnger = anger + randomAnger;
    mX = (float) (mX - distance * Math.cos(moveAnger) * velocity);
    mY = (float) (mY - distance * Math.sin(moveAnger) * velocity);
    radius = radius - 0.02F * velocity;
    num++;
    int maxDistance = 180;
    int maxNum = 400;
    if (velocity * num > maxDistance || num > maxNum) {
        num = 0;
        init(random, viewRadius);
    }
}

The background circle uses a RadialGradient with defined color stops to create an inner‑to‑outer gradient, applied to a Paint object before drawing the circle.

float[] mRadialGradientStops = {0F, 0.69F, 0.86F, 0.94F, 0.98F, 1F};
int[] mRadialGradientColors = new int[6];
// colors are set here
mRadialGradient = new RadialGradient(0, 0, mCenterX, mRadialGradientColors, mRadialGradientStops, Shader.TileMode.CLAMP);
mSweptPaint.setShader(mRadialGradient);
canvas.drawCircle(0, 0, mCenterX, mSweptPaint);

To draw a sector‑shaped area, a Path is built with addArc , a line to the center, and close() , then the canvas is clipped with canvas.clipPath(mArcPath) before drawing.

private void getSectorClip(float r, float startAngle, float sweepAngle) {
    mArcPath.reset();
    mArcPath.addArc(-r, -r, r, r, startAngle, sweepAngle);
    mArcPath.lineTo(0, 0);
    mArcPath.close();
}
// In onDraw():
canvas.clipPath(mArcPath);

The pointer image is recolored using a bitmap layer with PorterDuff.Mode.MULTIPLY . A white bitmap is drawn first, then the colored bitmap is blended to achieve the desired hue.

mXfermode = new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY);
canvas.saveLayer(mPointerRectF, mBmpPaint);
canvas.drawBitmap(mBitmapDST, null, mPointerRectF, mBmpPaint);
mBmpPaint.setXfermode(mXfermode);
canvas.drawBitmap(mBitmapSRT, null, mPointerRectF, mBmpPaint);
canvas.restore();

Background color changes with the sweep angle are calculated using android.animation.ArgbEvaluator . The view divides the 3600° sweep into four color segments, computes a fraction, and interpolates between start and end colors for each segment.

float fraction = progressValue % 900 / 900;
if (progressValue < 900) {
    mParameter.setInsideColor(evaluate(fraction, insideColor1, insideColor2));
    // other colors set similarly
} else if (progressValue < 1800) {
    // second segment
} // ...

Number animation is achieved with two TextView widgets inside a FrameLayout . A ValueAnimator drives a translation animation that moves the visible number up or down, creating a smooth jumping effect.

<FrameLayout ...>
    <TextView android:id="@+id/tv_number_one" ... />
    <TextView android:id="@+id/tv_number_two" ... />
</FrameLayout>

mNumberAnim = ValueAnimator.ofFloat(0F, 1F);
mNumberAnim.setDuration(400);
mNumberAnim.addUpdateListener(animation -> {
    float value = (float) animation.getAnimatedValue();
    if (UP_OR_DOWN_MODE == UP_ANIMATOR_MODE) {
        mTvFirst.setTranslationY(-mHeight * value);
        mTvSecond.setTranslationY(-mHeight * value);
    } else {
        mTvFirst.setTranslationY(mHeight * value);
        mTvSecond.setTranslationY(-2 * mHeight + mHeight * value);
    }
});

The complete source code for the custom view, including all the classes and XML layouts, is available at GitHub .

Mobile DevelopmentAndroidCustom ViewParticle AnimationProgressView
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.