/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.drawee.drawable; import java.util.Arrays; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.os.SystemClock; import com.facebook.common.internal.Preconditions; import com.facebook.common.internal.VisibleForTesting; /** * A drawable that fades to the specific layer. * * <p> Arbitrary number of layers is supported. 5 Different fade methods are supported. * Once the transition starts we will animate layers in or out based on used fade method. * fadeInLayer fades in specified layer to full opacity. * fadeOutLayer fades out specified layer to zero opacity. * fadeOutAllLayers fades out all layers to zero opacity. * fadeToLayer fades in specified layer to full opacity, fades out all other layers to zero opacity. * fadeUpToLayer fades in all layers up to specified layer to full opacity and * fades out all other layers to zero opacity. * */ public class FadeDrawable extends ArrayDrawable { /** * A transition is about to start. */ @VisibleForTesting public static final int TRANSITION_STARTING = 0; /** * The transition has started and the animation is in progress. */ @VisibleForTesting public static final int TRANSITION_RUNNING = 1; /** * No transition will be applied. */ @VisibleForTesting public static final int TRANSITION_NONE = 2; /** * Layers. */ private final Drawable[] mLayers; /** * The current state. */ @VisibleForTesting int mTransitionState; @VisibleForTesting int mDurationMs; @VisibleForTesting long mStartTimeMs; @VisibleForTesting int[] mStartAlphas; @VisibleForTesting int[] mAlphas; @VisibleForTesting int mAlpha; /** * Determines whether to fade-out a layer to zero opacity (false) or to fade-in to * the full opacity (true) */ @VisibleForTesting boolean[] mIsLayerOn; /** * When in batch mode, drawable won't invalidate self until batch mode finishes. */ @VisibleForTesting int mPreventInvalidateCount; /** * Creates a new fade drawable. * The first layer is displayed with full opacity whereas all other layers are invisible. * @param layers layers to fade between */ public FadeDrawable(Drawable[] layers) { super(layers); Preconditions.checkState(layers.length >= 1, "At least one layer required!"); mLayers = layers; mStartAlphas = new int[layers.length]; mAlphas = new int[layers.length]; mAlpha = 255; mIsLayerOn = new boolean[layers.length]; mPreventInvalidateCount = 0; resetInternal(); } @Override public void invalidateSelf() { if (mPreventInvalidateCount == 0) { super.invalidateSelf(); } } /** * Begins the batch mode so that it doesn't invalidate self on every operation. */ public void beginBatchMode() { mPreventInvalidateCount++; } /** * Ends the batch mode and invalidates. */ public void endBatchMode() { mPreventInvalidateCount--; invalidateSelf(); } /** * Sets the duration of the current transition in milliseconds. */ public void setTransitionDuration(int durationMs) { mDurationMs = durationMs; // re-initialize transition if it's running if (mTransitionState == TRANSITION_RUNNING) { mTransitionState = TRANSITION_STARTING; } } /** * Gets the transition duration. * @return transition duration in milliseconds. */ public int getTransitionDuration() { return mDurationMs; } /** * Resets internal state to the initial state. */ private void resetInternal() { mTransitionState = TRANSITION_NONE; Arrays.fill(mStartAlphas, 0); mStartAlphas[0] = 255; Arrays.fill(mAlphas, 0); mAlphas[0] = 255; Arrays.fill(mIsLayerOn, false); mIsLayerOn[0] = true; } /** * Resets to the initial state. */ public void reset() { resetInternal(); invalidateSelf(); } /** * Starts fading in the specified layer. * @param index the index of the layer to fade in. */ public void fadeInLayer(int index) { mTransitionState = TRANSITION_STARTING; mIsLayerOn[index] = true; invalidateSelf(); } /** * Starts fading out the specified layer. * @param index the index of the layer to fade out. */ public void fadeOutLayer(int index) { mTransitionState = TRANSITION_STARTING; mIsLayerOn[index] = false; invalidateSelf(); } /** * Starts fading in all layers. */ public void fadeInAllLayers() { mTransitionState = TRANSITION_STARTING; Arrays.fill(mIsLayerOn, true); invalidateSelf(); } /** * Starts fading out all layers. */ public void fadeOutAllLayers() { mTransitionState = TRANSITION_STARTING; Arrays.fill(mIsLayerOn, false); invalidateSelf(); } /** * Starts fading to the specified layer. * @param index the index of the layer to fade to */ public void fadeToLayer(int index) { mTransitionState = TRANSITION_STARTING; Arrays.fill(mIsLayerOn, false); mIsLayerOn[index] = true; invalidateSelf(); } /** * Starts fading up to the specified layer. * <p> * Layers up to the specified layer inclusive will fade in, other layers will fade out. * @param index the index of the layer to fade up to. */ public void fadeUpToLayer(int index) { mTransitionState = TRANSITION_STARTING; Arrays.fill(mIsLayerOn, 0, index + 1, true); Arrays.fill(mIsLayerOn, index + 1, mLayers.length, false); invalidateSelf(); } /** * Finishes transition immediately. */ public void finishTransitionImmediately() { mTransitionState = TRANSITION_NONE; for (int i = 0; i < mLayers.length; i++) { mAlphas[i] = mIsLayerOn[i] ? 255 : 0; } invalidateSelf(); } /** * Updates the current alphas based on the ratio of the elapsed time and duration. * @param ratio * @return whether the all layers have reached their target opacity */ private boolean updateAlphas(float ratio) { boolean done = true; for (int i = 0; i < mLayers.length; i++) { int dir = mIsLayerOn[i] ? +1 : -1; // determines alpha value and clamps it to [0, 255] mAlphas[i] = (int) (mStartAlphas[i] + dir * 255 * ratio); if (mAlphas[i] < 0) { mAlphas[i] = 0; } if (mAlphas[i] > 255) { mAlphas[i] = 255; } // determines whether the layer has reached its target opacity if (mIsLayerOn[i] && mAlphas[i] < 255) { done = false; } if (!mIsLayerOn[i] && mAlphas[i] > 0) { done = false; } } return done; } @Override public void draw(Canvas canvas) { boolean done = true; float ratio; switch (mTransitionState) { case TRANSITION_STARTING: // initialize start alphas and start time System.arraycopy(mAlphas, 0, mStartAlphas, 0, mLayers.length); mStartTimeMs = getCurrentTimeMs(); // if the duration is 0, update alphas to the target opacities immediately ratio = (mDurationMs == 0) ? 1.0f : 0.0f; // if all the layers have reached their target opacity, transition is done done = updateAlphas(ratio); mTransitionState = done ? TRANSITION_NONE : TRANSITION_RUNNING; break; case TRANSITION_RUNNING: Preconditions.checkState(mDurationMs > 0); // determine ratio based on the elapsed time ratio = (float) (getCurrentTimeMs() - mStartTimeMs) / mDurationMs; // if all the layers have reached their target opacity, transition is done done = updateAlphas(ratio); mTransitionState = done ? TRANSITION_NONE : TRANSITION_RUNNING; break; case TRANSITION_NONE: // there is no transition in progress and mAlphas should be left as is. done = true; break; } for (int i = 0; i < mLayers.length; i++) { drawDrawableWithAlpha(canvas, mLayers[i], mAlphas[i] * mAlpha / 255); } if (!done) { invalidateSelf(); } } private void drawDrawableWithAlpha(Canvas canvas, Drawable drawable, int alpha) { if (alpha > 0) { mPreventInvalidateCount++; drawable.mutate().setAlpha(alpha); mPreventInvalidateCount--; drawable.draw(canvas); } } @Override public void setAlpha(int alpha) { if (mAlpha != alpha) { mAlpha = alpha; invalidateSelf(); } } public int getAlpha() { return mAlpha; } /** * Returns current time. Absolute reference is not important as only time deltas are used. * Extracting this to a separate method allows better testing. * @return current time in milliseconds */ protected long getCurrentTimeMs() { return SystemClock.uptimeMillis(); } /** * Gets the transition state (STARTING, RUNNING, NONE). * Useful for testing purposes. * @return transition state */ @VisibleForTesting public int getTransitionState() { return mTransitionState; } public boolean isLayerOn(int index) { return mIsLayerOn[index]; } }