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 android.support.v4.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.support.annotation.ColorInt; 28 import android.support.annotation.NonNull; 29 import android.support.annotation.Nullable; 30 import android.support.annotation.RequiresApi; 31 import android.support.v4.view.ViewCompat; 32 import android.util.AttributeSet; 33 import android.util.Log; 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 * introduced after API level 4 in a backwards compatible fashion. 44 */ 45 public final class DrawableCompat { 46 /** 47 * Interface implementation that doesn't use anything about v4 APIs. 48 */ 49 static class DrawableCompatBaseImpl { jumpToCurrentState(Drawable drawable)50 public void jumpToCurrentState(Drawable drawable) { 51 drawable.jumpToCurrentState(); 52 } 53 setAutoMirrored(Drawable drawable, boolean mirrored)54 public void setAutoMirrored(Drawable drawable, boolean mirrored) { 55 } 56 isAutoMirrored(Drawable drawable)57 public boolean isAutoMirrored(Drawable drawable) { 58 return false; 59 } 60 setHotspot(Drawable drawable, float x, float y)61 public void setHotspot(Drawable drawable, float x, float y) { 62 } 63 setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom)64 public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { 65 } 66 setTint(Drawable drawable, int tint)67 public void setTint(Drawable drawable, int tint) { 68 if (drawable instanceof TintAwareDrawable) { 69 ((TintAwareDrawable) drawable).setTint(tint); 70 } 71 } 72 setTintList(Drawable drawable, ColorStateList tint)73 public void setTintList(Drawable drawable, ColorStateList tint) { 74 if (drawable instanceof TintAwareDrawable) { 75 ((TintAwareDrawable) drawable).setTintList(tint); 76 } 77 } 78 setTintMode(Drawable drawable, PorterDuff.Mode tintMode)79 public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { 80 if (drawable instanceof TintAwareDrawable) { 81 ((TintAwareDrawable) drawable).setTintMode(tintMode); 82 } 83 } 84 wrap(Drawable drawable)85 public Drawable wrap(Drawable drawable) { 86 if (!(drawable instanceof TintAwareDrawable)) { 87 return new DrawableWrapperApi14(drawable); 88 } 89 return drawable; 90 } 91 setLayoutDirection(Drawable drawable, int layoutDirection)92 public boolean setLayoutDirection(Drawable drawable, int layoutDirection) { 93 // No op for API < 23 94 return false; 95 } 96 getLayoutDirection(Drawable drawable)97 public int getLayoutDirection(Drawable drawable) { 98 return ViewCompat.LAYOUT_DIRECTION_LTR; 99 } 100 getAlpha(Drawable drawable)101 public int getAlpha(Drawable drawable) { 102 return 0; 103 } 104 applyTheme(Drawable drawable, Resources.Theme t)105 public void applyTheme(Drawable drawable, Resources.Theme t) { 106 } 107 canApplyTheme(Drawable drawable)108 public boolean canApplyTheme(Drawable drawable) { 109 return false; 110 } 111 getColorFilter(Drawable drawable)112 public ColorFilter getColorFilter(Drawable drawable) { 113 return null; 114 } 115 clearColorFilter(Drawable drawable)116 public void clearColorFilter(Drawable drawable) { 117 drawable.clearColorFilter(); 118 } 119 inflate(Drawable drawable, Resources res, XmlPullParser parser, AttributeSet attrs, Resources.Theme t)120 public void inflate(Drawable drawable, Resources res, XmlPullParser parser, 121 AttributeSet attrs, Resources.Theme t) 122 throws IOException, XmlPullParserException { 123 drawable.inflate(res, parser, attrs); 124 } 125 } 126 127 @RequiresApi(17) 128 static class DrawableCompatApi17Impl extends DrawableCompatBaseImpl { 129 private static final String TAG = "DrawableCompatApi17"; 130 131 private static Method sSetLayoutDirectionMethod; 132 private static boolean sSetLayoutDirectionMethodFetched; 133 134 private static Method sGetLayoutDirectionMethod; 135 private static boolean sGetLayoutDirectionMethodFetched; 136 137 @Override setLayoutDirection(Drawable drawable, int layoutDirection)138 public boolean setLayoutDirection(Drawable drawable, int layoutDirection) { 139 if (!sSetLayoutDirectionMethodFetched) { 140 try { 141 sSetLayoutDirectionMethod = 142 Drawable.class.getDeclaredMethod("setLayoutDirection", int.class); 143 sSetLayoutDirectionMethod.setAccessible(true); 144 } catch (NoSuchMethodException e) { 145 Log.i(TAG, "Failed to retrieve setLayoutDirection(int) method", e); 146 } 147 sSetLayoutDirectionMethodFetched = true; 148 } 149 150 if (sSetLayoutDirectionMethod != null) { 151 try { 152 sSetLayoutDirectionMethod.invoke(drawable, layoutDirection); 153 return true; 154 } catch (Exception e) { 155 Log.i(TAG, "Failed to invoke setLayoutDirection(int) via reflection", e); 156 sSetLayoutDirectionMethod = null; 157 } 158 } 159 return false; 160 } 161 162 @Override getLayoutDirection(Drawable drawable)163 public int getLayoutDirection(Drawable drawable) { 164 if (!sGetLayoutDirectionMethodFetched) { 165 try { 166 sGetLayoutDirectionMethod = Drawable.class.getDeclaredMethod("getLayoutDirection"); 167 sGetLayoutDirectionMethod.setAccessible(true); 168 } catch (NoSuchMethodException e) { 169 Log.i(TAG, "Failed to retrieve getLayoutDirection() method", e); 170 } 171 sGetLayoutDirectionMethodFetched = true; 172 } 173 174 if (sGetLayoutDirectionMethod != null) { 175 try { 176 return (int) sGetLayoutDirectionMethod.invoke(drawable); 177 } catch (Exception e) { 178 Log.i(TAG, "Failed to invoke getLayoutDirection() via reflection", e); 179 sGetLayoutDirectionMethod = null; 180 } 181 } 182 return ViewCompat.LAYOUT_DIRECTION_LTR; 183 } 184 } 185 186 /** 187 * Interface implementation for devices with at least KitKat APIs. 188 */ 189 @RequiresApi(19) 190 static class DrawableCompatApi19Impl extends DrawableCompatApi17Impl { 191 @Override setAutoMirrored(Drawable drawable, boolean mirrored)192 public void setAutoMirrored(Drawable drawable, boolean mirrored) { 193 drawable.setAutoMirrored(mirrored); 194 } 195 196 @Override isAutoMirrored(Drawable drawable)197 public boolean isAutoMirrored(Drawable drawable) { 198 return drawable.isAutoMirrored(); 199 } 200 201 @Override wrap(Drawable drawable)202 public Drawable wrap(Drawable drawable) { 203 if (!(drawable instanceof TintAwareDrawable)) { 204 return new DrawableWrapperApi19(drawable); 205 } 206 return drawable; 207 } 208 209 @Override getAlpha(Drawable drawable)210 public int getAlpha(Drawable drawable) { 211 return drawable.getAlpha(); 212 } 213 } 214 215 /** 216 * Interface implementation for devices with at least L APIs. 217 */ 218 @RequiresApi(21) 219 static class DrawableCompatApi21Impl extends DrawableCompatApi19Impl { 220 @Override setHotspot(Drawable drawable, float x, float y)221 public void setHotspot(Drawable drawable, float x, float y) { 222 drawable.setHotspot(x, y); 223 } 224 225 @Override setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom)226 public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { 227 drawable.setHotspotBounds(left, top, right, bottom); 228 } 229 230 @Override setTint(Drawable drawable, int tint)231 public void setTint(Drawable drawable, int tint) { 232 drawable.setTint(tint); 233 } 234 235 @Override setTintList(Drawable drawable, ColorStateList tint)236 public void setTintList(Drawable drawable, ColorStateList tint) { 237 drawable.setTintList(tint); 238 } 239 240 @Override setTintMode(Drawable drawable, PorterDuff.Mode tintMode)241 public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { 242 drawable.setTintMode(tintMode); 243 } 244 245 @Override wrap(Drawable drawable)246 public Drawable wrap(Drawable drawable) { 247 if (!(drawable instanceof TintAwareDrawable)) { 248 return new DrawableWrapperApi21(drawable); 249 } 250 return drawable; 251 } 252 253 @Override applyTheme(Drawable drawable, Resources.Theme t)254 public void applyTheme(Drawable drawable, Resources.Theme t) { 255 drawable.applyTheme(t); 256 } 257 258 @Override canApplyTheme(Drawable drawable)259 public boolean canApplyTheme(Drawable drawable) { 260 return drawable.canApplyTheme(); 261 } 262 263 @Override getColorFilter(Drawable drawable)264 public ColorFilter getColorFilter(Drawable drawable) { 265 return drawable.getColorFilter(); 266 } 267 268 @Override clearColorFilter(Drawable drawable)269 public void clearColorFilter(Drawable drawable) { 270 drawable.clearColorFilter(); 271 272 // API 21 + 22 have an issue where clearing a color filter on a DrawableContainer 273 // will not propagate to all of its children. To workaround this we unwrap the drawable 274 // to find any DrawableContainers, and then unwrap those to clear the filter on its 275 // children manually 276 if (drawable instanceof InsetDrawable) { 277 clearColorFilter(((InsetDrawable) drawable).getDrawable()); 278 } else if (drawable instanceof DrawableWrapper) { 279 clearColorFilter(((DrawableWrapper) drawable).getWrappedDrawable()); 280 } else if (drawable instanceof DrawableContainer) { 281 final DrawableContainer container = (DrawableContainer) drawable; 282 final DrawableContainer.DrawableContainerState state = 283 (DrawableContainer.DrawableContainerState) container.getConstantState(); 284 if (state != null) { 285 Drawable child; 286 for (int i = 0, count = state.getChildCount(); i < count; i++) { 287 child = state.getChild(i); 288 if (child != null) { 289 clearColorFilter(child); 290 } 291 } 292 } 293 } 294 } 295 296 @Override inflate(Drawable drawable, Resources res, XmlPullParser parser, AttributeSet attrs, Resources.Theme t)297 public void inflate(Drawable drawable, Resources res, XmlPullParser parser, 298 AttributeSet attrs, Resources.Theme t) 299 throws IOException, XmlPullParserException { 300 drawable.inflate(res, parser, attrs, t); 301 } 302 } 303 304 /** 305 * Interface implementation for devices with at least M APIs. 306 */ 307 @RequiresApi(23) 308 static class DrawableCompatApi23Impl extends DrawableCompatApi21Impl { 309 @Override setLayoutDirection(Drawable drawable, int layoutDirection)310 public boolean setLayoutDirection(Drawable drawable, int layoutDirection) { 311 return drawable.setLayoutDirection(layoutDirection); 312 } 313 314 @Override getLayoutDirection(Drawable drawable)315 public int getLayoutDirection(Drawable drawable) { 316 return drawable.getLayoutDirection(); 317 } 318 319 @Override wrap(Drawable drawable)320 public Drawable wrap(Drawable drawable) { 321 // No need to wrap on M+ 322 return drawable; 323 } 324 325 @Override clearColorFilter(Drawable drawable)326 public void clearColorFilter(Drawable drawable) { 327 // We can use clearColorFilter() safely on M+ 328 drawable.clearColorFilter(); 329 } 330 } 331 332 /** 333 * Select the correct implementation to use for the current platform. 334 */ 335 static final DrawableCompatBaseImpl IMPL; 336 static { 337 if (Build.VERSION.SDK_INT >= 23) { 338 IMPL = new DrawableCompatApi23Impl(); 339 } else if (Build.VERSION.SDK_INT >= 21) { 340 IMPL = new DrawableCompatApi21Impl(); 341 } else if (Build.VERSION.SDK_INT >= 19) { 342 IMPL = new DrawableCompatApi19Impl(); 343 } else if (Build.VERSION.SDK_INT >= 17) { 344 IMPL = new DrawableCompatApi17Impl(); 345 } else { 346 IMPL = new DrawableCompatBaseImpl(); 347 } 348 } 349 350 /** 351 * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}. 352 * <p> 353 * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} 354 * device this method does nothing. 355 * 356 * @param drawable The Drawable against which to invoke the method. 357 */ jumpToCurrentState(@onNull Drawable drawable)358 public static void jumpToCurrentState(@NonNull Drawable drawable) { 359 IMPL.jumpToCurrentState(drawable); 360 } 361 362 /** 363 * Set whether this Drawable is automatically mirrored when its layout 364 * direction is RTL (right-to left). See 365 * {@link android.util.LayoutDirection}. 366 * <p> 367 * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device 368 * this method does nothing. 369 * 370 * @param drawable The Drawable against which to invoke the method. 371 * @param mirrored Set to true if the Drawable should be mirrored, false if 372 * not. 373 */ setAutoMirrored(@onNull Drawable drawable, boolean mirrored)374 public static void setAutoMirrored(@NonNull Drawable drawable, boolean mirrored) { 375 IMPL.setAutoMirrored(drawable, mirrored); 376 } 377 378 /** 379 * Tells if this Drawable will be automatically mirrored when its layout 380 * direction is RTL right-to-left. See {@link android.util.LayoutDirection}. 381 * <p> 382 * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device 383 * this method returns false. 384 * 385 * @param drawable The Drawable against which to invoke the method. 386 * @return boolean Returns true if this Drawable will be automatically 387 * mirrored. 388 */ isAutoMirrored(@onNull Drawable drawable)389 public static boolean isAutoMirrored(@NonNull Drawable drawable) { 390 return IMPL.isAutoMirrored(drawable); 391 } 392 393 /** 394 * Specifies the hotspot's location within the drawable. 395 * 396 * @param drawable The Drawable against which to invoke the method. 397 * @param x The X coordinate of the center of the hotspot 398 * @param y The Y coordinate of the center of the hotspot 399 */ setHotspot(@onNull Drawable drawable, float x, float y)400 public static void setHotspot(@NonNull Drawable drawable, float x, float y) { 401 IMPL.setHotspot(drawable, x, y); 402 } 403 404 /** 405 * Sets the bounds to which the hotspot is constrained, if they should be 406 * different from the drawable bounds. 407 * 408 * @param drawable The Drawable against which to invoke the method. 409 */ setHotspotBounds(@onNull Drawable drawable, int left, int top, int right, int bottom)410 public static void setHotspotBounds(@NonNull Drawable drawable, int left, int top, 411 int right, int bottom) { 412 IMPL.setHotspotBounds(drawable, left, top, right, bottom); 413 } 414 415 /** 416 * Specifies a tint for {@code drawable}. 417 * 418 * @param drawable The Drawable against which to invoke the method. 419 * @param tint Color to use for tinting this drawable 420 */ setTint(@onNull Drawable drawable, @ColorInt int tint)421 public static void setTint(@NonNull Drawable drawable, @ColorInt int tint) { 422 IMPL.setTint(drawable, tint); 423 } 424 425 /** 426 * Specifies a tint for {@code drawable} as a color state list. 427 * 428 * @param drawable The Drawable against which to invoke the method. 429 * @param tint Color state list to use for tinting this drawable, or null to clear the tint 430 */ setTintList(@onNull Drawable drawable, @Nullable ColorStateList tint)431 public static void setTintList(@NonNull Drawable drawable, @Nullable ColorStateList tint) { 432 IMPL.setTintList(drawable, tint); 433 } 434 435 /** 436 * Specifies a tint blending mode for {@code drawable}. 437 * 438 * @param drawable The Drawable against which to invoke the method. 439 * @param tintMode A Porter-Duff blending mode 440 */ setTintMode(@onNull Drawable drawable, @Nullable PorterDuff.Mode tintMode)441 public static void setTintMode(@NonNull Drawable drawable, @Nullable PorterDuff.Mode tintMode) { 442 IMPL.setTintMode(drawable, tintMode); 443 } 444 445 /** 446 * Get the alpha value of the {@code drawable}. 447 * 0 means fully transparent, 255 means fully opaque. 448 * 449 * @param drawable The Drawable against which to invoke the method. 450 */ getAlpha(@onNull Drawable drawable)451 public static int getAlpha(@NonNull Drawable drawable) { 452 return IMPL.getAlpha(drawable); 453 } 454 455 /** 456 * Applies the specified theme to this Drawable and its children. 457 */ applyTheme(@onNull Drawable drawable, @NonNull Resources.Theme t)458 public static void applyTheme(@NonNull Drawable drawable, @NonNull Resources.Theme t) { 459 IMPL.applyTheme(drawable, t); 460 } 461 462 /** 463 * Whether a theme can be applied to this Drawable and its children. 464 */ canApplyTheme(@onNull Drawable drawable)465 public static boolean canApplyTheme(@NonNull Drawable drawable) { 466 return IMPL.canApplyTheme(drawable); 467 } 468 469 /** 470 * Returns the current color filter, or {@code null} if none set. 471 * 472 * @return the current color filter, or {@code null} if none set 473 */ getColorFilter(@onNull Drawable drawable)474 public static ColorFilter getColorFilter(@NonNull Drawable drawable) { 475 return IMPL.getColorFilter(drawable); 476 } 477 478 /** 479 * Removes the color filter from the given drawable. 480 */ clearColorFilter(@onNull Drawable drawable)481 public static void clearColorFilter(@NonNull Drawable drawable) { 482 IMPL.clearColorFilter(drawable); 483 } 484 485 /** 486 * Inflate this Drawable from an XML resource optionally styled by a theme. 487 * 488 * @param res Resources used to resolve attribute values 489 * @param parser XML parser from which to inflate this Drawable 490 * @param attrs Base set of attribute values 491 * @param theme Theme to apply, may be null 492 * @throws XmlPullParserException 493 * @throws IOException 494 */ inflate(@onNull Drawable drawable, @NonNull Resources res, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Resources.Theme theme)495 public static void inflate(@NonNull Drawable drawable, @NonNull Resources res, 496 @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, 497 @Nullable Resources.Theme theme) 498 throws XmlPullParserException, IOException { 499 IMPL.inflate(drawable, res, parser, attrs, theme); 500 } 501 502 /** 503 * Potentially wrap {@code drawable} so that it may be used for tinting across the 504 * different API levels, via the tinting methods in this class. 505 * 506 * <p>If the given drawable is wrapped, we will copy over certain state over to the wrapped 507 * drawable, such as its bounds, level, visibility and state.</p> 508 * 509 * <p>You must use the result of this call. If the given drawable is being used by a view 510 * (as its background for instance), you must replace the original drawable with 511 * the result of this call:</p> 512 * 513 * <pre> 514 * Drawable bg = DrawableCompat.wrap(view.getBackground()); 515 * // Need to set the background with the wrapped drawable 516 * view.setBackground(bg); 517 * 518 * // You can now tint the drawable 519 * DrawableCompat.setTint(bg, ...); 520 * </pre> 521 * 522 * <p>If you need to get hold of the original {@link android.graphics.drawable.Drawable} again, 523 * you can use the value returned from {@link #unwrap(Drawable)}.</p> 524 * 525 * @param drawable The Drawable to process 526 * @return A drawable capable of being tinted across all API levels. 527 * 528 * @see #setTint(Drawable, int) 529 * @see #setTintList(Drawable, ColorStateList) 530 * @see #setTintMode(Drawable, PorterDuff.Mode) 531 * @see #unwrap(Drawable) 532 */ wrap(@onNull Drawable drawable)533 public static Drawable wrap(@NonNull Drawable drawable) { 534 return IMPL.wrap(drawable); 535 } 536 537 /** 538 * Unwrap {@code drawable} if it is the result of a call to {@link #wrap(Drawable)}. If 539 * the {@code drawable} is not the result of a call to {@link #wrap(Drawable)} then 540 * {@code drawable} is returned as-is. 541 * 542 * @param drawable The drawable to unwrap 543 * @return the unwrapped {@link Drawable} or {@code drawable} if it hasn't been wrapped. 544 * 545 * @see #wrap(Drawable) 546 */ 547 @SuppressWarnings("TypeParameterUnusedInFormals") unwrap(@onNull Drawable drawable)548 public static <T extends Drawable> T unwrap(@NonNull Drawable drawable) { 549 if (drawable instanceof DrawableWrapper) { 550 return (T) ((DrawableWrapper) drawable).getWrappedDrawable(); 551 } 552 return (T) drawable; 553 } 554 555 /** 556 * Set the layout direction for this drawable. Should be a resolved 557 * layout direction, as the Drawable has no capacity to do the resolution on 558 * its own. 559 * 560 * @param layoutDirection the resolved layout direction for the drawable, 561 * either {@link ViewCompat#LAYOUT_DIRECTION_LTR} 562 * or {@link ViewCompat#LAYOUT_DIRECTION_RTL} 563 * @return {@code true} if the layout direction change has caused the 564 * appearance of the drawable to change such that it needs to be 565 * re-drawn, {@code false} otherwise 566 * @see #getLayoutDirection(Drawable) 567 */ setLayoutDirection(@onNull Drawable drawable, int layoutDirection)568 public static boolean setLayoutDirection(@NonNull Drawable drawable, int layoutDirection) { 569 return IMPL.setLayoutDirection(drawable, layoutDirection); 570 } 571 572 /** 573 * Returns the resolved layout direction for this Drawable. 574 * 575 * @return One of {@link ViewCompat#LAYOUT_DIRECTION_LTR}, 576 * {@link ViewCompat#LAYOUT_DIRECTION_RTL} 577 * @see #setLayoutDirection(Drawable, int) 578 */ getLayoutDirection(@onNull Drawable drawable)579 public static int getLayoutDirection(@NonNull Drawable drawable) { 580 return IMPL.getLayoutDirection(drawable); 581 } 582 DrawableCompat()583 private DrawableCompat() {} 584 } 585