android – How to create a repeating animated moving gradient drawable, like an indeterminate progress?-ThrowExceptions

Exception or error:

Background

Android has a standard ProgressBar with a special animation when being indeterminate . There are also plenty of libraries of so many kinds of progress views that are available (here).

The problem

In all that I’ve searched, I can’t find a way to do a very simple thing:

Have a gradient from color X to color Y, that shows horizontally, and moves in X coordinate so that the colors before X will go to color Y.

For example (just an illustration) , if I have a gradient of blue<->red , from edge to edge , it would go as such:

enter image description here

What I’ve tried

I’ve tried some solutions offered here on StackOverflow:

but sadly they all are about the standard ProgressBar view of Android, which means it has a different way of showing the animation of the drawable.

I’ve also tried finding something similar on Android Arsenal website, but even though there are many nice ones, I couldn’t find such a thing.

Of course, I could just animate 2 views myself, each has a gradient of its own (one opposite of the other), but I’m sure there is a better way.

The question

Is is possible to use a Drawable or an animation of it, that makes a gradient (or anything else) move this way (repeating of course)?

Maybe just extend from ImageView and animate the drawable there?

Is it also possible to set how much of the container will be used for the repeating drawable ? I mean, in the above example, it could be from blue to red, so that the blue will be on the edges, and the red color would be in the middle .


EDIT:

OK, I’ve made a bit of a progress, but I’m not sure if the movement is ok, and I think that it won’t be consistent in speed as it should, in case the CPU is a bit busy, because it doesn’t consider frame drops. What I did is to draw 2 GradientDrawables one next to another, as such:

class HorizontalProgressView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val speedInPercentage = 1.5f
    private var xMovement: Float = 0.0f
    private val rightDrawable: GradientDrawable = GradientDrawable()
    private val leftDrawable: GradientDrawable = GradientDrawable()

    init {
        if (isInEditMode)
            setGradientColors(intArrayOf(Color.RED, Color.BLUE))
        rightDrawable.gradientType = GradientDrawable.LINEAR_GRADIENT;
        rightDrawable.orientation = GradientDrawable.Orientation.LEFT_RIGHT
        rightDrawable.shape = GradientDrawable.RECTANGLE;
        leftDrawable.gradientType = GradientDrawable.LINEAR_GRADIENT;
        leftDrawable.orientation = GradientDrawable.Orientation.RIGHT_LEFT
        leftDrawable.shape = GradientDrawable.RECTANGLE;
    }

    fun setGradientColors(colors: IntArray) {
        rightDrawable.colors = colors
        leftDrawable.colors = colors
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSize = View.MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = View.MeasureSpec.getSize(heightMeasureSpec)
        rightDrawable.setBounds(0, 0, widthSize, heightSize)
        leftDrawable.setBounds(0, 0, widthSize, heightSize)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.save()
        if (xMovement < width) {
            canvas.translate(xMovement, 0.0f)
            rightDrawable.draw(canvas)
            canvas.translate(-width.toFloat(), 0.0f)
            leftDrawable.draw(canvas)
        } else {
            //now the left one is actually on the right
            canvas.translate(xMovement - width, 0.0f)
            leftDrawable.draw(canvas)
            canvas.translate(-width.toFloat(), 0.0f)
            rightDrawable.draw(canvas)
        }
        canvas.restore()
        xMovement += speedInPercentage * width / 100.0f
        if (isInEditMode)
            return
        if (xMovement >= width * 2.0f)
            xMovement = 0.0f
        invalidate()
    }
}

usage:

    horizontalProgressView.setGradientColors(intArrayOf(Color.RED, Color.BLUE))

And the result (it does loop well, just hard to edit the video) :

enter image description here

So my question now is, what should I do to make sure it animates well, even if the UI thread is a bit busy ?

It’s just that the invalidate doesn’t seem a reliable way to me to do it, alone. I think it should check more than that. Maybe it could use some animation API instead, with interpolator .

How to solve:

The idea behind my solution is relatively simple: display a FrameLayout that has two child views (a start-end gradient and a end-start gradient) and use a ValueAnimator to animate the child views’ translationX attribute. Because you’re not doing any custom drawing, and because you’re using the framework-provided animation utilities, you shouldn’t have to worry about animation performance.

