Friday, March 8, 2013

Glow effect

The task to accomplish is to make a bit of colour animation on a button (or practically any view), when a defined event happens (lats say, when a "flying" list item flies to it). The animation should be a growing-then-declining glow effect.
So the background should be animated from the current state (which is #ff333333)
to change its color to #ff3355dd within 200ms
than change its color to #ff333333 again within 200ms.

Using Android Honeycomb this can easily done with property animation:
private static final int GLOW_ANIM_DURATION = 400;
    private void tabGlowAnimation(View targetTab) {
        final ObjectAnimator objAnim =
                ObjectAnimator.ofObject(targetTab,
                                        "backgroundColor", // we want to modify the backgroundColor
                                        new ArgbEvaluator(), // this can be used to interpolate between two color values
                                        targetTab.getContext().getResources().getColor(R.color.tab_background), // start color defined in resources as #ff333333
                                        targetTab.getContext().getResources().getColor(R.color.tab_glow) // end color defined in resources as #ff3355dd
                );
        objAnim.setDuration(GLOW_ANIM_DURATION / 2);
        objAnim.setRepeatMode(ValueAnimator.REVERSE); // start reverse animation after the "growing" phase
        objAnim.setRepeatCount(1);
        objAnim.start();
    }

This is very convenient. However it still has 2 problems:
  • It doesn't work on preHC devices
  • the start color is predefined during coding time (which is not a problem in this case). It is not easy to determine the color of the background, because the background is a drawable and not a color at this point. And/or you needed to implement a custom Evaluator

Unfortunately on earlier devices it is - lets say - challenging to implement a same effect. My solution is an intermittent view layer between the foreground and background of the button. This view has a custom drawable, which alpha value is animated as required. Actually with this we can have any fancy glow (circular, tiger shaped, etc.). I used an idea found in TransitionDrawable.java.

I've seen this post: http://nathanael.hevenet.com/android-dev-fading-background-animation/, but since I tried to use it on a tabview, it messed up the layout.

public class GlowEffect implements Runnable {
    private static final int MAX_ALPHA = 255;
    /**
     * time interval to reschedule a redraw.
     */
    private static final int MIN_TICK_MILLIS = 30;
    /**
     * The alpha of the drawable of this view will be adjusted for the glow effect
     */
    private View mGlowLayer;

    /**
     * the time, when the animation started. (Or should have started if we restart the glow animation)
     */
    private long mStartTimeMillis;
    /**
     * duration of the whole animation = glow increase + glow decrease
     */
    private int mDuration;
    /**
     * inner representation of the required alpha value. Used when the animation restarts.
     * We have to store it, because Drawable.getAlpha() doesn't exist.
     */
    private double mAlpha;
    /**
     * The time between each animation steps.
     */
    private int mTickMillis;

    public GlowEffect() {
    }

    public void glow(View glowLayer, int durationMillis) {
        mGlowLayer = glowLayer;
        // correct duration time if the animation has been restarted and the previous is still in progress
        // we continue the animation from that point
        final long suspectedTimeFromStart = Math.round(mAlpha * mDuration / 2);
        mDuration = durationMillis;

        mStartTimeMillis = SystemClock.uptimeMillis() - suspectedTimeFromStart;
        mGlowLayer.setVisibility(View.VISIBLE);
        mGlowLayer.getBackground().setAlpha(0);
        mTickMillis = Math.max(MIN_TICK_MILLIS, durationMillis / MAX_ALPHA);
        mGlowLayer.postDelayed(this, mTickMillis);
    }

    @Override
    public void run() {
        boolean done = true;
        final Drawable who = mGlowLayer.getBackground();

        float normalized = (float) (SystemClock.uptimeMillis() - mStartTimeMillis) / mDuration;
        normalized = Math.min(normalized, 1.0f);
        done = normalized >= 1.0f;

        // linear increase for half of the duration, than linear decrease
        //noinspection MagicNumber
        mAlpha = (0.5 - Math.abs(normalized - 0.5f)) * 2.0f;
        int alpha = (int)Math.round(mAlpha * MAX_ALPHA);

        alpha = Math.max(0, alpha);
        alpha = Math.min(MAX_ALPHA, alpha);

        who.setAlpha(alpha);
        who.invalidateSelf();

        if (!done) {
            mGlowLayer.postDelayed(this, mTickMillis);
        } else {
            mGlowLayer.setVisibility(View.GONE);
        }
    }

}

Whenever a glow is needed, the glow() should be called.


No comments: