/* * 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.generic; import javax.annotation.Nullable; import android.annotation.SuppressLint; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.drawable.Animatable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import com.facebook.common.internal.Preconditions; import com.facebook.drawee.drawable.FadeDrawable; import com.facebook.drawee.drawable.ForwardingDrawable; import com.facebook.drawee.drawable.MatrixDrawable; import com.facebook.drawee.drawable.Rounded; import com.facebook.drawee.drawable.RoundedBitmapDrawable; import com.facebook.drawee.drawable.RoundedColorDrawable; import com.facebook.drawee.drawable.RoundedCornersDrawable; import com.facebook.drawee.drawable.ScaleTypeDrawable; import com.facebook.drawee.drawable.SettableDrawable; import com.facebook.drawee.drawable.VisibilityAwareDrawable; import com.facebook.drawee.drawable.VisibilityCallback; import com.facebook.drawee.interfaces.SettableDraweeHierarchy; import static com.facebook.drawee.drawable.ScalingUtils.ScaleType; /** * A SettableDraweeHierarchy that displays placeholder image until the actual image is set. * If provided, failure image will be used in case of failure (placeholder otherwise). * If provided, retry image will be used in case of failure when retrying is enabled. * If provided, progressbar will be displayed until fully loaded. * Each image can be displayed with a different scale type (or no scaling at all). * Fading between the layers is supported. * * <p> * Example hierarchy with placeholder, retry, failure and one actual image: * <pre> * o FadeDrawable (top level drawable) * | * +--o ScaleTypeDrawable * | | * | +--o Drawable (placeholder image) * | * +--o ScaleTypeDrawable * | | * | +--o SettableDrawable * | | * | +--o Drawable (actual image) * | * +--o ScaleTypeDrawable * | | * | +--o Drawable (retry image) * | * +--o ScaleTypeDrawable * | * +--o Drawable (failure image) * </pre> * * <p> * Note: * - ScaleType and Matrix transformations will be added only if specified. If both are unspecified, * then the branch for that image will be attached directly. * - It is not permitted to set both ScaleType transformation and Matrix transformation for the * same image. * - A Matrix transformation is only supported for actual image. * - All branches (placeholder, failure, retry, actual image, progressBar) are optional. * If some branch is not specified it won't be created. The exception is placeholder branch, * which will, if not specified, be created with a transparent drawable. * - If overlays and/or backgrounds are specified, they are added to the same fade drawable, and * are always displayed. * - Instance of some drawable should be used by only one DH. If more than one DH is being built * with the same builder, different drawable instances must be specified for each DH. */ public class GenericDraweeHierarchy implements SettableDraweeHierarchy { public static class RootDrawable extends ForwardingDrawable implements VisibilityAwareDrawable { @Nullable private VisibilityCallback mVisibilityCallback; public RootDrawable(Drawable drawable) { super(drawable); } @Override public int getIntrinsicWidth() { return -1; } @Override public int getIntrinsicHeight() { return -1; } @Override public void setVisibilityCallback(@Nullable VisibilityCallback visibilityCallback) { mVisibilityCallback = visibilityCallback; } @Override public boolean setVisible(boolean visible, boolean restart) { if (mVisibilityCallback != null) { mVisibilityCallback.onVisibilityChange(visible); } return super.setVisible(visible, restart); } @SuppressLint("WrongCall") @Override public void draw(Canvas canvas) { if (!isVisible()) { return; } if (mVisibilityCallback != null) { mVisibilityCallback.onDraw(); } super.draw(canvas); } } private Drawable mEmptyPlaceholderDrawable; private final Drawable mEmptyActualImageDrawable = new ColorDrawable(Color.TRANSPARENT); private final Drawable mEmptyControllerOverlayDrawable = new ColorDrawable(Color.TRANSPARENT); private final Resources mResources; private final RootDrawable mTopLevelDrawable; private final FadeDrawable mFadeDrawable; private final SettableDrawable mActualImageSettableDrawable; private final int mPlaceholderImageIndex; private final int mProgressBarImageIndex; private final int mActualImageIndex; private final int mRetryImageIndex; private final int mFailureImageIndex; private final int mControllerOverlayIndex; private RoundingParams mRoundingParams; GenericDraweeHierarchy(GenericDraweeHierarchyBuilder builder) { mResources = builder.getResources(); mRoundingParams = builder.getRoundingParams(); int numLayers = 0; // backgrounds int numBackgrounds = (builder.getBackgrounds() != null) ? builder.getBackgrounds().size() : 0; int backgroundsIndex = numLayers; numLayers += numBackgrounds; // placeholder image branch Drawable placeholderImageBranch = builder.getPlaceholderImage(); if (placeholderImageBranch == null) { placeholderImageBranch = getEmptyPlaceholderDrawable(); } placeholderImageBranch = maybeApplyRoundingBitmapOnly( mRoundingParams, mResources, placeholderImageBranch); placeholderImageBranch = maybeWrapWithScaleType( placeholderImageBranch, builder.getPlaceholderImageScaleType()); mPlaceholderImageIndex = numLayers++; // actual image branch Drawable actualImageBranch; mActualImageSettableDrawable = new SettableDrawable(mEmptyActualImageDrawable); actualImageBranch = mActualImageSettableDrawable; actualImageBranch = maybeWrapWithScaleType( actualImageBranch, builder.getActualImageScaleType(), builder.getActualImageFocusPoint()); actualImageBranch = maybeWrapWithMatrix( actualImageBranch, builder.getActualImageMatrix()); actualImageBranch.setColorFilter(builder.getActualImageColorFilter()); mActualImageIndex = numLayers++; // progressBar image branch Drawable progressBarImageBranch = builder.getProgressBarImage(); mProgressBarImageIndex = numLayers++; if (progressBarImageBranch != null) { progressBarImageBranch = maybeWrapWithScaleType( progressBarImageBranch, builder.getProgressBarImageScaleType()); } // retry image branch Drawable retryImageBranch = builder.getRetryImage(); mRetryImageIndex = numLayers++; if (retryImageBranch != null) { retryImageBranch = maybeWrapWithScaleType( retryImageBranch, builder.getRetryImageScaleType()); } // failure image branch Drawable failureImageBranch = builder.getFailureImage(); mFailureImageIndex = numLayers++; if (failureImageBranch != null) { failureImageBranch = maybeWrapWithScaleType( failureImageBranch, builder.getFailureImageScaleType()); } // overlays int overlaysIndex = numLayers; int numOverlays = ((builder.getOverlays() != null) ? builder.getOverlays().size() : 0) + ((builder.getPressedStateOverlay() != null) ? 1 : 0); numLayers += numOverlays; // controller overlay mControllerOverlayIndex = numLayers++; // array of layers Drawable[] layers = new Drawable[numLayers]; if (numBackgrounds > 0) { int index = 0; for (Drawable background : builder.getBackgrounds()) { layers[backgroundsIndex + index++] = maybeApplyRoundingBitmapOnly(mRoundingParams, mResources, background); } } layers[mPlaceholderImageIndex] = placeholderImageBranch; layers[mActualImageIndex] = actualImageBranch; layers[mProgressBarImageIndex] = progressBarImageBranch; layers[mRetryImageIndex] = retryImageBranch; layers[mFailureImageIndex] = failureImageBranch; if (numOverlays > 0) { int index = 0; if (builder.getOverlays() != null) { for (Drawable overlay : builder.getOverlays()) { layers[overlaysIndex + index++] = overlay; } } if (builder.getPressedStateOverlay() != null) { layers[overlaysIndex + index++] = builder.getPressedStateOverlay(); } } if (mControllerOverlayIndex >= 0) { layers[mControllerOverlayIndex] = mEmptyControllerOverlayDrawable; } // fade drawable composed of branches mFadeDrawable = new FadeDrawable(layers); mFadeDrawable.setTransitionDuration(builder.getFadeDuration()); // rounded corners drawable (optional) Drawable maybeRoundedDrawable = maybeWrapWithRoundedOverlayColor(mRoundingParams, mFadeDrawable); // top-level drawable mTopLevelDrawable = new RootDrawable(maybeRoundedDrawable); mTopLevelDrawable.mutate(); resetFade(); } private static Drawable maybeWrapWithScaleType( Drawable drawable, @Nullable ScaleType scaleType) { return maybeWrapWithScaleType(drawable, scaleType, null); } private static Drawable maybeWrapWithScaleType( Drawable drawable, @Nullable ScaleType scaleType, @Nullable PointF focusPoint) { Preconditions.checkNotNull(drawable); if (scaleType == null) { return drawable; } ScaleTypeDrawable scaleTypeDrawable = new ScaleTypeDrawable(drawable, scaleType); if (focusPoint != null) { scaleTypeDrawable.setFocusPoint(focusPoint); } return scaleTypeDrawable; } private static Drawable maybeWrapWithMatrix( Drawable drawable, @Nullable Matrix matrix) { Preconditions.checkNotNull(drawable); if (matrix == null) { return drawable; } return new MatrixDrawable(drawable, matrix); } private static void applyRoundingParams(Rounded rounded, RoundingParams roundingParams) { rounded.setCircle(roundingParams.getRoundAsCircle()); rounded.setRadii(roundingParams.getCornersRadii()); rounded.setBorder( roundingParams.getBorderColor(), roundingParams.getBorderWidth()); } private static Drawable maybeWrapWithRoundedOverlayColor( @Nullable RoundingParams roundingParams, Drawable drawable) { if (roundingParams != null && roundingParams.getRoundingMethod() == RoundingParams.RoundingMethod.OVERLAY_COLOR) { RoundedCornersDrawable roundedCornersDrawable = new RoundedCornersDrawable(drawable); applyRoundingParams(roundedCornersDrawable, roundingParams); roundedCornersDrawable.setOverlayColor(roundingParams.getOverlayColor()); return roundedCornersDrawable; } else { return drawable; } } private static Drawable maybeApplyRoundingBitmapOnly( @Nullable RoundingParams roundingParams, Resources resources, Drawable drawable) { if (roundingParams == null || roundingParams.getRoundingMethod() != RoundingParams.RoundingMethod.BITMAP_ONLY) { return drawable; } if (drawable instanceof BitmapDrawable || drawable instanceof ColorDrawable) { return applyRounding(roundingParams, resources, drawable); } else { Drawable parent = drawable; Drawable child = parent.getCurrent(); while (child != null && parent != child) { if (parent instanceof ForwardingDrawable && (child instanceof BitmapDrawable || child instanceof ColorDrawable)) { ((ForwardingDrawable) parent).setCurrent( applyRounding(roundingParams, resources, child)); } parent = child; child = parent.getCurrent(); } } return drawable; } private static Drawable applyRounding( @Nullable RoundingParams roundingParams, Resources resources, Drawable drawable) { if (drawable instanceof BitmapDrawable) { RoundedBitmapDrawable roundedBitmapDrawable = RoundedBitmapDrawable.fromBitmapDrawable(resources, (BitmapDrawable) drawable); applyRoundingParams(roundedBitmapDrawable, roundingParams); return roundedBitmapDrawable; } if (drawable instanceof ColorDrawable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { RoundedColorDrawable roundedColorDrawable = RoundedColorDrawable.fromColorDrawable((ColorDrawable) drawable); applyRoundingParams(roundedColorDrawable, roundingParams); return roundedColorDrawable; } return drawable; } private void resetActualImages() { if (mActualImageSettableDrawable != null) { mActualImageSettableDrawable.setDrawable(mEmptyActualImageDrawable); } } private void resetFade() { if (mFadeDrawable != null) { mFadeDrawable.beginBatchMode(); // turn on all layers (backgrounds, branches, overlays) mFadeDrawable.fadeInAllLayers(); // turn off branches (leaving backgrounds and overlays on) fadeOutBranches(); // turn on placeholder fadeInLayer(mPlaceholderImageIndex); mFadeDrawable.finishTransitionImmediately(); mFadeDrawable.endBatchMode(); } } private void fadeOutBranches() { fadeOutLayer(mPlaceholderImageIndex); fadeOutLayer(mActualImageIndex); fadeOutLayer(mProgressBarImageIndex); fadeOutLayer(mRetryImageIndex); fadeOutLayer(mFailureImageIndex); } private void fadeInLayer(int index) { if (index >= 0) { mFadeDrawable.fadeInLayer(index); } } private void fadeOutLayer(int index) { if (index >= 0) { mFadeDrawable.fadeOutLayer(index); } } private void setProgress(float progress) { Drawable progressBarDrawable = getLayerChildDrawable(mProgressBarImageIndex); if (progressBarDrawable == null) { return; } // display progressbar when not fully loaded, hide otherwise if (progress >= 0.999f) { if (progressBarDrawable instanceof Animatable) { ((Animatable) progressBarDrawable).stop(); } fadeOutLayer(mProgressBarImageIndex); } else { if (progressBarDrawable instanceof Animatable) { ((Animatable) progressBarDrawable).start(); } fadeInLayer(mProgressBarImageIndex); } // set drawable level, scaled to [0, 10000] per drawable specification progressBarDrawable.setLevel(Math.round(progress * 10000)); } // SettableDraweeHierarchy interface @Override public Drawable getTopLevelDrawable() { return mTopLevelDrawable; } @Override public void reset() { resetActualImages(); resetFade(); } @Override public void setImage(Drawable drawable, float progress, boolean immediate) { drawable = maybeApplyRoundingBitmapOnly(mRoundingParams, mResources, drawable); drawable.mutate(); mActualImageSettableDrawable.setDrawable(drawable); mFadeDrawable.beginBatchMode(); fadeOutBranches(); fadeInLayer(mActualImageIndex); setProgress(progress); if (immediate) { mFadeDrawable.finishTransitionImmediately(); } mFadeDrawable.endBatchMode(); } @Override public void setProgress(float progress, boolean immediate) { mFadeDrawable.beginBatchMode(); setProgress(progress); if (immediate) { mFadeDrawable.finishTransitionImmediately(); } mFadeDrawable.endBatchMode(); } @Override public void setFailure(Throwable throwable) { mFadeDrawable.beginBatchMode(); fadeOutBranches(); if (mFadeDrawable.getDrawable(mFailureImageIndex) != null) { fadeInLayer(mFailureImageIndex); } else { fadeInLayer(mPlaceholderImageIndex); } mFadeDrawable.endBatchMode(); } @Override public void setRetry(Throwable throwable) { mFadeDrawable.beginBatchMode(); fadeOutBranches(); if (mFadeDrawable.getDrawable(mRetryImageIndex) != null) { fadeInLayer(mRetryImageIndex); } else { fadeInLayer(mPlaceholderImageIndex); } mFadeDrawable.endBatchMode(); } @Override public void setControllerOverlay(@Nullable Drawable drawable) { if (drawable == null) { drawable = mEmptyControllerOverlayDrawable; } mFadeDrawable.setDrawable(mControllerOverlayIndex, drawable); } public void setFadeDuration(int durationMs) { mFadeDrawable.setTransitionDuration(durationMs); } // Helper methods for accessing layers /** * Gets the drawable at the specified index while skipping MatrixDrawable and ScaleTypeDrawable. * * <p> If <code>returnParent</code> is set, parent drawable will be returned instead. If * MatrixDrawable or ScaleTypeDrawable is found at that index, it will be returned as a parent. * Otherwise, the FadeDrawable will be returned as a parent. */ private Drawable getLayerDrawable(int index, boolean returnParent) { Drawable parent = mFadeDrawable; Drawable child = mFadeDrawable.getDrawable(index); if (child instanceof MatrixDrawable) { parent = child; child = parent.getCurrent(); } if (child instanceof ScaleTypeDrawable) { parent = child; child = parent.getCurrent(); } return returnParent ? parent : child; } /** * Returns the ScaleTypeDrawable at the specified index, or null if not found. */ private @Nullable ScaleTypeDrawable findLayerScaleTypeDrawable(int index) { Drawable drawable = mFadeDrawable.getDrawable(index); if (drawable instanceof MatrixDrawable) { drawable = drawable.getCurrent(); } if (drawable instanceof ScaleTypeDrawable) { return (ScaleTypeDrawable) drawable; } else { return null; } } /** * Sets a child drawable at the specified index. * * <p> Note: This uses {@link #getLayerDrawable} to find the parent drawable. Given drawable is * then set as its child. */ private void setLayerChildDrawable(int index, Drawable drawable) { Drawable parent = getLayerDrawable(index, true /* returnParent */); if (parent == mFadeDrawable) { mFadeDrawable.setDrawable(index, drawable); } else { ((ForwardingDrawable) parent).setCurrent(drawable); } } /** * Gets the child drawable at the specified index. */ private Drawable getLayerChildDrawable(int index) { return getLayerDrawable(index, false /* returnParent */); } private Drawable getEmptyPlaceholderDrawable() { if (mEmptyPlaceholderDrawable == null) { mEmptyPlaceholderDrawable = new ColorDrawable(Color.TRANSPARENT); } return mEmptyPlaceholderDrawable; } // Mutability /** Sets the actual image focus point. */ public void setActualImageFocusPoint(PointF focusPoint) { Preconditions.checkNotNull(focusPoint); ScaleTypeDrawable scaleTypeDrawable = findLayerScaleTypeDrawable(mActualImageIndex); if (scaleTypeDrawable == null) { throw new UnsupportedOperationException("ScaleTypeDrawable not found!"); } scaleTypeDrawable.setFocusPoint(focusPoint); } /** Sets the actual image scale type. */ public void setActualImageScaleType(ScaleType scaleType) { Preconditions.checkNotNull(scaleType); ScaleTypeDrawable scaleTypeDrawable = findLayerScaleTypeDrawable(mActualImageIndex); if (scaleTypeDrawable == null) { throw new UnsupportedOperationException("ScaleTypeDrawable not found!"); } scaleTypeDrawable.setScaleType(scaleType); } /** Sets the color filter to be applied on the actual image. */ public void setActualImageColorFilter(ColorFilter colorfilter) { mFadeDrawable.getDrawable(mActualImageIndex).setColorFilter(colorfilter); } /** * Gets the post-scaling bounds of the actual image. * * <p> Note: the returned bounds are not cropped. * @param outBounds rect to fill with bounds */ public void getActualImageBounds(RectF outBounds) { mActualImageSettableDrawable.getTransformedBounds(outBounds); } /** * Sets a new placeholder drawable. * * <p>The placeholder scale type will not be changed. */ public void setPlaceholderImage(Drawable drawable) { setPlaceholderImage(drawable, null); } public void setPlaceholderImage(@Nullable Drawable drawable, @Nullable ScaleType scaleType) { if (drawable == null) { drawable = getEmptyPlaceholderDrawable(); } setDrawableAndScaleType(drawable, scaleType, mPlaceholderImageIndex); } public void setPlaceholderImageFocusPoint(PointF focusPoint) { Preconditions.checkNotNull(focusPoint); ScaleTypeDrawable scaleTypeDrawable = findLayerScaleTypeDrawable(mPlaceholderImageIndex); if (scaleTypeDrawable == null) { throw new UnsupportedOperationException("ScaleTypeDrawable not found!"); } scaleTypeDrawable.setFocusPoint(focusPoint); } /** * Sets a new placeholder drawable using the supplied resource ID. * * <p>The placeholder scale type will not be changed. * @param resourceId an identifier of an Android drawable or color resource. */ public void setPlaceholderImage(int resourceId) { setPlaceholderImage(mResources.getDrawable(resourceId)); } /** * Sets a new failure drawable. */ public void setFailureImage(Drawable drawable) { setFailureImage(drawable, null); } /** * Sets a new failure drawable and scale type. */ public void setFailureImage(@Nullable Drawable drawable, @Nullable ScaleType scaleType) { setDrawableAndScaleType(drawable, scaleType, mFailureImageIndex); } /** * Sets a new retry drawable. */ public void setRetryImage(Drawable drawable) { setRetryImage(drawable, null); } /** * Sets a new retry drawable and scale type. */ public void setRetryImage(@Nullable Drawable drawable, @Nullable ScaleType scaleType) { setDrawableAndScaleType(drawable, scaleType, mRetryImageIndex); } /** * Sets a new progress bar drawable. */ public void setProgressBarImage(Drawable drawable) { setProgressBarImage(drawable, null); } /** * Sets a new progress bar drawable and scale type. */ public void setProgressBarImage(@Nullable Drawable drawable, @Nullable ScaleType scaleType) { setDrawableAndScaleType(drawable, scaleType, mProgressBarImageIndex); } private void setDrawableAndScaleType( @Nullable Drawable drawable, @Nullable ScaleType scaleType, int index) { if (drawable == null) { mFadeDrawable.setDrawable(index, null); return; } drawable = maybeApplyRoundingBitmapOnly(mRoundingParams, mResources, drawable); if (scaleType != null) { ScaleTypeDrawable scaleTypeDrawable = findLayerScaleTypeDrawable(index); if (scaleTypeDrawable != null) { scaleTypeDrawable.setScaleType(scaleType); } else { drawable = maybeWrapWithScaleType(drawable, scaleType); } } setLayerChildDrawable(index, drawable); } /** * Sets the rounding params. */ public void setRoundingParams(RoundingParams roundingParams) { mRoundingParams = roundingParams; updateOverlayColorRounding(); updateBitmapOnlyRounding(); } private void updateOverlayColorRounding() { Drawable topDrawableChild = mTopLevelDrawable.getCurrent(); if (mRoundingParams != null && mRoundingParams.getRoundingMethod() == RoundingParams.RoundingMethod.OVERLAY_COLOR) { // Overlay rounding requested - either update the overlay params or add a new // drawable that will do the requested rounding. if (topDrawableChild instanceof RoundedCornersDrawable) { RoundedCornersDrawable roundedCornersDrawable = (RoundedCornersDrawable) topDrawableChild; applyRoundingParams(roundedCornersDrawable, mRoundingParams); roundedCornersDrawable.setOverlayColor(mRoundingParams.getOverlayColor()); } else { // important: remove the child before wrapping it with a new parent! topDrawableChild = mTopLevelDrawable.setCurrent(mEmptyActualImageDrawable); topDrawableChild = maybeWrapWithRoundedOverlayColor(mRoundingParams, topDrawableChild); mTopLevelDrawable.setCurrent(topDrawableChild); } } else if (topDrawableChild instanceof RoundedCornersDrawable) { // Overlay rounding no longer required so remove drawable that was doing the rounding. RoundedCornersDrawable roundedCornersDrawable = (RoundedCornersDrawable) topDrawableChild; // Extract the drawable out of roundedCornersDrawable before setting it to a new parent. topDrawableChild = roundedCornersDrawable.setCurrent(mEmptyActualImageDrawable); mTopLevelDrawable.setCurrent(topDrawableChild); } } private void updateBitmapOnlyRounding() { if (mRoundingParams != null && mRoundingParams.getRoundingMethod() == RoundingParams.RoundingMethod.BITMAP_ONLY) { // Bitmap rounding - either update the params or wrap the current drawable in a drawable // that will round it. for (int i = 0; i < mFadeDrawable.getNumberOfLayers(); i++) { Drawable layer = getLayerChildDrawable(i); if (layer instanceof Rounded) { Rounded rounded = (Rounded) layer; applyRoundingParams(rounded, mRoundingParams); } else if (layer != null) { // important: remove the child before wrapping it with a new parent! setLayerChildDrawable(i, mEmptyActualImageDrawable); Drawable roundedLayer = maybeApplyRoundingBitmapOnly(mRoundingParams, mResources, layer); setLayerChildDrawable(i, roundedLayer); } } } else { // No bitmap rounding requested - reset all layers so no rounding occurs. for (int i = 0; i < mFadeDrawable.getNumberOfLayers(); i++) { final Drawable layer = getLayerChildDrawable(i); if (layer instanceof Rounded) { resetRoundedDrawable((Rounded) layer); } } } } private static void resetRoundedDrawable(Rounded rounded) { rounded.setCircle(false); rounded.setRadius(0); rounded.setBorder(Color.TRANSPARENT, 0); } /** * Gets the rounding params. * @return rounding params */ public RoundingParams getRoundingParams() { return mRoundingParams; } }