Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a repeating animated moving gradient drawable, like an indeterminate progress?

Tags:

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:

  • Change horizontal progress bar indeterminate color
  • How to change android indeterminate ProgressBar color?
  • Custom Drawable for ProgressBar/ProgressDialog
  • How to change progress bar's progress color in Android
  • How to Change Horizontal ProgressBar start color and end Color gradient

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 .

like image 209
android developer Avatar asked Jan 30 '18 11:01

android developer


People also ask

What is indeterminate progress bar android?

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.

How do I fix the gradient on my animation?

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.

How to create gradient backgrounds in Android?

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.

What does repeating-linear-gradient do?

background-image: repeating-linear-gradient ( angle | to side-or-corner, color-stop1, color-stop2, ... ); Defines an angle of direction for the gradient.

How do I edit a gradient on the timeline?

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.


2 Answers

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> 
like image 136
android developer Avatar answered Sep 24 '22 03:09

android developer


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.

like image 43
Ben P. Avatar answered Sep 24 '22 03:09

Ben P.