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