How to create a repeating animated moving gradient drawable, like an indeterminate progress?
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>
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
andtwo
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.