17

Has anyone looked into implementing the Loading images pattern from Google's latest Material Design guide.

It's a recommended way that "illustrations and photographs may load and transition in three phases at staggered durations". Those being Opacity, Exposure and Saturation:

enter image description here

I'm currently using the Volley NetworkImageView (actually a derived class from this).

I'm sure it's got to be some variant of the answer to this question. I'm just not sure which classes/code to use for both the saturation and animation curves that are described.

Jose Alcérreca
  • 1,809
  • 17
  • 20
David Crawford
  • 273
  • 2
  • 10
  • 1
    I know Google's material design effort should happen independent of code/implementation, but it sure would be nice to have a cookbook for these recommendations as a starting point! – bdalziel Dec 03 '14 at 19:58
  • 1
    David - your attempted edit to OneWay's answer should actually be its own answer. – admdrew Dec 04 '14 at 17:11
  • @admdrew I went ahead and did that. I can see reasons for wanting to have it either ways (full solution in a single answer; multiple solutions, depending on version, in separate answers). Why, exactly, do you think it should be it's own answer? – David Crawford Dec 04 '14 at 19:01
  • 1
    @DavidCrawford It certainly could be *part* of OneWay's answer, but that's for him/her to decide. Edits to posts should really only be for revising/improving the existing content, not to (effectively) add different content - so instead of making the edit, it could've been better to add a comment requesting the additional information be added. Also, what you posted is valuable, and seems different enough to warrant its own answer (and points!). – admdrew Dec 04 '14 at 19:22

4 Answers4

10

Thanks to @mttmllns! Previous Answer.

Since the previous answer shows an example written in C# and I was curious, I ported it to java. Complete GitHub Example

It outlines a 3-steps process where a combination of opacity, contrast/luminosity and saturation is used in concert to help salvage our poor users eyesight.

For a detailed explanation read this article.

EDIT:

See, the excellent answer provided by @DavidCrawford.

BTW: I fixed the linked GitHubProject to support pre-Lollipop devices. (Since API Level 11)

The Code

AlphaSatColorMatrixEvaluator.java

import android.animation.TypeEvaluator;
import android.graphics.ColorMatrix;

public class AlphaSatColorMatrixEvaluator implements TypeEvaluator {
    private ColorMatrix colorMatrix;
    float[] elements = new float[20];

    public AlphaSatColorMatrixEvaluator() {
        colorMatrix = new ColorMatrix ();
    }

    public ColorMatrix getColorMatrix() {
        return colorMatrix;
    }

    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        // There are 3 phases so we multiply fraction by that amount
        float phase = fraction * 3;

        // Compute the alpha change over period [0, 2]
        float alpha = Math.min(phase, 2f) / 2f;
        // elements [19] = (float)Math.round(alpha * 255);
        elements [18] = alpha;

        // We substract to make the picture look darker, it will automatically clamp
        // This is spread over period [0, 2.5]
        final int MaxBlacker = 100;
        float blackening = (float)Math.round((1 - Math.min(phase, 2.5f) / 2.5f) * MaxBlacker);
        elements [4] = elements [9] = elements [14] = -blackening;

        // Finally we desaturate over [0, 3], taken from ColorMatrix.SetSaturation
        float invSat = 1 - Math.max(0.2f, fraction);
        float R = 0.213f * invSat;
        float G = 0.715f * invSat;
        float B = 0.072f * invSat;

        elements[0] = R + fraction; elements[1] = G;            elements[2] = B;
        elements[5] = R;            elements[6] = G + fraction; elements[7] = B;
        elements[10] = R;           elements[11] = G;           elements[12] = B + fraction;

        colorMatrix.set(elements);
        return colorMatrix;
    }
}

Here is how you can set it up:

ImageView imageView = (ImageView)findViewById(R.id.imageView);
final BitmapDrawable drawable = (BitmapDrawable) getResources().getDrawable(R.drawable.image);
imageView.setImageDrawable(drawable);
AlphaSatColorMatrixEvaluator evaluator = new AlphaSatColorMatrixEvaluator ();
final ColorMatrixColorFilter filter = new ColorMatrixColorFilter(evaluator.getColorMatrix());
drawable.setColorFilter(filter);

ObjectAnimator animator = ObjectAnimator.ofObject(filter, "colorMatrix", evaluator, evaluator.getColorMatrix());

animator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        drawable.setColorFilter (filter);
    }
});
animator.setDuration(1500);
animator.start();

And here is the result:

enter image description here

Community
  • 1
  • 1
