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 17package androidx.core.graphics.drawable; 18 19import android.content.res.ColorStateList; 20import android.content.res.Resources; 21import android.graphics.ColorFilter; 22import android.graphics.PorterDuff; 23import android.graphics.drawable.Drawable; 24import android.graphics.drawable.DrawableContainer; 25import android.graphics.drawable.InsetDrawable; 26import android.os.Build; 27import android.util.AttributeSet; 28import android.util.Log; 29 30import androidx.annotation.ColorInt; 31import androidx.annotation.NonNull; 32import androidx.annotation.Nullable; 33import androidx.core.view.ViewCompat; 34 35import org.xmlpull.v1.XmlPullParser; 36import org.xmlpull.v1.XmlPullParserException; 37 38import java.io.IOException; 39import java.lang.reflect.Method; 40 41/** 42 * Helper for accessing features in {@link android.graphics.drawable.Drawable}. 43 */ 44public 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