Skip to content

Instantly share code, notes, and snippets.

@FrancoisBlavoet
Last active November 17, 2020 07:03
Show Gist options
  • Save FrancoisBlavoet/c34f5ee555ebb8f05add11dd8141e322 to your computer and use it in GitHub Desktop.
Save FrancoisBlavoet/c34f5ee555ebb8f05add11dd8141e322 to your computer and use it in GitHub Desktop.
Correct color interpolation

Android's framework already gives us ArgbEvaluator to animate between two colors. However, it does so with a linear interpolation on the ARGB components of the colors.
Since the RGB components are gamma encoded, a linear interpolation leads to some unseemly color jumps.

We can solve this with an interpolator which convert the start & end color to linear values, interpolate them and then brings them back to gamma-encoded values.

Credits to Romain Guy for the base implementation.

import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import static java.lang.Math.pow;
public class GammaEvaluator implements TypeEvaluator {
private static final GammaEvaluator instance = new GammaEvaluator();
/**
* Returns an instance of <code>GammaEvaluator</code> that may be used in
* {@link ValueAnimator#setEvaluator(TypeEvaluator)}. The same instance may
* be used in multiple <code>Animator</code>s because it holds no state.
*
* @return An instance of <code>GammaEvaluator</code>.
*/
public static GammaEvaluator getInstance() {
return instance;
}
public Object evaluate(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
float startA = ((startInt >> 24) & 0xff) / 255.0f;
float startR = ((startInt >> 16) & 0xff) / 255.0f;
float startG = ((startInt >> 8) & 0xff) / 255.0f;
float startB = (startInt & 0xff) / 255.0f;
int endInt = (Integer) endValue;
float endA = ((endInt >> 24) & 0xff) / 255.0f;
float endR = ((endInt >> 16) & 0xff) / 255.0f;
float endG = ((endInt >> 8) & 0xff) / 255.0f;
float endB = (endInt & 0xff) / 255.0f;
// convert from sRGB to linear
startR = EOCF_sRGB(startR);
startG = EOCF_sRGB(startG);
startB = EOCF_sRGB(startB);
endR = EOCF_sRGB(endR);
endG = EOCF_sRGB(endG);
endB = EOCF_sRGB(endB);
// compute the interpolated color in linear space
float a = startA + fraction * (endA - startA);
float r = startR + fraction * (endR - startR);
float g = startG + fraction * (endG - startG);
float b = startB + fraction * (endB - startB);
// convert back to sRGB in the [0..255] range
a = a * 255.0f;
r = OECF_sRGB(r) * 255.0f;
g = OECF_sRGB(g) * 255.0f;
b = OECF_sRGB(b) * 255.0f;
return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
}
/**
* Opto-electronic conversion function for the sRGB color space
* Takes a gamma-encoded sRGB value and converts it to a linear sRGB value
*/
public static float OECF_sRGB(float linear) {
// IEC 61966-2-1:1999
return linear <= 0.0031308f ?
linear * 12.92f : (float) ((pow(linear, 1.0f / 2.4f) * 1.055f) - 0.055f);
}
/**
* Electro-optical conversion function for the sRGB color space
* Takes a linear sRGB value and converts it to a gamma-encoded sRGB value
*/
public static float EOCF_sRGB(float srgb) {
// IEC 61966-2-1:1999
return srgb <= 0.04045f ? srgb / 12.92f : (float) pow((srgb + 0.055f) / 1.055f, 2.4f);
}
}
import android.animation.ValueAnimator;
import android.graphics.drawable.ColorDrawable;
import android.support.annotation.ColorInt;
public class Sample {
public void animate(final ColorDrawable colorDrawable,
@ColorInt int colorFrom,
@ColorInt int colorTo) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override public void onAnimationUpdate(ValueAnimator animation) {
colorDrawable.setColor((Integer) animation.getAnimatedValue());
}
});
valueAnimator.setIntValues(colorFrom, colorTo);
valueAnimator.setEvaluator(GammaEvaluator.getInstance());
valueAnimator.start();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment