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).
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:
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.
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) :
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 .
Indeterminate Progress Bar is a user interface that shows the progress of an operation on the screen until the task completes. There are two modes in which you can show the progress of an operation on the screen.
If the edge of the gradient is visible at any point during the animation, you can fix this easily. Just scrub the playhead on the Timeline until the white edge is visible. Use Control+T / Command+T to transform and move the gradient to fit within the bounds of the design.
Start a new android application development project. 2. Right click on drawable folder –>New -> Drawable Resource File . 3. Now we have to create three different gradient background files inside the drawable folder.
background-image: repeating-linear-gradient ( angle | to side-or-corner, color-stop1, color-stop2, ... ); Defines an angle of direction for the gradient.
Just scrub the playhead on the Timeline until the white edge is visible. Use Control+T / Command+T to transform and move the gradient to fit within the bounds of the design. Repeat these steps as necessary.
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.
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:
FrameLayout
AttributeSet
one
and two
child views (not very creative names, I know)GradientDrawable
for each child view and apply it as the backgroundOnGlobalLayoutListener
to set up our animationThe 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.
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