I created a custom FrameLayout subclass to manage all this for you. All you have to do is add an instance of the view to your layout, like this:

<com.example.MyHorizontalProgress
    android:layout_width="match_parent"
    android:layout_height="6dp"
    app:animationDuration="2000"
    app:gradientStartColor="#000"
    app:gradientEndColor="#fff"/>

You can customize the gradient colors and the speed of the animation directly from XML.

The code

First we need to define our custom attributes in res/values/attrs.xml:

<declare-styleable name="MyHorizontalProgress">
    <attr name="animationDuration" format="integer"/>
    <attr name="gradientStartColor" format="color"/>
    <attr name="gradientEndColor" format="color"/>
</declare-styleable>

And we have a layout resource file to inflate our two animated views:

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <View
        android:id="@+id/one"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <View
        android:id="@+id/two"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</merge>

And here’s the Java:

public class MyHorizontalProgress extends FrameLayout {

    private static final int DEFAULT_ANIMATION_DURATION = 2000;
    private static final int DEFAULT_START_COLOR = Color.RED;
    private static final int DEFAULT_END_COLOR = Color.BLUE;

    private final View one;
    private final View two;

    private int animationDuration;
    private int startColor;
    private int endColor;

    private int laidOutWidth;

    public MyHorizontalProgress(Context context, AttributeSet attrs) {
        super(context, attrs);

        inflate(context, R.layout.my_horizontal_progress, this);
        readAttributes(attrs);

        one = findViewById(R.id.one);
        two = findViewById(R.id.two);

        ViewCompat.setBackground(one, new GradientDrawable(LEFT_RIGHT, new int[]{ startColor, endColor }));
        ViewCompat.setBackground(two, new GradientDrawable(LEFT_RIGHT, new int[]{ endColor, startColor }));

        getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

            @Override
            public void onGlobalLayout() {
                laidOutWidth = MyHorizontalProgress.this.getWidth();

                ValueAnimator animator = ValueAnimator.ofInt(0, 2 * laidOutWidth);
                animator.setInterpolator(new LinearInterpolator());
                animator.setRepeatCount(ValueAnimator.INFINITE);
                animator.setRepeatMode(ValueAnimator.RESTART);
                animator.setDuration(animationDuration);
                animator.addUpdateListener(updateListener);
                animator.start();

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
                else {
                    getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }
            }
        });
    }

    private void readAttributes(AttributeSet attrs) {
        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.MyHorizontalProgress);
        animationDuration = a.getInt(R.styleable.MyHorizontalProgress_animationDuration, DEFAULT_ANIMATION_DURATION);
        startColor = a.getColor(R.styleable.MyHorizontalProgress_gradientStartColor, DEFAULT_START_COLOR);
        endColor = a.getColor(R.styleable.MyHorizontalProgress_gradientEndColor, DEFAULT_END_COLOR);
        a.recycle();
    }

    private ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            int offset = (int) valueAnimator.getAnimatedValue();
            one.setTranslationX(calculateOneTranslationX(laidOutWidth, offset));
            two.setTranslationX(calculateTwoTranslationX(laidOutWidth, offset));
        }
    };

    private int calculateOneTranslationX(int width, int offset) {
        return (-1 * width) + offset;
    }

    private int calculateTwoTranslationX(int width, int offset) {
        if (offset <= width) {
            return offset;
        }
        else {
            return (-2 * width) + offset;
        }
    }
}

How the Java works is pretty simple. Here’s a step-by-step of what’s going on:

  • Inflate our layout resource, adding our two to-be-animated children into the FrameLayout
  • Read the animation duration and color values from the AttributeSet
  • Find the one and two child views (not very creative names, I know)
  • Create a GradientDrawable for each child view and apply it as the background
  • Use an OnGlobalLayoutListener to set up our animation

The use of the OnGlobalLayoutListener makes sure we get a real value for the width of the progress bar, and makes sure we don’t start animating until we’re laid out.

The animation is pretty simple as well. We set up an infinitely-repeating ValueAnimator that emits values between 0 and 2 * width. On each “update” event, our updateListener calls setTranslationX() on our child views with a value computed from the emitted “update” value.

And that’s it! Let me know if any of the above was unclear and I’ll be happy to help.

###

