getBounds(): the story of Drawables and their View masters

67867d2677e7dd983000441ff0b1c089?s=47 NeiL saitug
September 20, 2014

getBounds(): the story of Drawables and their View masters

At Devoxx 2013, Cyril Mottier gave a great talk on Mastering Android Drawables. Hoping to build on that talk and go a bit more indepth into how to actually implement a custom drawable. What is a Drawable? How do you make one? How do they interact with their Views. Lisa’s got a few stories to share about writing a custom Drawable type from scratch. Do you know the difference between getMinimumHeight vs getIntrinsicHeight, and more importantly why making custom Drawables is painful. We’ll talk about the native Drawable types, how custom Drawables can be included in XML (hint, they can’t), the nuts and bolts of how Drawable instances share their constant state (So why bitmap images for Drawables get shared). Finally, we’ll go into the relationship between views and drawables -- how do drawables’ size get set, and what’s the difference between using a ColorDrawable versus a BitmapDrawable with respect to the View that is using the Drawable as a view backing.

Collaboration with Jamie Huson (@jamiehuson)

Presented at DroidCon NYC 2014 on Sept 20
http://nyc.droidcon.com/2014/dcnyc/85/

67867d2677e7dd983000441ff0b1c089?s=128

NeiL saitug

September 20, 2014
Tweet

