How to animate gradient?
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:
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) :
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