I’ve decided to put ” pskink” answer here in Kotlin (origin here). I write it here only because the other solutions either didn’t work, or were workarounds instead of what I asked about.

class ScrollingGradient(private val pixelsPerSecond: Float) : Drawable(), Animatable, TimeAnimator.TimeListener {
    private val paint = Paint()
    private var x: Float = 0.toFloat()
    private val animator = TimeAnimator()

    init {
        animator.setTimeListener(this)
    }

    override fun onBoundsChange(bounds: Rect) {
        paint.shader = LinearGradient(0f, 0f, bounds.width().toFloat(), 0f, Color.WHITE, Color.BLUE, Shader.TileMode.MIRROR)
    }

    override fun draw(canvas: Canvas) {
        canvas.clipRect(bounds)
        canvas.translate(x, 0f)
        canvas.drawPaint(paint)
    }

    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

    override fun start() {
        animator.start()
    }

    override fun stop() {
        animator.cancel()
    }

    override fun isRunning(): Boolean = animator.isRunning

    override fun onTimeUpdate(animation: TimeAnimator, totalTime: Long, deltaTime: Long) {
        x = pixelsPerSecond * totalTime / 1000
        invalidateSelf()
    }
}

usage:

MainActivity.kt

    val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200f, resources.getDisplayMetrics())
    progress.indeterminateDrawable = ScrollingGradient(px)

activity_main.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="200dp"
        android:layout_height="20dp" android:indeterminate="true"/>
</LinearLayout>

###

final View bar = view.findViewById(R.id.progress);
final GradientDrawable background = new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, new int[]{Color.BLUE, Color.RED, Color.BLUE, Color.RED});
bar.setBackground(background);
bar.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
    @Override
    public void onLayoutChange(final View v, final int left, final int top, final int right, final int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
        background.setBounds(-2 * v.getWidth(), 0, v.getWidth(), v.getHeight());
        ValueAnimator animation = ValueAnimator.ofInt(0, 2 * v.getWidth());
        animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                background.setBounds(-2 * v.getWidth() + (int) animation.getAnimatedValue(), 0, v.getWidth() + (int) animation.getAnimatedValue(), v.getHeight());
            }
        });
        animation.setRepeatMode(ValueAnimator.RESTART);
        animation.setInterpolator(new LinearInterpolator());
        animation.setRepeatCount(ValueAnimator.INFINITE);
        animation.setDuration(3000);
        animation.start();
    }
});

This is the view for testing:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center" >

    <View
        android:id="@+id/progress"
        android:layout_width="match_parent"
        android:layout_height="40dp"/>

</FrameLayout>

###

I’ve modified ‘android developer’s code slightly which might help some people.

The animation didn’t seem to resize properly so I’ve fixed that, made the animation speed a bit easier to set (seconds instead of pixel based) and relocated the init code to allow embedding right into the layout xml without code in your Activity.

ScrollingProgressBar.kt

package com.test

import android.content.Context
import android.util.AttributeSet
import android.widget.ProgressBar
import android.animation.TimeAnimator
import android.graphics.*
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable

class ScrollingGradient : Drawable(), Animatable, TimeAnimator.TimeListener {

    private val paint = Paint()
    private var x: Float = 0.toFloat()
    private val animator = TimeAnimator()
    private var pixelsPerSecond: Float = 0f
    private val animationTime: Int = 2

    init {
        animator.setTimeListener(this)
    }

    override fun onBoundsChange(bounds: Rect) {
        paint.shader = LinearGradient(0f, 0f, bounds.width().toFloat(), 0f, Color.parseColor("#00D3D3D3"), Color.parseColor("#CCD3D3D3"), Shader.TileMode.MIRROR)
        pixelsPerSecond = ((bounds.right - bounds.left) / animationTime).toFloat()
    }

    override fun draw(canvas: Canvas) {
        canvas.clipRect(bounds)
        canvas.translate(x, 0f)
        canvas.drawPaint(paint)
    }

    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    override fun getOpacity(): Int = PixelFormat.TRANSLUCENT

    override fun start() {
        animator.start()
    }

    override fun stop() {
        animator.cancel()
    }

    override fun isRunning(): Boolean = animator.isRunning

    override fun onTimeUpdate(animation: TimeAnimator, totalTime: Long, deltaTime: Long) {
        x = pixelsPerSecond * totalTime / 1000
        invalidateSelf()
    }
}

