How to animate gradient?
Asked Answered
B

4

18

How to animate gradient from color#1 to color#2? Something similar to

enter image description here

I'm planning to use it as health-bar for unit (so, it will be finit animation starting with green and ending with red)

Beating answered 26/8, 2015 at 10:57 Comment(0)
B
33

While googling it, I found 2 ways to do it for android: use ShaderFactory or extends View, using new Shader(new LinearGradient()). Both answers do the same - calling new Shader() every View.onDraw(Canvas canvas) method's call. Its really expensive if number of such animated gradients more than ~3.

So I did it another way. I avoided calling new every onDraw(), using single precalculated LinearGradient. That's how it is look like (gif, so animation decayed) :

enter image description here

The trick is to create LinearGradient which is colorsCount times larger than View.getWidth(). After that you can use canvas.translate(), while drawing gradient, to change its color, so no new calls in onDraw() at all.

To create gradient, you need current width and height. I did it in onSizeChanged(). Also I set Shader here too.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);

    width = getWidth();
    height = getHeight();

    LinearGradient gradient = new LinearGradient(
            0, height / 2, width * colors.length - 1, height / 2,
            colors, null, Shader.TileMode.REPEAT);
    fillPaint.setShader(gradient);

    shapePath = getParallelogrammPath(width, height, sidesGap);
    shapeBorderPath = getParallelogrammPath(width, height, sidesGap);
}

I use path because of parallelogramy views, you can use whatever you want. When implementing drawing, you should notice 2 things: you need to translate() whole canvas on current offset and offset() your filling shape:

@Override
protected void onDraw(Canvas canvas) {
    canvas.save();
    canvas.translate(-gradientOffset, 0);
    shapePath.offset(gradientOffset, 0f, tempPath);
    canvas.drawPath(tempPath, fillPaint);
    canvas.restore();

    canvas.drawPath(shapeBorderPath, borderPaint);

    super.onDraw(canvas); // my View is FrameLayout, so need to call it after
}

Also you should use canvas.save() & canvas.restore(). It will save inner matrix of canvas to stack and restore it correspondingly.

So the last what you need to do is to animate gradientOffset. You can use everything you want like ObjectAnimator (Property Animation). I used TimeAnimator, because I needed to control updateTick and starting offset directly. Here is my realisation (a bit difficult and harsh):

static public final int LIFETIME_DEAFULT = 2300;
private long lifetime = LIFETIME_DEAFULT, updateTickMs = 25, timeElapsed = 0;
private long accumulatorMs = 0;
private float gradientOffset = 0f;

public void startGradientAnimation() {
    stopGradientAnimation();
    resolveTimeElapsed();

    final float gradientOffsetCoef = (float) (updateTickMs) / lifetime;
    final int colorsCount = this.colors.length - 1;
    gradientAnimation.setTimeListener(new TimeAnimator.TimeListener() {
        @Override
        public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
            final long gradientWidth = width * colorsCount;
            if (totalTime > (lifetime - timeElapsed)) {
                animation.cancel();
                gradientOffset = gradientWidth;
                invalidate();
            } else {
                accumulatorMs += deltaTime;

                final long gradientOffsetsCount = accumulatorMs / updateTickMs;
                gradientOffset += (gradientOffsetsCount * gradientWidth) * gradientOffsetCoef;
                accumulatorMs %= updateTickMs;

                boolean gradientOffsetChanged = (gradientOffsetsCount > 0) ? true : false;
                if (gradientOffsetChanged) {
                    invalidate();
                }
            }
        }
    });

    gradientAnimation.start();
}

The full View code you can find here

Beating answered 26/8, 2015 at 10:57 Comment(5)
I don't understand ... you asked the question and answered it at the same time on the same day? This never was a question was it.Megargee
@Chris-Jr, I searched a lot and didn't find an answer I wanted. So I did some sort of research and finally got it. Then I thought "hm, maybe someone else will search it in the future and be stuck with it as I do?" - so I created the question with the answer. Also, there is a built-in feature to do that. Try to click Ask Question. There is a check Answer your own question – share your knowledge, Q&A-styleBeating
Ah, alright got you, I was just confused by the timing. Thanks for that, and thanks for sharing your solution.Megargee
Thanks Nexen, it's definitely the right thing to do. I appreciate it and I'm definitely glad to have your answer to start my own research fromCoquelicot
thanks for point the way, time saved. Also TimeAnimator require api16, Objectanimator with AnimatorUpdateListener will also do the trick.Kistler
T
26

