Home | History | Annotate | Download | only in drawable
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.core.graphics.drawable;
     18 
     19 import android.content.res.ColorStateList;
     20 import android.content.res.Resources;
     21 import android.graphics.ColorFilter;
     22 import android.graphics.PorterDuff;
     23 import android.graphics.drawable.Drawable;
     24 import android.graphics.drawable.DrawableContainer;
     25 import android.graphics.drawable.InsetDrawable;
     26 import android.os.Build;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 
     30 import androidx.annotation.ColorInt;
     31 import androidx.annotation.NonNull;
     32 import androidx.annotation.Nullable;
     33 import androidx.core.view.ViewCompat;
     34 
     35 import org.xmlpull.v1.XmlPullParser;
     36 import org.xmlpull.v1.XmlPullParserException;
     37 
     38 import java.io.IOException;
     39 import java.lang.reflect.Method;
     40 
     41 /**
     42  * Helper for accessing features in {@link android.graphics.drawable.Drawable}.
     43  */
     44 public final class DrawableCompat {
     45     private static final String TAG = "DrawableCompat";
     46 
     47     private static Method sSetLayoutDirectionMethod;
     48     private static boolean sSetLayoutDirectionMethodFetched;
     49 
     50     private static Method sGetLayoutDirectionMethod;
     51     private static boolean sGetLayoutDirectionMethodFetched;
     52 
     53     /**
     54      * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}.
     55      *
     56      * @param drawable The Drawable against which to invoke the method.
     57      *
     58      * @deprecated Use {@link Drawable#jumpToCurrentState()} directly.
     59      */
     60     @Deprecated
     61     public static void jumpToCurrentState(@NonNull Drawable drawable) {
     62         drawable.jumpToCurrentState();
     63     }
     64 
     65     /**
     66      * Set whether this Drawable is automatically mirrored when its layout
     67      * direction is RTL (right-to left). See
     68      * {@link android.util.LayoutDirection}.
     69      * <p>
     70      * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device
     71      * this method does nothing.
     72      *
     73      * @param drawable The Drawable against which to invoke the method.
     74      * @param mirrored Set to true if the Drawable should be mirrored, false if
     75      *            not.
     76      */
     77     public static void setAutoMirrored(@NonNull Drawable drawable, boolean mirrored) {
     78         if (Build.VERSION.SDK_INT >= 19) {
     79             drawable.setAutoMirrored(mirrored);
     80         }
     81     }
     82 
     83     /**
     84      * Tells if this Drawable will be automatically mirrored when its layout
     85      * direction is RTL right-to-left. See {@link android.util.LayoutDirection}.
     86      * <p>
     87      * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device
     88      * this method returns false.
     89      *
     90      * @param drawable The Drawable against which to invoke the method.
     91      * @return boolean Returns true if this Drawable will be automatically
     92      *         mirrored.
     93      */
     94     public static boolean isAutoMirrored(@NonNull Drawable drawable) {
     95         if (Build.VERSION.SDK_INT >= 19) {
     96             return drawable.isAutoMirrored();
     97         } else {
     98             return false;
     99         }
    100     }
    101 
    102     /**
    103      * Specifies the hotspot's location within the drawable.
    104      *
    105      * @param drawable The Drawable against which to invoke the method.
    106      * @param x The X coordinate of the center of the hotspot
    107      * @param y The Y coordinate of the center of the hotspot
    108      */
    109     public static void setHotspot(@NonNull Drawable drawable, float x, float y) {
    110         if (Build.VERSION.SDK_INT >= 21) {
    111             drawable.setHotspot(x, y);
    112         }
    113     }
    114 
    115     /**
    116      * Sets the bounds to which the hotspot is constrained, if they should be
    117      * different from the drawable bounds.
    118      *
    119      * @param drawable The Drawable against which to invoke the method.
    120      */
    121     public static void setHotspotBounds(@NonNull Drawable drawable, int left, int top,
    122             int right, int bottom) {
    123         if (Build.VERSION.SDK_INT >= 21) {
    124             drawable.setHotspotBounds(left, top, right, bottom);
    125         }
    126     }
    127 
    128     /**
    129      * Specifies a tint for {@code drawable}.
    130      *
    131      * @param drawable The Drawable against which to invoke the method.
    132      * @param tint     Color to use for tinting this drawable
    133      */
    134     public static void setTint(@NonNull Drawable drawable, @ColorInt int tint) {
    135         if (Build.VERSION.SDK_INT >= 21) {
    136             drawable.setTint(tint);
    137         } else if (drawable instanceof TintAwareDrawable) {
    138             ((TintAwareDrawable) drawable).setTint(tint);
    139         }
    140     }
    141 
    142     /**
    143      * Specifies a tint for {@code drawable} as a color state list.
    144      *
    145      * @param drawable The Drawable against which to invoke the method.
    146      * @param tint     Color state list to use for tinting this drawable, or null to clear the tint
    147      */
    148     public static void setTintList(@NonNull Drawable drawable, @Nullable ColorStateList tint) {
    149         if (Build.VERSION.SDK_INT >= 21) {
    150             drawable.setTintList(tint);
    151         } else if (drawable instanceof TintAwareDrawable) {
    152             ((TintAwareDrawable) drawable).setTintList(tint);
    153         }
    154     }
    155 
    156     /**
    157      * Specifies a tint blending mode for {@code drawable}.
    158      *
    159      * @param drawable The Drawable against which to invoke the method.
    160      * @param tintMode A Porter-Duff blending mode
    161      */
    162     public static void setTintMode(@NonNull Drawable drawable, @NonNull PorterDuff.Mode tintMode) {
    163         if (Build.VERSION.SDK_INT >= 21) {
    164             drawable.setTintMode(tintMode);
    165         } else if (drawable instanceof TintAwareDrawable) {
    166             ((TintAwareDrawable) drawable).setTintMode(tintMode);
    167         }
    168     }
    169 
    170     /**
    171      * Get the alpha value of the {@code drawable}.
    172      * 0 means fully transparent, 255 means fully opaque.
    173      *
    174      * @param drawable The Drawable against which to invoke the method.
    175      */
    176     public static int getAlpha(@NonNull Drawable drawable) {
    177         if (Build.VERSION.SDK_INT >= 19) {
    178             return drawable.getAlpha();
    179         } else {
    180             return 0;
    181         }
    182     }
    183 
    184     /**
    185      * Applies the specified theme to this Drawable and its children.
    186      */
    187     public static void applyTheme(@NonNull Drawable drawable, @NonNull Resources.Theme theme) {
    188         if (Build.VERSION.SDK_INT >= 21) {
    189             drawable.applyTheme(theme);
    190         }
    191     }
    192 
    193     /**
    194      * Whether a theme can be applied to this Drawable and its children.
    195      */
    196     public static boolean canApplyTheme(@NonNull Drawable drawable) {
    197         if (Build.VERSION.SDK_INT >= 21) {
    198             return drawable.canApplyTheme();
    199         } else {
    200             return false;
    201         }
    202     }
    203 
    204     /**
    205      * Returns the current color filter, or {@code null} if none set.
    206      *
    207      * @return the current color filter, or {@code null} if none set
    208      */
    209     public static ColorFilter getColorFilter(@NonNull Drawable drawable) {
    210         if (Build.VERSION.SDK_INT >= 21) {
    211             return drawable.getColorFilter();
    212         } else {
    213             return null;
    214         }
    215     }
    216 
    217     /**
    218      * Removes the color filter from the given drawable.
    219      */
    220     public static void clearColorFilter(@NonNull Drawable drawable) {
    221         if (Build.VERSION.SDK_INT >= 23) {
    222             // We can use clearColorFilter() safely on M+
    223             drawable.clearColorFilter();
    224         } else if (Build.VERSION.SDK_INT >= 21) {
    225             drawable.clearColorFilter();
    226 
    227             // API 21 + 22 have an issue where clearing a color filter on a DrawableContainer
    228             // will not propagate to all of its children. To workaround this we unwrap the drawable
    229             // to find any DrawableContainers, and then unwrap those to clear the filter on its
    230             // children manually
    231             if (drawable instanceof InsetDrawable) {
    232                 clearColorFilter(((InsetDrawable) drawable).getDrawable());
    233             } else if (drawable instanceof WrappedDrawable) {
    234                 clearColorFilter(((WrappedDrawable) drawable).getWrappedDrawable());
    235             } else if (drawable instanceof DrawableContainer) {
    236                 final DrawableContainer container = (DrawableContainer) drawable;
    237                 final DrawableContainer.DrawableContainerState state =
    238                         (DrawableContainer.DrawableContainerState) container.getConstantState();
    239                 if (state != null) {
    240                     Drawable child;
    241                     for (int i = 0, count = state.getChildCount(); i < count; i++) {
    242                         child = state.getChild(i);
    243                         if (child != null) {
    244                             clearColorFilter(child);
    245                         }
    246                     }
    247                 }
    248             }
    249         } else {
    250             drawable.clearColorFilter();
    251         }
    252     }
    253 
    254     /**
    255      * Inflate this Drawable from an XML resource optionally styled by a theme.
    256      *
    257      * @param res Resources used to resolve attribute values
    258      * @param parser XML parser from which to inflate this Drawable
    259      * @param attrs Base set of attribute values
    260      * @param theme Theme to apply, may be null
    261      * @throws XmlPullParserException
    262      * @throws IOException
    263      */
    264     public static void inflate(@NonNull Drawable drawable, @NonNull Resources res,
    265             @NonNull XmlPullParser parser, @NonNull AttributeSet attrs,
    266             @Nullable Resources.Theme theme)
    267             throws XmlPullParserException, IOException {
    268         if (Build.VERSION.SDK_INT >= 21) {
    269             drawable.inflate(res, parser, attrs, theme);
    270         } else {
    271             drawable.inflate(res, parser, attrs);
    272         }
    273     }
    274 
    275     /**
    276      * Potentially wrap {@code drawable} so that it may be used for tinting across the
    277      * different API levels, via the tinting methods in this class.
    278      *
    279      * <p>If the given drawable is wrapped, we will copy over certain state over to the wrapped
    280      * drawable, such as its bounds, level, visibility and state.</p>
    281      *
    282      * <p>You must use the result of this call. If the given drawable is being used by a view
    283      * (as its background for instance), you must replace the original drawable with
    284      * the result of this call:</p>
    285      *
    286      * <pre>
    287      * Drawable bg = DrawableCompat.wrap(view.getBackground());
    288      * // Need to set the background with the wrapped drawable
    289      * view.setBackground(bg);
    290      *
    291      * // You can now tint the drawable
    292      * DrawableCompat.setTint(bg, ...);
    293      * </pre>
    294      *
    295      * <p>If you need to get hold of the original {@link android.graphics.drawable.Drawable} again,
    296      * you can use the value returned from {@link #unwrap(Drawable)}.</p>
    297      *
    298      * @param drawable The Drawable to process
    299      * @return A drawable capable of being tinted across all API levels.
    300      *
    301      * @see #setTint(Drawable, int)
    302      * @see #setTintList(Drawable, ColorStateList)
    303      * @see #setTintMode(Drawable, PorterDuff.Mode)
    304      * @see #unwrap(Drawable)
    305      */
    306     public static Drawable wrap(@NonNull Drawable drawable) {
    307         if (Build.VERSION.SDK_INT >= 23) {
    308             return drawable;
    309         } else if (Build.VERSION.SDK_INT >= 21) {
    310             if (!(drawable instanceof TintAwareDrawable)) {
    311                 return new WrappedDrawableApi21(drawable);
    312             }
    313             return drawable;
    314         } else {
    315             if (!(drawable instanceof TintAwareDrawable)) {
    316                 return new WrappedDrawableApi14(drawable);
    317             }
    318             return drawable;
    319         }
    320     }
    321 
    322     /**
    323      * Unwrap {@code drawable} if it is the result of a call to {@link #wrap(Drawable)}. If
    324      * the {@code drawable} is not the result of a call to {@link #wrap(Drawable)} then
    325      * {@code drawable} is returned as-is.
    326      *
    327      * @param drawable The drawable to unwrap
    328      * @return the unwrapped {@link Drawable} or {@code drawable} if it hasn't been wrapped.
    329      *
    330      * @see #wrap(Drawable)
    331      */
    332     @SuppressWarnings("TypeParameterUnusedInFormals")
    333     public static <T extends Drawable> T unwrap(@NonNull Drawable drawable) {
    334         if (drawable instanceof WrappedDrawable) {
    335             return (T) ((WrappedDrawable) drawable).getWrappedDrawable();
    336         }
    337         return (T) drawable;
    338     }
    339 
    340     /**
    341      * Set the layout direction for this drawable. Should be a resolved
    342      * layout direction, as the Drawable has no capacity to do the resolution on
    343      * its own.
    344      *
    345      * @param layoutDirection the resolved layout direction for the drawable,
    346      *                        either {@link ViewCompat#LAYOUT_DIRECTION_LTR}
    347      *                        or {@link ViewCompat#LAYOUT_DIRECTION_RTL}
    348      * @return {@code true} if the layout direction change has caused the
    349      *         appearance of the drawable to change such that it needs to be
    350      *         re-drawn, {@code false} otherwise
    351      * @see #getLayoutDirection(Drawable)
    352      */
    353     public static boolean setLayoutDirection(@NonNull Drawable drawable, int layoutDirection) {
    354         if (Build.VERSION.SDK_INT >= 23) {
    355             return drawable.setLayoutDirection(layoutDirection);
    356         } else if (Build.VERSION.SDK_INT >= 17) {
    357             if (!sSetLayoutDirectionMethodFetched) {
    358                 try {
    359                     sSetLayoutDirectionMethod =
    360                             Drawable.class.getDeclaredMethod("setLayoutDirection", int.class);
    361                     sSetLayoutDirectionMethod.setAccessible(true);
    362                 } catch (NoSuchMethodException e) {
    363                     Log.i(TAG, "Failed to retrieve setLayoutDirection(int) method", e);
    364                 }
    365                 sSetLayoutDirectionMethodFetched = true;
    366             }
    367 
    368             if (sSetLayoutDirectionMethod != null) {
    369                 try {
    370                     sSetLayoutDirectionMethod.invoke(drawable, layoutDirection);
    371                     return true;
    372                 } catch (Exception e) {
    373                     Log.i(TAG, "Failed to invoke setLayoutDirection(int) via reflection", e);
    374                     sSetLayoutDirectionMethod = null;
    375                 }
    376             }
    377             return false;
    378         } else {
    379             return false;
    380         }
    381     }
    382 
    383     /**
    384      * Returns the resolved layout direction for this Drawable.
    385      *
    386      * @return One of {@link ViewCompat#LAYOUT_DIRECTION_LTR},
    387      *         {@link ViewCompat#LAYOUT_DIRECTION_RTL}
    388      * @see #setLayoutDirection(Drawable, int)
    389      */
    390     public static int getLayoutDirection(@NonNull Drawable drawable) {
    391         if (Build.VERSION.SDK_INT >= 23) {
    392             return drawable.getLayoutDirection();
    393         } else if (Build.VERSION.SDK_INT >= 17) {
    394             if (!sGetLayoutDirectionMethodFetched) {
    395                 try {
    396                     sGetLayoutDirectionMethod =
    397                             Drawable.class.getDeclaredMethod("getLayoutDirection");
    398                     sGetLayoutDirectionMethod.setAccessible(true);
    399                 } catch (NoSuchMethodException e) {
    400                     Log.i(TAG, "Failed to retrieve getLayoutDirection() method", e);
    401                 }
    402                 sGetLayoutDirectionMethodFetched = true;
    403             }
    404 
    405             if (sGetLayoutDirectionMethod != null) {
    406                 try {
    407                     return (int) sGetLayoutDirectionMethod.invoke(drawable);
    408                 } catch (Exception e) {
    409                     Log.i(TAG, "Failed to invoke getLayoutDirection() via reflection", e);
    410                     sGetLayoutDirectionMethod = null;
    411                 }
    412             }
    413             return ViewCompat.LAYOUT_DIRECTION_LTR;
    414         } else {
    415             return ViewCompat.LAYOUT_DIRECTION_LTR;
    416         }
    417     }
    418 
    419     private DrawableCompat() {}
    420 }
    421