DrawableUtils.java
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.appcompat.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Insets;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.DrawableContainer;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ScaleDrawable;
import android.os.Build;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appcompat.graphics.drawable.DrawableWrapper;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.graphics.drawable.WrappedDrawable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/** @hide */
@RestrictTo(LIBRARY_GROUP_PREFIX)
public class DrawableUtils {
private static final int[] CHECKED_STATE_SET = new int[] { android.R.attr.state_checked };
private static final int[] EMPTY_STATE_SET = new int[0];
public static final Rect INSETS_NONE = new Rect();
private DrawableUtils() {
// This class is non-instantiable.
}
/**
* Allows us to get the optical insets for a {@link Drawable}. Since this is hidden we need to
* use reflection. Since the {@code Insets} class is hidden also, we return a Rect instead.
*/
@NonNull
public static Rect getOpticalBounds(@NonNull Drawable drawable) {
if (Build.VERSION.SDK_INT >= 29) {
final Insets insets = Api29Impl.getOpticalInsets(drawable);
return new Rect(
insets.left,
insets.top,
insets.right,
insets.bottom
);
} else if (Build.VERSION.SDK_INT >= 18) {
return Api18Impl.getOpticalInsets(DrawableCompat.unwrap(drawable));
}
// If we reach here, either we're running on a device pre-v18, the Drawable didn't have
// any optical insets, or a reflection issue, so we'll just return an empty rect.
return INSETS_NONE;
}
/**
* Attempt the fix any issues in the given drawable, usually caused by platform bugs in the
* implementation. This method should be call after retrieval from
* {@link Resources} or a {@link TypedArray}.
*/
static void fixDrawable(@NonNull Drawable drawable) {
String className = drawable.getClass().getName();
if (Build.VERSION.SDK_INT == 21
&& "android.graphics.drawable.VectorDrawable".equals(className)) {
// VectorDrawable has an issue on API 21 where it sometimes doesn't create its tint
// filter until a state change event has occurred.
forceDrawableStateChange(drawable);
} else if (Build.VERSION.SDK_INT >= 29 && Build.VERSION.SDK_INT < 31
&& "android.graphics.drawable.ColorStateListDrawable".equals(className)) {
// ColorStateListDrawable has an issue on APIs 29 and 30 where it doesn't set up the
// default color until a state change event has occurred.
forceDrawableStateChange(drawable);
}
}
/**
* Some drawable implementations have problems with mutation. This method returns false if
* there is a known issue in the given drawable's implementation.
*/
public static boolean canSafelyMutateDrawable(@NonNull Drawable drawable) {
if (Build.VERSION.SDK_INT < 15 && drawable instanceof InsetDrawable) {
return false;
} else if (Build.VERSION.SDK_INT < 15 && drawable instanceof GradientDrawable) {
// GradientDrawable has a bug pre-ICS which results in mutate() resulting
// in loss of color
return false;
} else if (Build.VERSION.SDK_INT < 17 && drawable instanceof LayerDrawable) {
return false;
}
if (drawable instanceof DrawableContainer) {
// If we have a DrawableContainer, let's traverse its child array
final Drawable.ConstantState state = drawable.getConstantState();
if (state instanceof DrawableContainer.DrawableContainerState) {
final DrawableContainer.DrawableContainerState containerState =
(DrawableContainer.DrawableContainerState) state;
for (final Drawable child : containerState.getChildren()) {
if (!canSafelyMutateDrawable(child)) {
return false;
}
}
}
} else if (drawable instanceof WrappedDrawable) {
return canSafelyMutateDrawable(((WrappedDrawable) drawable).getWrappedDrawable());
} else if (drawable instanceof DrawableWrapper) {
return canSafelyMutateDrawable(((DrawableWrapper) drawable).getWrappedDrawable());
} else if (drawable instanceof ScaleDrawable) {
return canSafelyMutateDrawable(((ScaleDrawable) drawable).getDrawable());
}
return true;
}
/**
* Force a drawable state change.
*/
private static void forceDrawableStateChange(final Drawable drawable) {
final int[] originalState = drawable.getState();
if (originalState == null || originalState.length == 0) {
// The drawable doesn't have a state, so set it to be checked
drawable.setState(CHECKED_STATE_SET);
} else {
// Else the drawable does have a state, so clear it
drawable.setState(EMPTY_STATE_SET);
}
// Now set the original state
drawable.setState(originalState);
}
/**
* Parses tint mode.
*/
public static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
switch (value) {
case 3:
return PorterDuff.Mode.SRC_OVER;
case 5:
return PorterDuff.Mode.SRC_IN;
case 9:
return PorterDuff.Mode.SRC_ATOP;
case 14:
return PorterDuff.Mode.MULTIPLY;
case 15:
return PorterDuff.Mode.SCREEN;
case 16:
return PorterDuff.Mode.ADD;
default:
return defaultMode;
}
}
// Only accessible on SDK_INT >= 18 and < 29.
@RequiresApi(18)
static class Api18Impl {
private static final boolean sReflectionSuccessful;
private static final Method sGetOpticalInsets;
private static final Field sLeft;
private static final Field sTop;
private static final Field sRight;
private static final Field sBottom;
static {
Method getOpticalInsets = null;
Field left = null;
Field top = null;
Field right = null;
Field bottom = null;
boolean success = false;
try {
Class<?> insets = Class.forName("android.graphics.Insets");
getOpticalInsets = Drawable.class.getMethod("getOpticalInsets");
left = insets.getField("left");
top = insets.getField("top");
right = insets.getField("right");
bottom = insets.getField("bottom");
success = true;
} catch (NoSuchMethodException e) {
// Not successful, null everything out.
} catch (ClassNotFoundException e) {
// Not successful, null everything out.
} catch (NoSuchFieldException e) {
// Not successful, null everything out.
}
if (success) {
sGetOpticalInsets = getOpticalInsets;
sLeft = left;
sTop = top;
sRight = right;
sBottom = bottom;
sReflectionSuccessful = true;
} else {
sGetOpticalInsets = null;
sLeft = null;
sTop = null;
sRight = null;
sBottom = null;
sReflectionSuccessful = false;
}
}
private Api18Impl() {
// This class is not instantiable.
}
@NonNull
static Rect getOpticalInsets(@NonNull Drawable drawable) {
// Check the SDK_INT to avoid UncheckedReflection error.
if (Build.VERSION.SDK_INT < 29 && sReflectionSuccessful) {
try {
Object insets = sGetOpticalInsets.invoke(drawable);
if (insets != null) {
return new Rect(
sLeft.getInt(insets),
sTop.getInt(insets),
sRight.getInt(insets),
sBottom.getInt(insets)
);
}
} catch (IllegalAccessException e) {
// Ignore, we'll return empty insets.
} catch (InvocationTargetException e) {
// Ignore, we'll return empty insets.
}
}
return DrawableUtils.INSETS_NONE;
}
}
@RequiresApi(29)
static class Api29Impl {
private Api29Impl() {
// This class is not instantiable.
}
@DoNotInline
static Insets getOpticalInsets(Drawable drawable) {
return drawable.getOpticalInsets();
}
}
}