Transcript

  1. getBounds() The story of Drawables and their View masters Jamie

    Huson + Lisa Neigut | 20 Sept 2014
  2. None
  3. Drawables!

  4. What is a Drawable?

  5. "A drawable is a positioned entity that is to be

    drawn on a canvas." -Cyril Mottier
  6. What Makes Drawables Awesome: 4 Drawing is fun! 4 State

    Changes 4 Total Separation of View Logic from Code 4 Focused code 4 XML
  7. NinePatchDrawable

  8. ShapeDrawable

  9. StateListDrawable

  10. BitmapDrawable

  11. LayerDrawable

  12. So many options…

  13. BadgeDrawable

  14. IconDrawable

  15. IconDrawable

  16. Custom Drawables

  17. Using built-in Drawables /res ../drawable .. layered_drawable.xml <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!--

    Transparent top line for evenness --> <item> <shape android:shape="oval"> <solid android:color="@color/transparent"/> <corners android:radius="@dimen/gen_avatar_corners_small"/> <padding android:top="@dimen/gen_avatar_border_shadow"/> </shape> </item> <!-- Light grey --> ...
  18. Using built-in Drawables layered_drawable.xml <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Transparent top line

    for evenness --> <item> <shape android:shape="oval"> <solid android:color="@color/transparent"/> <corners android:radius="@dimen/gen_avatar_corners_small"/> <padding android:top="@dimen/gen_avatar_border_shadow"/> </shape> </item> <!-- Light grey --> ... layout.xml <View ... android:background="@drawable/layered_drawable" />
  19. Custom Drawable /res ../drawable ..custom_drawable.xml <custom-drawable xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" app:icon="@id/ic_etsy_e" app:color="@color/etsy_orange"

    > </custom-drawable>
  20. Custom Drawable custom_drawable.xml <custom-drawable xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" app:icon="@id/ic_etsy_e" app:color="@color/etsy_orange" > </custom-drawable>

    layout.xml <View ... android:background="@drawable/custom_drawable” />
  21. Runtime Error Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #2:

    __invalid drawable tag custom__
  22. Drawable.java public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs)

    throws XmlPullParserException, IOException { Drawable drawable; final String name = parser.getName(); if (name.equals("selector")) { drawable = new StateListDrawable(); } else if (name.equals("level-list")) { drawable = new LevelListDrawable(); } else if (name.equals("layer-list")) { drawable = new LayerDrawable(); } else if (name.equals("transition")) { drawable = new TransitionDrawable(); } else if (name.equals("color")) { drawable = new ColorDrawable(); } else if (name.equals("shape")) { drawable = new GradientDrawable(); } ... } else { throw new XmlPullParserException(parser.getPositionDescription() + ": invalid drawable tag " + name); } drawable.inflate(r, parser, attrs); return drawable; }
  23. Seriously?

  24. None
  25. Good ol' Java CustomDrawable drawable = new CustomDrawable(); drawable.setColorFilter(Color.BLACK, PorterDuff.Mode.DST);

    mView.setBackground(drawable);
  26. The Drawable API

  27. Methods to @Override: 4 draw(Canvas canvas) 4 getOpacity() 4 getIntrinsicHeight/Width()

    4 getMinimumHeight/Width() 4 getConstantState() and mutate()
  28. @Override draw(Canvas canvas)

  29. @Override draw(Canvas canvas) 4 Just like a View's drawing. 4

    Call canvas.drawSomething(Paint) methods. 4 Transformations, Rotations, Shaders all still apply. 4 Use getBounds() to determine drawing area
  30. @Override getOpacity()

  31. @Override getOpacity() ColorDrawable.java public int getOpacity() { switch (mState.mUseColor >>>

    24) { case 255: return PixelFormat.OPAQUE; case 0: return PixelFormat.TRANSPARENT; } return PixelFormat.TRANSLUCENT; }
  32. @Override getIntrinsicHeight\Width()

  33. @Override getIntrinsicHeight\Width() ColorDrawable -> Drawable public int getIntrinsicHeight() { return

    -1; }
  34. @Override getMinimumHeight\Width()

  35. @Override getMinimumHeight\Width() Default: returns 0

  36. @Override getConstantState() and mutate()

  37. @Override getConstantState()

  38. @Override mutate() 4 Make this drawable mutable, independent of other

    instances. 4 Basically creates an ‘independent’ state.
  39. Drawables + Views

  40. Drawable.Callback

  41. Drawable.Callback public static interface Callback { public void invalidateDrawable(Drawable who);

    public void scheduleDrawable(Drawable who, Runnable what, long when); public void unscheduleDrawable(Drawable who, Runnable what); }
  42. Drawable.Callback public class View implements Drawable.Callback { ... public void

    setBackground(Drawable d) { mBackground = d; d.setCallback(this); } ... public void invalidateDrawable(Drawable drawable) { ... final Rect dirty = drawable.getBounds(); final int scrollX = mScrollX; final int scrollY = mScrollY; invalidate(dirty.left + scrollX, dirty.top + scrollY, dirty.right + scrollX, dirty.bottom + scrollY); ... } ... }
  43. Drawable.Callback public abstract class Drawable { private WeakReference<Callback> mCallback =

    null; ... public void invalidateSelf() { final Callback callback = getCallback(); if (callback != null) { callback.invalidateDrawable(this); } } ... }
  44. Drawable Bounds

  45. Drawable.java setBounds(Rect rect) setBounds(int left, int top, int right, int

    bottom)
  46. setBounds(Rect rect) setBounds(int left, int top, int right, int bottom)

  47. setBounds(Rect rect) setBounds(int left, int top, int right, int bottom)

  48. View Render Passes 4 Measurement 4 Layout 4 Draw

  49. View Render Passes 4 Measurement 4 Layout 4 Draw

  50. onMeasure 4 View queries the Drawable for its desired size

    getIntrinsic…()
  51. onMeasure 4 View queries the Drawable for its desired size

    getIntrinsic…() 4 View uses the dimens of the Drawable to calculate its dimens and reports back its size to its parent
  52. onDraw 4 Calculates the bounds for that drawable*

  53. onDraw 4 Calculates the bounds for that drawable* 4 Calls

    setBounds() on the drawable*
  54. onDraw 4 Calculates the bounds for that drawable* 4 Calls

    setBounds() on the drawable* 4 Calls draw(canvas) on the drawable
  55. A Few Notes: Views assume that the drawable knows what

    size it wants to be before the bounds get set.
  56. A Few Notes: Views assume that the drawable knows what

    size it wants to be before the bounds get set. Every View computes the drawable’s bounds differently.
  57. View Bound Calculations: ColorDrawable.java getIntrinsicHeight() { returns -1; }

  58. View Bound Calculations: ColorDrawable.java getIntrinsicHeight() { returns -1; } CompoundButton

    setBounds(-1, -1, -1, -1);
  59. View Bound Calculations: ColorDrawable.java getIntrinsicHeight() { returns -1; } CompoundButton

    setBounds(-1, -1, -1, -1); ImageView setBounds(leftMax, rightMax, topMax, bottomMax)
  60. The Future of Drawable

  61. Ripple

  62. None
  63. None
  64. Ripple public class Drawable { ... public void setHotspot(float x,

    float y) { /* compiled code */ } ... }
  65. Pre-L?

  66. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { }

  67. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { private Drawable mask;

    public RippleDrawableCompat(Drawable content, Drawable mask) { super(content != null ? new Drawable[] { content } : new Drawable[] { } ); this.mask = mask; } }
  68. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { private ColorStateList colors;

    public void setColor(ColorStateList colors) { this.colors = colors; } }
  69. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { private float hotspotX;

    private float hotspotY; public void setHotspot(float x, float y) { this.hotspotX = x; this.hotspotY = y; } }
  70. RippleDrawableCompat public class RippledImageView extends ImageView { ... @Override public

    boolean onTouchEvent(MotionEvent event) { updateHotspot(event.getX(), event.getY()); return super.onTouchEvent(event); } private void updateHotspot(float x, float y) { Drawable background = getBackground(); if (background != null && background instanceof RippleDrawableCompat) { ((RippleDrawableCompat) background).setHotspot(x, y); } Drawable src = getDrawable(); if (src != null && src instanceof RippleDrawableCompat) { ((RippleDrawableCompat) src).setHotspot(x, y); } } }
  71. Mask

  72. RippleDrawableCompat private Bitmap convertDrawableToBitmap(Drawable mask, Bounds bounds) { Bitmap maskBitmap

    = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); }
  73. RippleDrawableCompat private Bitmap convertDrawableToBitmap(Drawable mask, Bounds bounds) { Bitmap maskBitmap

    = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); Canvas maskCanvas = new Canvas(maskBitmap); }
  74. RippleDrawableCompat private Bitmap convertDrawableToBitmap(Drawable mask, Bounds bounds) { Bitmap maskBitmap

    = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ALPHA_8); Canvas maskCanvas = new Canvas(maskBitmap); mask.setBounds(bounds); mask.draw(maskCanvas); return maskBitmap; }
  75. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { @Override protected void

    onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); // we have a Drawable to use as a mask if (mask != null) { Bitmap maskBitmap = convertDrawableToBitmap(mask, bounds) // this shader will limit where drawing takes place to only the mask area maskShader = new BitmapShader(maskBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); } } }
  76. BitmapShader

  77. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { @Override protected boolean

    onStateChange(int[] state) { // get the color for the current state int color = colorStateList.getColorForState(state, Color.TRANSPARENT); } }
  78. RippleDrawableCompat public class RippleDrawableCompat extends LayerDrawable { @Override protected boolean

    onStateChange(int[] state) { // get the color for the current state int color = colorStateList.getColorForState(state, Color.TRANSPARENT); LinearGradient gradient = new LinearGradient(0, 0, 0, 0, color, color, Shader.TileMode.CLAMP); ComposedShader shader = new ComposeShader(maskShader, gradient, PorterDuff.Mode.SRC_IN); paint.setShader(shader); } }
  79. None
  80. Thank You!

  81. getBounds() The story of Drawables and their View masters Jamie

    Huson + Lisa Neigut | 20 Sept 2014