rnrneverdies
  • 15,243
  • 9
  • 65
  • 95
  • I've played around with this quite a bit, and have an issue I don't understand. I needed to implement material image loading in a way that the alpha, the saturation and the exposure components run in different time frames, so they start at the same time, but finish after one another. The problem is, if I only leave the exposure implementation in the evaluator class and solve the other two animation components in a separate way, the exposure won't be detectable. You can try it by only leaving the exposure related codes in the evaluator class and delete the rest. Any ideas? – Analizer Jul 21 '15 at 08:10
  • @Analizer I think you can try by playing with _phase_ var and their dependencies. Instead of starting in 0 of _fraction_, start at the point that you want. – rnrneverdies Jul 22 '15 at 01:56
  • I'm getting a crash when using this approach in a recycler view on some devices. I'm assuming its because the reference to the drawable. Any ideas on how to solve this? I was using the solution from http://stackoverflow.com/a/16936581/1296369 before and have no issues when scrolling. – kevskree Jul 29 '15 at 15:28
  • please see my answer below with a proposed fix to the transition. – Gil Moshayof Aug 11 '15 at 10:10
  • This code turns a transparent background for a png to black. Any fix? – Steve McMeen Nov 01 '17 at 06:56
8

Please note that this answer, as it stands, works for Lollipop only. The reason for this is because the colorMatrix property is not available to animate on the ColorMatrixColorFilter class (it doesn't provide getColorMatrix and setColorMatrix methods). To see this in action, try the code, in logcat output you should see a warning message like this:

Method setColorMatrix() with type class android.graphics.ColorMatrix not found on target class class android.graphics.ColorMatrixColorFilter

That being said, I was able to get this to work on older android versions (pre-Lollipop) by creating the following class (not the best name, I know)

private class AnimateColorMatrixColorFilter {
    private ColorMatrixColorFilter mFilter;
    private ColorMatrix mMatrix;

    public AnimateColorMatrixColorFilter(ColorMatrix matrix) {
        setColorMatrix(matrix);
    }

    public ColorMatrixColorFilter getColorFilter() {
        return mFilter;
    }

    public void setColorMatrix(ColorMatrix matrix) {
        mMatrix = matrix;
        mFilter = new ColorMatrixColorFilter(matrix);
    }

    public ColorMatrix getColorMatrix() {
        return mMatrix;
    }
}

Then, the setup code would look something like the following. Note that I have this "setup" in a derived class from ImageView and so I'm doing this in the overriden method setImageBitmap.

@Override
public void setImageBitmap(Bitmap bm) {
    final Drawable drawable = new BitmapDrawable(getContext().getResources(), bm);
    setImageDrawable(drawable);

    AlphaSatColorMatrixEvaluator evaluator = new AlphaSatColorMatrixEvaluator();
    final AnimateColorMatrixColorFilter filter = new AnimateColorMatrixColorFilter(evaluator.getColorMatrix());
    drawable.setColorFilter(filter.getColorFilter());

    ObjectAnimator animator = ObjectAnimator.ofObject(filter, "colorMatrix", evaluator, evaluator.getColorMatrix());

    animator.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            drawable.setColorFilter(filter.getColorFilter());
        }
    });
    animator.setDuration(1500);
    animator.start();
}
Community
  • 1
  • 1
David Crawford
  • 273
  • 2
  • 10
2

Following up on rnrneverdies's excellent answer, I'd like to offer a small fix to this animation logic.

My problem with this implementation is when it comes to png images with transparency (for example, circular images, or custom shapes). For these images, the colour filter will draw the transparency of the image as black, rather than just leaving them transparent.

The problem is with this line:

elements [19] = (float)Math.round(alpha * 255);

What's happening here is that the colour matrix is telling the bitmap that the alpha value of each pixels is equal to the current phase of the alpha. This is obviously not perfect, since pixels which were already transparent will lose their transparency and appear as black.

To fix this, instead of applying the alpha of the "additive" alpha field in the colour matrix, apply it on the "multiplicative" field:

Rm | 0  | 0  | 0  | Ra
0  | Gm | 0  | 0  | Ga
0  | 0  | Bm | 0  | Ba
0  | 0  | 0  | Am | Aa

Xm = multiplicative field
Xa = additive field

So instead of applying the alpha value on the "Aa" field (elements[19]), apply it on the "Am" field (elements[18]), and use the 0-1 value rather than the 0-255 value:

//elements [19] = (float)Math.round(alpha * 255);
elements [18] = alpha;

Now the transition will multiply the original alpha value of the bitmap with the alpha phase of the animation and not force an alpha value when there shouldn't be one.

Hope this helps

Community
  • 1
  • 1
Gil Moshayof
  • 16,633
  • 4
  • 47
  • 58
1

Was just wondering this same thing. I found a blog post detailing how to go about it with an example written in Xamarin:

http://blog.neteril.org/blog/2014/11/23/android-material-image-loading/

The gist for someone writing in Java: use ColorMatrix and ColorMatrixColorFilter.

The post also mentions an important optimization: using the Lollipop hidden setColorMatrix() API to avoid GC and GPU churn.

Have you seen it in use in any Google apps yet? If you end up implementing it I'd love to see your source.

mttmllns
  • 1,740
  • 1
  • 15
  • 13
  • From poking around it looks as if the Google I/O 2014 app is supposed to be the place where they are showing off this design and implementation. Since it was first released, they've gone back and update the app with new Lollipop code changes. That being said, they didn't actually implement their image fade in, per spec, as far as I can tell. – David Crawford Dec 04 '14 at 17:07