class ScrollingProgressBar : ProgressBar {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

    init {
        this.indeterminateDrawable = ScrollingGradient()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        this.indeterminateDrawable.setBounds(this.left, this.top, this.right, this.bottom)
    }
}

Layout xml (replace com.test.ScrollingProgressBar with the location of code above)

<com.test.ScrollingProgressBar
        android:id="@+id/progressBar1"
        android:background="#464646"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:gravity="center"
        android:indeterminateOnly="true"/>

###

You may achieve it if you have different drawables which defines the colors which are required to show as progress bar.

Use AnimationDrawable animation_list

 <animation-list android:id="@+id/selected" android:oneshot="false">
    <item android:drawable="@drawable/color1" android:duration="50" />
    <item android:drawable="@drawable/color2" android:duration="50" />
    <item android:drawable="@drawable/color3" android:duration="50" />
    <item android:drawable="@drawable/color4" android:duration="50" />
    -----
    -----
 </animation-list>

And in your Activity/xml set this as a background resource to your progressbar.

Then do as follows

// Get the background, which has been compiled to an AnimationDrawable object.
 AnimationDrawable frameAnimation = (AnimationDrawable)prgressBar.getBackground();

 // Start the animation (looped playback by default).
 frameAnimation.start();

If we take the respective drawables in such a way which covers blue to red
and red to blue gradient effects respectively those images we have to mention in animation list as color1, color2 etc

This approach is similar to how we will make a GIF image with multiple static images.

###

for performance I would extend the ProgressBar class and override the onDraw method myself. Then draw a Rect with the proper Gradient in the Paint :
Canvas’s drawRect method where you specify coordinates and the Paint

Here is a good android input to start custom drawing :
Custom drawing by Android

And here is a simple start example of a custom drawing view :
Simple example using onDraw

So, in code, something like this would do for a static Gradient :

public class MyView extends View {
    private int color1 = 0, color2 = 1;
    private LinearGradient linearGradient = new LinearGradient(0,0,0,0,color1,color2, Shader.TileMode.REPEAT);
    Paint p;
    public MyView(Context context) {
        super(context);
    }

    @Override
    protected synchronized void onDraw(Canvas canvas) {
        p = new Paint();
        p.setDither(true);
        p.setShader(linearGradient);
        canvas.drawRect(0,0,getWidth(),getHeight(),p);
    }

    @Override
    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        linearGradient = new LinearGradient(0,heightMeasureSpec/2, widthMeasureSpec,heightMeasureSpec/2,color1,color2, Shader.TileMode.REPEAT);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

You can play with LinearGradient other constructor to get the desired effect (accepts a list of points, you would probably need 3 of them, the one in the middle giving the progress). You can implement the progress with a variable in your view. The onMeasure method allows me to adapt to the view changing it’s size. You can create a setProgress(float progress) method that sets a variable progress and invalidates the View :

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.view.View;

public class MyProgressBar extends View {

private int myWidth = 0, myHeight = 0;
private int[] myColors = new int[]{0,1};
private float[] myPositions = new float[]{0.0f,0.0f,1.0f};

private LinearGradient myLinearGradient = new LinearGradient(0,0,myWidth,myHeight/2,myColors,myPositions, Shader.TileMode.REPEAT);
private Paint myPaint = new Paint();

public MyProgressBar(Context context) {
    super(context);
    myPaint.setDither(true);
}

@Override
protected synchronized void onDraw(Canvas canvas) {
    myPaint.setShader(myLinearGradient);
    canvas.drawRect(0,0,getWidth(),getHeight(),p);
}

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    myWidth = widthMeasureSpec;
    myHeight = heightMeasureSpec;
    myLinearGradient = new LinearGradient(0,0,myWidth,myHeight/2,myColors,myPositions, Shader.TileMode.REPEAT);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// progress must be a percentage, a float between 0.0f and 1.0f
public void setProgress(float progress) {
    myPositions[1] = progress;
    myLinearGradient = new LinearGradient(0,0,myWidth,myHeight/2,myColors,myPositions, Shader.TileMode.REPEAT);
    this.invalidate();
}
}

Of course, you have to use the setProgress(progress) method for it to be dynamic.

Leave a Reply

Your email address will not be published. Required fields are marked *