Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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/

NeiL saitug

September 20, 2014
Tweet

More Decks by NeiL saitug

Other Decks in Programming

Transcript

  1. getBounds()
    The story of Drawables and their View masters
    Jamie Huson + Lisa Neigut | 20 Sept 2014

    View Slide

  2. View Slide

  3. Drawables!

    View Slide

  4. What is a Drawable?

    View Slide

  5. "A drawable is a positioned entity
    that is to be drawn on a canvas."
    -Cyril Mottier

    View Slide

  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

    View Slide

  7. NinePatchDrawable

    View Slide

  8. ShapeDrawable

    View Slide

  9. StateListDrawable

    View Slide

  10. BitmapDrawable

    View Slide

  11. LayerDrawable

    View Slide

  12. So many options…

    View Slide

  13. BadgeDrawable

    View Slide

  14. IconDrawable

    View Slide

  15. IconDrawable

    View Slide

  16. Custom Drawables

    View Slide

  17. Using built-in Drawables
    /res
    ../drawable
    .. layered_drawable.xml






    android:top="@dimen/gen_avatar_border_shadow"/>



    ...

    View Slide

  18. Using built-in Drawables
    layered_drawable.xml






    android:top="@dimen/gen_avatar_border_shadow"/>



    ...
    layout.xml
    ...
    android:background="@drawable/layered_drawable"
    />

    View Slide

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

    View Slide

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

    layout.xml
    ...
    android:background="@drawable/custom_drawable”
    />

    View Slide

  21. Runtime Error
    Caused by: org.xmlpull.v1.XmlPullParserException:
    Binary XML file line #2: __invalid drawable tag custom__

    View Slide

  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;
    }

    View Slide

  23. Seriously?

    View Slide

  24. View Slide

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

    View Slide

  26. The Drawable API

    View Slide

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

    View Slide

  28. @Override
    draw(Canvas canvas)

    View Slide

  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

    View Slide

  30. @Override
    getOpacity()

    View Slide

  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;
    }

    View Slide

  32. @Override
    getIntrinsicHeight\Width()

    View Slide

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

    View Slide

  34. @Override
    getMinimumHeight\Width()

    View Slide

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

    View Slide

  36. @Override
    getConstantState() and mutate()

    View Slide

  37. @Override
    getConstantState()

    View Slide

  38. @Override
    mutate()
    4 Make this drawable mutable,
    independent of other
    instances.
    4 Basically creates an
    ‘independent’ state.

    View Slide

  39. Drawables + Views

    View Slide

  40. Drawable.Callback

    View Slide

  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);
    }

    View Slide

  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);
    ...
    }
    ...
    }

    View Slide

  43. Drawable.Callback
    public abstract class Drawable {
    private WeakReference mCallback = null;
    ...
    public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
    callback.invalidateDrawable(this);
    }
    }
    ...
    }

    View Slide

  44. Drawable Bounds

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. View Render Passes
    4 Measurement
    4 Layout
    4 Draw

    View Slide

  49. View Render Passes
    4 Measurement
    4 Layout
    4 Draw

    View Slide

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

    View Slide

  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

    View Slide

  52. onDraw
    4 Calculates the bounds for that drawable*

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  60. The Future of Drawable

    View Slide

  61. Ripple

    View Slide

  62. View Slide

  63. View Slide

  64. Ripple
    public class Drawable {
    ...
    public void setHotspot(float x, float y) { /* compiled code */ }
    ...
    }

    View Slide

  65. Pre-L?

    View Slide

  66. RippleDrawableCompat
    public class RippleDrawableCompat extends LayerDrawable {
    }

    View Slide

  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;
    }
    }

    View Slide

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

    View Slide

  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;
    }
    }

    View Slide

  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);
    }
    }
    }

    View Slide

  71. Mask

    View Slide

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

    View Slide

  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);
    }

    View Slide

  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;
    }

    View Slide

  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);
    }
    }
    }

    View Slide

  76. BitmapShader

    View Slide

  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);
    }
    }

    View Slide

  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);
    }
    }

    View Slide

  79. View Slide

  80. Thank You!

    View Slide

  81. getBounds()
    The story of Drawables and their View masters
    Jamie Huson + Lisa Neigut | 20 Sept 2014

    View Slide