/* * 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.content.res.Resources; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.PointF; 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.DrawableParent; 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 static com.facebook.drawee.drawable.ScalingUtils.ScaleType; /** * A class that contains helper methods for wrapping and rounding. */ public class WrappingUtils { // Empty drawable to be temporarily used for hierarchy manipulations. // // Since drawables are allowed to have at most one parent, and this is a static instance, this // drawable may only be used temporarily while carrying some hierarchy manipulations. After those // manipulations are done, the drawable must not be owned by any parent anymore. // // The reason why we need this drawable at all is as follows: // Consider Drawable A and its child X. Suppose we want to put X under a new parent B. If we just // do B.setCurrent(X), the old parent A still considers X to be its child. If at some later point // we do A.setChild(Y), drawable A will clear Drawable.Callback from its old child X, and will set // callback to its new child Y. But X is no longer a child of A, and so will A incorrectly remove // the callback that B set on X. To avoid that, before setting X as a child of B, we must first // remove it from A like so: A.setCurrent(empty); B.setCurrent(X);. In cases where we can't set a // null child, we use an empty drawable. private static final Drawable sEmptyDrawable = new ColorDrawable(Color.TRANSPARENT); /** * Wraps the given drawable with a new {@link ScaleTypeDrawable}. * * <p> If the provided drawable or scale type is null, the given drawable is returned without * being wrapped. * * @return the wrapping scale type drawable, or the original drawable if the wrapping didn't * take place */ @Nullable static Drawable maybeWrapWithScaleType( @Nullable Drawable drawable, @Nullable ScaleType scaleType) { return maybeWrapWithScaleType(drawable, scaleType, null); } /** * Wraps the given drawable with a new {@link ScaleTypeDrawable}. * * <p> If the provided drawable or scale type is null, the given drawable is returned without * being wrapped. * * @return the wrapping scale type drawable, or the original drawable if the wrapping didn't * take place */ @Nullable static Drawable maybeWrapWithScaleType( @Nullable Drawable drawable, @Nullable ScaleType scaleType, @Nullable PointF focusPoint) { if (drawable == null || scaleType == null) { return drawable; } ScaleTypeDrawable scaleTypeDrawable = new ScaleTypeDrawable(drawable, scaleType); if (focusPoint != null) { scaleTypeDrawable.setFocusPoint(focusPoint); } return scaleTypeDrawable; } /** * Wraps the given drawable with a new {@link MatrixDrawable}. * * <p> If the provided drawable or matrix is null, the given drawable is returned without * being wrapped. * * @return the wrapping matrix drawable, or the original drawable if the wrapping didn't * take place */ @Nullable static Drawable maybeWrapWithMatrix( @Nullable Drawable drawable, @Nullable Matrix matrix) { if (drawable == null || matrix == null) { return drawable; } return new MatrixDrawable(drawable, matrix); } /** * Wraps the parent's child with a ScaleTypeDrawable. */ static ScaleTypeDrawable wrapChildWithScaleType(DrawableParent parent, ScaleType scaleType) { Drawable child = parent.setDrawable(sEmptyDrawable); child = maybeWrapWithScaleType(child, scaleType); parent.setDrawable(child); Preconditions.checkNotNull(child, "Parent has no child drawable!"); return (ScaleTypeDrawable) child; } /** * Updates the overlay-color rounding of the parent's child drawable. * * <ul> * <li>If rounding mode is OVERLAY_COLOR and the child is not a RoundedCornersDrawable, * a new RoundedCornersDrawable is created and the child gets wrapped with it. * <li>If rounding mode is OVERLAY_COLOR and the child is already wrapped with a * RoundedCornersDrawable, its rounding parameters are updated. * <li>If rounding mode is not OVERLAY_COLOR and the child is wrapped with a * RoundedCornersDrawable, the rounded drawable gets removed and its child gets * attached directly to the parent. * </ul> */ static void updateOverlayColorRounding( DrawableParent parent, @Nullable RoundingParams roundingParams) { Drawable child = parent.getDrawable(); if (roundingParams != null && roundingParams.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 (child instanceof RoundedCornersDrawable) { RoundedCornersDrawable roundedCornersDrawable = (RoundedCornersDrawable) child; applyRoundingParams(roundedCornersDrawable, roundingParams); roundedCornersDrawable.setOverlayColor(roundingParams.getOverlayColor()); } else { // Important: remove the child before wrapping it with a new parent! child = parent.setDrawable(sEmptyDrawable); child = maybeWrapWithRoundedOverlayColor(child, roundingParams); parent.setDrawable(child); } } else if (child instanceof RoundedCornersDrawable) { // Overlay rounding no longer required so remove drawable that was doing the rounding. RoundedCornersDrawable roundedCornersDrawable = (RoundedCornersDrawable) child; // Important: remove the child before wrapping it with a new parent! child = roundedCornersDrawable.setCurrent(sEmptyDrawable); parent.setDrawable(child); // roundedCornersDrawable is removed and will get garbage collected, clear the child callback sEmptyDrawable.setCallback(null); } } /** * Updates the leaf rounding of the parent's child drawable. * * <ul> * <li>If rounding mode is BITMAP_ONLY and the child is not a rounded drawable, * it gets rounded with a new rounded drawable. * <li>If rounding mode is BITMAP_ONLY and the child is already rounded, * its rounding parameters are updated. * <li>If rounding mode is not BITMAP_ONLY and the child is rounded, * its rounding parameters are reset so that no rounding occurs. * </ul> */ static void updateLeafRounding( DrawableParent parent, @Nullable RoundingParams roundingParams, Resources resources) { parent = findDrawableParentForLeaf(parent); Drawable child = parent.getDrawable(); if (roundingParams != null && roundingParams.getRoundingMethod() == RoundingParams.RoundingMethod.BITMAP_ONLY) { // Leaf rounding requested - either update the params or wrap the current drawable in a // drawable that will round it. if (child instanceof Rounded) { Rounded rounded = (Rounded) child; applyRoundingParams(rounded, roundingParams); } else if (child != null) { // Important: remove the child before wrapping it with a new parent! parent.setDrawable(sEmptyDrawable); Drawable rounded = applyLeafRounding(child, roundingParams, resources); parent.setDrawable(rounded); } } else if (child instanceof Rounded) { // No rounding requested - reset rounding params so no rounding occurs. resetRoundingParams((Rounded) child); } } /** * Wraps the given drawable with a new {@link RoundedCornersDrawable}. * * <p> If the provided drawable is null, or if the rounding params do not specify OVERLAY_COLOR * mode, the given drawable is returned without being wrapped. * * @return the wrapping rounded drawable, or the original drawable if the wrapping didn't * take place */ static Drawable maybeWrapWithRoundedOverlayColor( @Nullable Drawable drawable, @Nullable RoundingParams roundingParams) { if (drawable == null || roundingParams == null || roundingParams.getRoundingMethod() != RoundingParams.RoundingMethod.OVERLAY_COLOR) { return drawable; } RoundedCornersDrawable roundedCornersDrawable = new RoundedCornersDrawable(drawable); applyRoundingParams(roundedCornersDrawable, roundingParams); roundedCornersDrawable.setOverlayColor(roundingParams.getOverlayColor()); return roundedCornersDrawable; } /** * Applies rounding on the drawable's leaf. * * <p> Currently only {@link BitmapDrawable} or {@link ColorDrawable} leafs can be rounded. * <p> If the leaf cannot be rounded, or the rounding params do not specify BITMAP_ONLY mode, * the given drawable is returned without being rounded. * <p> If the given drawable is a leaf itself, and it can be rounded, then the rounded drawable * is returned. * <p> If the given drawable is not a leaf, and its leaf can be rounded, the leaf gets rounded, * and the original drawable is returned. * * @return the rounded drawable, or the original drawable if the rounding didn't take place * or it took place on a drawable's child */ static Drawable maybeApplyLeafRounding( @Nullable Drawable drawable, @Nullable RoundingParams roundingParams, Resources resources) { if (drawable == null || roundingParams == null || roundingParams.getRoundingMethod() != RoundingParams.RoundingMethod.BITMAP_ONLY) { return drawable; } if (drawable instanceof ForwardingDrawable) { DrawableParent parent = findDrawableParentForLeaf((ForwardingDrawable) drawable); Drawable child = parent.setDrawable(sEmptyDrawable); child = applyLeafRounding(child, roundingParams, resources); parent.setDrawable(child); return drawable; } else { return applyLeafRounding(drawable, roundingParams, resources); } } /** * Rounds the given drawable with a {@link RoundedBitmapDrawable} or {@link RoundedColorDrawable}. * * <p> If the given drawable is not a {@link BitmapDrawable} or a {@link ColorDrawable}, it is * returned without being rounded. * * @return the rounded drawable, or the original drawable if rounding didn't take place */ private static Drawable applyLeafRounding( Drawable drawable, RoundingParams roundingParams, Resources resources) { if (drawable instanceof BitmapDrawable) { final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; RoundedBitmapDrawable roundedBitmapDrawable = new RoundedBitmapDrawable( resources, bitmapDrawable.getBitmap(), bitmapDrawable.getPaint()); 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; } /** * Applies the given rounding params on the specified rounded drawable. */ static void applyRoundingParams(Rounded rounded, RoundingParams roundingParams) { rounded.setCircle(roundingParams.getRoundAsCircle()); rounded.setRadii(roundingParams.getCornersRadii()); rounded.setBorder(roundingParams.getBorderColor(), roundingParams.getBorderWidth()); rounded.setPadding(roundingParams.getPadding()); } /** * Resets the rounding params on the specified rounded drawable, so that no rounding occurs. */ static void resetRoundingParams(Rounded rounded) { rounded.setCircle(false); rounded.setRadius(0); rounded.setBorder(Color.TRANSPARENT, 0); rounded.setPadding(0); } /** * Finds the immediate parent of a leaf drawable. */ static DrawableParent findDrawableParentForLeaf(DrawableParent parent) { while (true) { Drawable child = parent.getDrawable(); if (child == parent || !(child instanceof DrawableParent)) { break; } parent = (DrawableParent) child; } return parent; } }