I've figured out different, way shorter approach. Simply use setColors (int[] colors) method of GradientDrawable and animate it over time.

This is an example how to do it:

val start = Color.DKGRAY
val mid = Color.MAGENTA
val end = Color.BLUE

//content.background is set as a GradientDrawable in layout xml file 
val gradient = content.background as GradientDrawable

val evaluator = ArgbEvaluator()
val animator = TimeAnimator.ofFloat(0.0f, 1.0f)
animator.duration = 1500
animator.repeatCount = ValueAnimator.INFINITE
animator.repeatMode = ValueAnimator.REVERSE
animator.addUpdateListener {
    val fraction = it.animatedFraction
    val newStart = evaluator.evaluate(fraction, start, end) as Int
    val newMid = evaluator.evaluate(fraction, mid, start) as Int
    val newEnd = evaluator.evaluate(fraction, end, mid) as Int

    gradient.colors = intArrayOf(newStart, newMid, newEnd)
}

animator.start()

And the outcome:

enter image description here

Tobe answered 24/6, 2018 at 7:47 Comment(0)
A
10

just rewrited @Krzysztof Misztal's answer to java :

public static void startAnimation(final int view, final Activity activity) {
        final int start = Color.parseColor("#FDB72B");
        final int mid = Color.parseColor("#88FDB72B");
        final int end = Color.TRANSPARENT;


        final ArgbEvaluator evaluator = new ArgbEvaluator();
        View preloader = activity.findViewById(R.id.gradientPreloaderView);
        preloader.setVisibility(View.VISIBLE);
        final GradientDrawable gradient = (GradientDrawable) preloader.getBackground();

        ValueAnimator animator = TimeAnimator.ofFloat(0.0f, 1.0f);
        animator.setDuration(500);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setRepeatMode(ValueAnimator.REVERSE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                Float fraction = valueAnimator.getAnimatedFraction();
                int newStrat = (int) evaluator.evaluate(fraction, start, end);
                int newMid = (int) evaluator.evaluate(fraction, mid, start);
                int newEnd = (int) evaluator.evaluate(fraction, end, mid);
                int[] newArray = {newStrat, newMid, newEnd};
                gradient.setColors(newArray);
            }
        });

        animator.start();
    }




public static void stopAnimation(final int view, final Activity activity){

    ObjectAnimator.ofFloat(activity.findViewById(view), "alpha", 0f).setDuration(125).start();
}

where the view is a simple View with gradient background:

//gradient_preloader
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:startColor="#FDB72B"
        android:endColor="#00000000"
        android:angle="0"/>
</shape>

the view :

<View
    android:id="@+id/gradientPreloaderView"
    android:layout_width="match_parent"
    android:visibility="gone"
    android:layout_height="@dimen/basic_8_dp"
    android:background="@drawable/gradient_preloader" />

hope my answer will help

Agan answered 8/11, 2018 at 11:37 Comment(0)
O
3

The up to date alternative answer.

class GradientAnimationDrawable(
  start: Int = Color.rgb(0, 143, 209),
  center: Int = Color.rgb(1, 106, 154),
  end: Int = Color.rgb(28, 179, 249),
  frameDuration: Int = 3000,
  enterFadeDuration: Int = 0,
  exitFadeDuration: Int = 3000
) : AnimationDrawable() {

  private val gradientStart = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, intArrayOf(start, center, end))
    .apply {
      shape = GradientDrawable.RECTANGLE
      gradientType = GradientDrawable.LINEAR_GRADIENT
    }

  private val gradientCenter = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, intArrayOf(center, end, start))
    .apply {
      shape = GradientDrawable.RECTANGLE
      gradientType = GradientDrawable.LINEAR_GRADIENT
    }

  private val gradientEnd = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, intArrayOf(end, start, center))
    .apply {
      shape = GradientDrawable.RECTANGLE
      gradientType = GradientDrawable.LINEAR_GRADIENT
    }

  init {
    addFrame(gradientStart, frameDuration)
    addFrame(gradientCenter, frameDuration)
    addFrame(gradientEnd, frameDuration)
    setEnterFadeDuration(enterFadeDuration)
    setExitFadeDuration(exitFadeDuration)
    isOneShot = false
  }

}

Oat answered 2/8, 2019 at 11:19 Comment(1)
Can you write it in Java?Martial

© 2022 - 2024 — McMap. All rights reserved.