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

Øredev 2017: Measure. Layout. Draw. Repeat.

Øredev 2017: Measure. Layout. Draw. Repeat.

Huyen's presentation at Øredev 2017 on custom Views and ViewGroups in Android.

Huyen Tue Dao

November 09, 2017
Tweet

More Decks by Huyen Tue Dao

Other Decks in Programming

Transcript

  1. 1
    MEASURE.
    LAYOUT.
    DRAW.
    REPEAT.
    HUYEN TUE DAO
    @QUEENCODEMONKEY

    View Slide

  2. 2
    MEASURE.
    LAYOUT.
    DRAW.
    REPEAT.
    CUSTOM VIEWS
    WHY?
    CUSTOM LOOK/BEHAVIOR
    REUSABILITY/MODULARITY
    PERFORMANCE

    View Slide

  3. 3
    MEASURE.
    LAYOUT.
    DRAW.
    REPEAT.
    CUSTOM VIEWS
    WHY NOT?
    DIFFICULT
    TIME-CONSUMING
    DRAWING/INTERACTIONS/A11Y
    → ON YOUR OWN

    View Slide

  4. 4
    MEASURE.
    LAYOUT.
    DRAW.
    REPEAT.
    HOW ANDROID DRAWS VIEWS
    HOW TO GO CUSTOM
    DRAW
    MEASURE
    VIEW GROUPS
    LAYOUT

    View Slide

  5. R
    MEASURE
    LAYOUT
    DRAW
    INFLATION/INSTANTIATION
    DEPTH-FIRST
    TRAVERSAL
    HOW ANDROID
    DRAWS VIEWS
    onLayout()
    onDraw()
    onMeasure()

    View Slide

  6. R
    PARENT PASSES
    CONSTRAINTS
    CHILD CALCULATES
    MEASURED WIDTH/HEIGHT
    MEASURE
    HOW ANDROID
    DRAWS VIEWS
    onMeasure()
    LAYOUT
    onLayout()
    DRAW
    onDraw()
    MEASURE OWN
    CHILDREN (IF ANY)

    View Slide

  7. R
    PARENT SIZES AND
    POSITIONS CHILD
    MEASURE
    LAYOUT
    DRAW
    HOW ANDROID
    DRAWS VIEWS
    onMeasure()
    onLayout()
    onDraw()

    View Slide

  8. PARENT DRAWS
    PARENT TELLS
    CHILDREN TO DRAW
    MEASURE
    LAYOUT
    DRAW
    0
    1
    Z
    parent
    children
    HOW ANDROID
    DRAWS VIEWS
    onMeasure()
    onLayout()
    onDraw()

    View Slide

  9. DIFFERENT WAYS TO
    GO CUSTOM
    VIEW VIEWGROUP
    EXTEND
    EXISTING
    CUSTOM LAYOUTS ON ANDROID, @LUCASRATMUNDO
    EXTEND
    BASE
    CUSTOM EXTENDED*
    TWEAK BEHAVIOR
    ARGUABLY SIMPLEST
    COMPOSITE
    MODULAR UI
    LAYOUT REUSE
    FLAT CUSTOM
    CUSTOM DRAWING/BEHAVIOR
    PERFORMANCE
    CUSTOM COMPOSITE
    CUSTOM LAYOUT LOGIC
    PERFORMANCE

    View Slide

  10. WHAT DO YOU NEED
    TO IMPLEMENT?
    VIEW
    MEASURE
    LAYOUT
    DRAW
    onMeasure()
    onLayout()
    onDraw()
    NO
    NO
    YES
    BUT YOU SHOULD
    REUSABILITY/FLEXIBILITY
    VIEW HAS NO CHILDREN
    REQUIRED FOR VIEW
    TO APPEAR

    View Slide

  11. 11
    WHAT WE WILL BUILD.
    GREAT, HUH?

    View Slide

  12. 12
    /**
    * An interface for a view implementing a tally counter.
    !*/
    public interface TallyCounter {
    /**
    * Reset the counter.
    !*/
    void reset();
    /**
    * Increment the counter.
    !*/
    void increment();
    /**
    * @return The current count of the counter.
    !*/
    int getCount();
    /**
    * Set the counter value.
    !*/
    void setCount(int count);
    }

    View Slide

  13. VIEW CONSTRUCTORS
    CREATE NEW FROM CODE
    View(ContextAcontext)
    CREATE FROM XML
    View(ContextBcontext, AttributeSetBattrs)
    CREATE FROM XML WITH A STYLE FROM A THEME ATTRIBUTE
    View(ContextCcontext, AttributeSetCattrs,
    intCdefStyleAttr)
    CREATE FROM XML WITH A STYLE FROM A THEME ATTRIBUTE OR STYLE RESOURCE
    View(ContextDcontext, AttributeSetDattrs,
    intDdefStyleAttr, intDdefStyleRes)
    A DEEP DIVE INTO ANDROID VIEW CONSTRUCTORS
    @DANLEW42

    View Slide

  14. 14
    public TallyCounterView(ContextAcontext) {
    this(context, null);
    }
    public TallyCounterView(ContextBcontext, AttributeSetBattrs) {
    super(context, attrs);
    }A

    View Slide

  15. 15
    public TallyCounterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    }A

    View Slide

  16. 16
    public TallyCounterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    !// Set up paints for canvas drawing.
    backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    backgroundPaint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));
    linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    linePaint.setColor(ContextCompat.getColor(context, R.color.colorAccent));
    linePaint.setStrokeWidth(1f);
    numberPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    numberPaint.setColor(ContextCompat.getColor(context, android.R.color.white));
    }A

    View Slide

  17. 17
    public TallyCounterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    !// Set up paints for canvas drawing.
    backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    backgroundPaint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));
    linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    linePaint.setColor(ContextCompat.getColor(context, R.color.colorAccent));
    linePaint.setStrokeWidth(1f);
    numberPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    numberPaint.setColor(ContextCompat.getColor(context, android.R.color.white));
    !// Set the number text size to be 64sp.
    !// Translate 64sp…
    numberPaint.setTextSize(
    Math.round(64f * getResources().getDisplayMetrics().scaledDensity));
    }A

    View Slide

  18. 18
    public TallyCounterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    !// Set up paints for canvas drawing.
    backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    backgroundPaint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));
    linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    linePaint.setColor(ContextCompat.getColor(context, R.color.colorAccent));
    linePaint.setStrokeWidth(1f);
    numberPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    numberPaint.setColor(ContextCompat.getColor(context, android.R.color.white));
    !// Set the number text size to be 64sp.
    !// Translate 64sp…
    numberPaint.setTextSize(
    Math.round(64f * getResources().getDisplayMetrics().scaledDensity));
    !// Allocate objects needed for canvas drawing here.
    backgroundRect = new RectF();
    !// Initialize drawing measurements.
    cornerRadius = Math.round(2f * getResources().getDisplayMetrics().density);
    }A

    View Slide

  19. 19
    public TallyCounterView(Context context, AttributeSet attrs) {
    super(context, attrs);
    !// Set up paints for canvas drawing.
    backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    backgroundPaint.setColor(ContextCompat.getColor(context, R.color.colorPrimary));
    linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    linePaint.setColor(ContextCompat.getColor(context, R.color.colorAccent));
    linePaint.setStrokeWidth(1f);
    numberPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    numberPaint.setColor(ContextCompat.getColor(context, android.R.color.white));
    !// Set the number text size to be 64sp.
    !// Translate 64sp…
    numberPaint.setTextSize(
    Math.round(64f * getResources().getDisplayMetrics().scaledDensity));
    !// Allocate objects needed for canvas drawing here.
    backgroundRect = new RectF();
    !// Initialize drawing measurements.
    cornerRadius = Math.round(2f * getResources().getDisplayMetrics().density);
    !// Do initial count setup.
    setCount(0);
    }A

    View Slide

  20. 20
    @Override
    protected void onDraw(Canvas canvas) {
    }A
    WHAT WE WANT
    0000

    View Slide

  21. 21
    @Override
    protected void onDraw(Canvas canvas) {
    !// Grab canvas dimensions.
    final int canvasWidth = canvas.getWidth();
    final int canvasHeight = canvas.getHeight();
    !// Calculate horizontal center.
    final float centerX = canvasWidth * 0.5f;
    }A
    WHAT WE WANT
    0000

    View Slide

  22. 22
    @Override
    protected void onDraw(Canvas canvas) {
    !// Grab canvas dimensions.
    final int canvasWidth = canvas.getWidth();
    final int canvasHeight = canvas.getHeight();
    !// Calculate horizontal center.
    final float centerX = canvasWidth * 0.5f;
    !// Draw the background.
    backgroundRect.set(0f, 0f, canvasWidth, canvasHeight);
    canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
    }A
    WHAT WE WANT
    0000

    View Slide

  23. 23
    @Override
    protected void onDraw(Canvas canvas) {
    !// Grab canvas dimensions.
    final int canvasWidth = canvas.getWidth();
    final int canvasHeight = canvas.getHeight();
    !// Calculate horizontal center.
    final float centerX = canvasWidth * 0.5f;
    !// Draw the background.
    backgroundRect.set(0f, 0f, canvasWidth, canvasHeight);
    canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
    !// Draw baseline.
    final float baselineY = Math.round(canvasHeight * 0.6f);
    canvas.drawLine(0, baselineY, canvasWidth, baselineY, linePaint);
    }A
    WHAT WE WANT
    0000

    View Slide

  24. 24
    @Override
    protected void onDraw(Canvas canvas) {
    !// Grab canvas dimensions.
    final int canvasWidth = canvas.getWidth();
    final int canvasHeight = canvas.getHeight();
    !// Calculate horizontal center.
    final float centerX = canvasWidth * 0.5f;
    !// Draw the background.
    backgroundRect.set(0f, 0f, canvasWidth, canvasHeight);
    canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
    !// Draw baseline.
    final float baselineY = Math.round(canvasHeight * 0.6f);
    canvas.drawLine(0, baselineY, canvasWidth, baselineY, linePaint);
    !// Draw text.
    }A
    WHAT WE WANT
    0000

    View Slide

  25. 25
    @Override
    protected void onDraw(Canvas canvas) {
    !// Grab canvas dimensions.
    final int canvasWidth = canvas.getWidth();
    final int canvasHeight = canvas.getHeight();
    !// Calculate horizontal center.
    final float centerX = canvasWidth * 0.5f;
    !// Draw the background.
    backgroundRect.set(0f, 0f, canvasWidth, canvasHeight);
    canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
    !// Draw baseline.
    final float baselineY = Math.round(canvasHeight * 0.6f);
    canvas.drawLine(0, baselineY, canvasWidth, baselineY, linePaint);
    !// Draw text.
    !// Measure the width of text to display.
    final float textWidth = numberPaint.measureText(displayedCount);
    !// Figure out an x-coordinate that will center the text in the canvas.
    final float textX = Math.round(centerX - textWidth * 0.5f);
    !// Draw.
    canvas.drawText(displayedCount, textX, baselineY, numberPaint);
    }A
    WHAT WE WANT
    0000

    View Slide

  26. 26

    View Slide

  27. REFLECTING STATE CHANGES
    MARK A VIEW AS DIRTY (NEEDS A RE-DRAW)
    invalidate()
    RE-DRAWN “AT SOME POINT IN THE FUTURE”
    BE MORE PRECISE:
    invalidate(int l, int t, int r, int b)

    View Slide

  28. 28
    !//
    !// TallyCounter interface
    !//
    @Override
    public void reset() {
    setCount(0);
    }
    @Override
    public void increment() {
    setCount(count + 1);
    }
    @OverrideA
    public void setCount(int count) {
    count = Math.min(count, MAX_COUNT);
    this.count = count;
    !// Create the string here.
    this.displayedCount = String.format(Locale.getDefault(), "%04d", count);
    }A
    @Override
    public int getCount() {
    return count;
    }

    View Slide

  29. 29
    @OverrideA
    public void setCount(int count) {
    count = Math.min(count, MAX_COUNT);
    this.count = count;
    !// Create the string here.
    this.displayedCount = String.format(Locale.getDefault(), "%04d", count);
    }A

    View Slide

  30. 30
    @OverrideA
    public void setCount(int count) {
    count = Math.min(count, MAX_COUNT);
    this.count = count;
    !// Create the string here.
    this.displayedCount = String.format(Locale.getDefault(), "%04d", count);
    invalidate();
    }A

    View Slide

  31. 31

    View Slide

  32. THINGS TO REMEMBER WHEN DRAWING
    NO ALLOCATIONS IN ONDRAW.
    SERIOUSLY.
    INVALIDATE ONLY WHEN YOU NEED.
    INVALIDATE ONLY WHAT YOU NEED.
    TEXT IS POSITIONED AT THE
    BASELINE.
    DRAW IN PX.
    THINK IN DP AND SP.

    View Slide

  33. View Slide

  34. WHERE’S THAT OTHER BUTTON?
    android:id="@+id/increment"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="16dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop="48dp"
    android:text="@string/click" !/>
    android:id="@+id/tally_counter"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="16dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop="40dp" !/>
    android:id="@+id/reset"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="16dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop=“40dp"
    android:text="@string/reset" !/>

    View Slide

  35. HOW MEASUREMENTS
    ARE MADE
    CHILD
    PARENT
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    setLayoutParams()
    How a child tells its parent how
    it wants to be laid out.
    MarginLayoutParams
    LinearLayout.LayoutParams
    1Child defines LayoutParams
    in XML or Java
    getLayoutParams()

    View Slide

  36. HOW MEASUREMENTS
    ARE MADE
    CHILD
    PARENT
    How a parent communicates
    constraints to children
    child.measure()
    int = { mode | size }
    AT_MOST X → Be any size up to X
    EXACTLY X → Be exactly X
    UNSPECIFIED → No constraints
    1
    2
    Child defines LayoutParams
    in XML or Java
    Parent calculates MeasureSpecs
    and passes to child.measure()

    View Slide

  37. HOW MEASUREMENTS
    ARE MADE
    Must call; otherwise → runtime
    exception.
    CHILD
    PARENT
    setMeasuredDimension()
    1
    2
    3
    Child defines LayoutParams
    in XML or Java
    Parent calculates MeasureSpecs
    and passes to child.measure()
    Child calculates width/height;
    setMeasuredDimension()
    getMeasuredWidth()
    getMeasuredHeight()

    View Slide

  38. HOW MEASUREMENTS
    ARE MADE
    Child defines LayoutParams
    in XML or Java
    1
    Parent calculates MeasureSpecs
    and passes to child.measure()
    2
    Child calculates width/height;
    setMeasuredDimension()
    3
    Parent calls child.layout();
    final child size/position.
    4
    CHILD
    PARENT
    child.layout()
    getWidth()
    getHeight()

    View Slide

  39. 39
    private int reconcileSize(int contentSize, int measureSpec) {
    final int mode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch (mode) {Z
    case MeasureSpec.EXACTLY:
    return specSize;
    case MeasureSpec.AT_MOST:
    if (contentSize < specSize) {B
    return contentSize;
    }Belse {C
    return specSize;
    }C
    case MeasureSpec.UNSPECIFIED:
    default:
    return contentSize;
    }Z
    }Z

    View Slide

  40. 40
    private int reconcileSize(int contentSize, int measureSpec) {
    final int mode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch (mode) {A
    case MeasureSpec.EXACTLY:
    return specSize;
    case MeasureSpec.AT_MOST:
    if (contentSize < specSize) {B
    return contentSize;
    }Belse {C
    return specSize;B
    }C
    case MeasureSpec.UNSPECIFIED:
    default:
    return contentSize;B
    }A
    }Z

    View Slide

  41. 41
    private int reconcileSize(int contentSize, int measureSpec) {
    final int mode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch (mode) {A
    case MeasureSpec.EXACTLY:
    return specSize;A
    case MeasureSpec.AT_MOST:
    if (contentSize < specSize) {B
    return contentSize;Q
    }Belse {C
    return specSize;B
    }C
    case MeasureSpec.UNSPECIFIED:
    default:
    return contentSize;B
    }A
    }Z

    View Slide

  42. 42
    private int reconcileSize(int contentSize, int measureSpec) {
    final int mode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch (mode) {A
    case MeasureSpec.EXACTLY:
    return specSize;A
    case MeasureSpec.AT_MOST:
    if (contentSize < specSize) {B
    return contentSize;Q
    }Belse {C
    return specSize;B
    }C
    case MeasureSpec.UNSPECIFIED:
    default:
    return contentSize;
    }A
    }Z

    View Slide

  43. HOWEVER, YOU SHOULD
    ACTUALLY USE THIS
    public static int View.resolveSize(int size, int measureSpec)

    View Slide

  44. 44
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    }A

    View Slide

  45. 45
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    }A

    View Slide

  46. 46
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    !// Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    }A

    View Slide

  47. 47
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    !// Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    !// Estimate maximum possible height of text.
    final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
    }A

    View Slide

  48. 48
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    !// Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    !// Estimate maximum possible height of text.
    final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
    !// Add padding to maximum width calculation.
    final int desiredWidth =
    Math.round(maxTextWidth + getPaddingLeft() + getPaddingRight());
    }A

    View Slide

  49. 49
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    !// Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    !// Estimate maximum possible height of text.
    final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
    !// Add padding to maximum width calculation.
    final int desiredWidth =
    Math.round(maxTextWidth + getPaddingLeft() + getPaddingRight());
    !// Add padding to maximum height calculation.
    final int desiredHeight =
    Math.round(maxTextHeight * 2f + getPaddingTop() + getPaddingBottom());
    }A

    View Slide

  50. 50
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    !// Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    !// Estimate maximum possible height of text.
    final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
    !// Add padding to maximum width calculation.
    final int desiredWidth =
    Math.round(maxTextWidth + getPaddingLeft() + getPaddingRight());
    !// Add padding to maximum height calculation.
    final int desiredHeight =
    Math.round(maxTextHeight * 2f + getPaddingTop() + getPaddingBottom());
    !// Reconcile size that this view wants to be with the size
    !// that the parent will let it be.
    final int measuredWidth = reconcileSize(desiredWidth, widthMeasureSpec);
    final int measuredHeight = reconcileSize(desiredHeight, heightMeasureSpec);
    }A

    View Slide

  51. 51
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    A
    final Paint.FontMetrics fontMetrics = numberPaint.getFontMetrics();
    !// Measure maximum possible width of text.
    final float maxTextWidth = numberPaint.measureText(MAX_COUNT_STRING);
    !// Estimate maximum possible height of text.
    final float maxTextHeight = -fontMetrics.top + fontMetrics.bottom;
    !// Add padding to maximum width calculation.
    final int desiredWidth =
    Math.round(maxTextWidth + getPaddingLeft() + getPaddingRight());
    !// Add padding to maximum height calculation.
    final int desiredHeight =
    Math.round(maxTextHeight * 2f + getPaddingTop() + getPaddingBottom());
    !// Reconcile size that this view wants to be with the size
    !// that the parent will let it be.
    final int measuredWidth = reconcileSize(desiredWidth, widthMeasureSpec);
    final int measuredHeight = reconcileSize(desiredHeight, heightMeasureSpec);
    !// Store the final measured dimensions.
    setMeasuredDimension(measuredWidth, measuredHeight);
    }A

    View Slide

  52. 52

    View Slide

  53. WHAT DO YOU NEED
    TO IMPLEMENT?
    VIEWGROUP
    MEASURE
    LAYOUT
    DRAW
    YES
    YES
    NO
    HAVE TO MEASURE EACH
    CHILD: child.measure()
    (ABSTRACT) HAVE TO LAYOUT
    EACH CHILD: child.layout()
    DOES NOT DRAW BY DEFAULT
    setWillNotDraw
    onMeasure()
    onLayout()
    onDraw()

    View Slide

  54. 54

    View Slide

  55. 55
    xmlns:android="http:"//schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/list_item_padding_vertical"
    android:paddingBottom="@dimen/list_item_padding_vertical"
    android:paddingStart="0dp"
    android:paddingEnd="@dimen/activity_horizontal_margin"
    …>
    android:id="@+id/icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="8dp"
    android:layout_marginEnd="12dp"
    android:contentDescription="@null" !/>
    android:id="@+id/title"
    android:layout_width="wrap_content"
    android:layout_height=“wrap_content" !/>
    android:id="@+id/subtitle"
    android:layout_width="wrap_content"
    android:layout_height=“wrap_content" !/>
    !

    View Slide

  56. 56
    public class SimpleListItem extends ViewGroup {
    @BindView(R.id.icon)
    ImageView icon;
    @BindView(R.id.title)
    TextView titleView;
    @BindView(R.id.subtitle)
    TextView subtitleView;
    /**
    * Constructor.
    *
    * @param context The current context.
    * @param attrs The attributes of the XML tag that is inflating the view.
    !*/
    public SimpleListItem(Context context, AttributeSet attrs) {
    super(context, attrs);
    }
    @Override
    protected void onFinishInflate() {
    super.onFinishInflate();
    ButterKnife.bind(this);
    }

    View Slide

  57. VALIDATE
    checkLayoutParams()
    VIEW WITHOUT LAYOUT PARAMETERS
    generateDefaultLayoutParams()
    VIEW WITH INVALID LAYOUT PARAMETERS
    generateLayoutParams(ViewGroup.LayoutParams p)
    VIEW WITH LAYOUT XML ATTRIBUTES
    generateLayoutParams(AttributeSet attrs)
    CUSTOM LAYOUT
    PARAMETERS
    ViewGroup.LayoutParams
    (width, height)
    MarginLayoutParams
    (width, height)
    (left, top, right, bottom)

    View Slide

  58. USEFUL MEASUREMENT
    METHODS
    MEASURE + PADDING
    measureChild(…)
    measureChildren(…)
    MEASURE SPEC + CHILD LAYOUT PARAMETERS
    getChildMeasureSpec(…)
    MEASURE + PADDING + MARGINS + SPACE USED
    measureChildWithMargins(…)
    “The heavy lifting is done
    in getChildMeasureSpec.”

    View Slide

  59. 59
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof MarginLayoutParams;
    }
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
    return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
    }
    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return generateDefaultLayoutParams();
    }

    View Slide

  60. 60
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    }A
    WHAT WE WANT

    View Slide

  61. 61
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    !// Measure icon.
    measureChildWithMargins(icon, widthMeasureSpec, 0, heightMeasureSpec, 0);
    }A
    WHAT WE WANT

    View Slide

  62. 62
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    !// Measure icon.
    measureChildWithMargins(icon, widthMeasureSpec, 0, heightMeasureSpec, 0);
    !// Figure out how much width and height the icon used.
    MarginLayoutParams lp = (MarginLayoutParams) icon.getLayoutParams();
    int widthUsed = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int heightUsed = 0;
    }A
    WHAT WE WANT

    View Slide

  63. 63
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    !// Measure icon.
    measureChildWithMargins(icon, widthMeasureSpec, 0, heightMeasureSpec, 0);
    !// Figure out how much width and height the icon used.
    MarginLayoutParams lp = (MarginLayoutParams) icon.getLayoutParams();
    int widthUsed = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int heightUsed = 0;
    !// Measure title
    measureChildWithMargins(
    titleView,
    widthMeasureSpec, widthUsed,
    heightMeasureSpec, heightUsed
    );
    }A
    WHAT WE WANT

    View Slide

  64. 64
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    !// Measure icon.
    measureChildWithMargins(icon, widthMeasureSpec, 0, heightMeasureSpec, 0);
    !// Figure out how much width and height the icon used.
    MarginLayoutParams lp = (MarginLayoutParams) icon.getLayoutParams();
    int widthUsed = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int heightUsed = 0;
    !// Measure title
    measureChildWithMargins(
    titleView,
    widthMeasureSpec, widthUsed,
    heightMeasureSpec, heightUsed
    );
    !// Measure the Subtitle.
    measureChildWithMargins(A
    subtitleView,
    widthMeasureSpec, widthUsed,A
    heightMeasureSpec, titleView.getMeasuredHeight());
    }A
    WHAT WE WANT

    View Slide

  65. 65
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    …A
    }A
    WHAT WE WANT

    View Slide

  66. 66
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    …A
    !// Calculate this view's measured width and height.
    }A
    WHAT WE WANT

    View Slide

  67. 67
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    …A
    !// Calculate this view's measured width and height.
    !// Figure out how much total space the icon used.
    int iconWidth = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int iconHeight = icon.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    lp = (MarginLayoutParams) titleView.getLayoutParams();
    !// Figure out how much total space the title used.
    int titleWidth = titleView.getMeasuredWidth() +
    lp.leftMargin + lp.rightMargin;
    int titleHeight = titleView.getMeasuredHeight() +
    lp.topMargin + lp.bottomMargin;
    lp = (MarginLayoutParams) subtitleView.getLayoutParams();
    !// Figure out how much total space the subtitle used.
    int subtitleWidth = subtitleView.getMeasuredWidth() +
    lp.leftMargin +lp.rightMargin;
    int subtitleHeight = subtitleView.getMeasuredHeight() +
    lp.topMargin + lp.bottomMargin;
    }A
    WHAT WE WANT

    View Slide

  68. 68
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    …A
    !// The width taken by the children + padding.
    int width = getPaddingTop() + getPaddingBottom() +
    iconWidth + Math.max(titleWidth, subtitleWidth);
    !// The height taken by the children + padding.
    int height = getPaddingTop() + getPaddingBottom() +
    Math.max(iconHeight, titleHeight + subtitleHeight);
    }A
    WHAT WE WANT

    View Slide

  69. 69
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {A
    …A
    !// The width taken by the children + padding.
    int width = getPaddingTop() + getPaddingBottom() +
    iconWidth + Math.max(titleWidth, subtitleWidth);
    !// The height taken by the children + padding.
    int height = getPaddingTop() + getPaddingBottom() +
    Math.max(iconHeight, titleHeight + subtitleHeight);
    !// Reconcile the measured dimensions with the this view's constraints and
    !// set the final measured width and height.
    setMeasuredDimension(
    resolveSize(width, widthMeasureSpec),
    resolveSize(height, heightMeasureSpec)
    );
    }A
    WHAT WE WANT

    View Slide

  70. 70
    @Override
    protected void onLayout(boolean changed,
    int left, int top, int right, int bottom) {A
    }A
    WHAT WE WANT

    View Slide

  71. 71
    @Override
    protected void onLayout(boolean changed,
    int left, int top, int right, int bottom)
    MarginLayoutParams layoutParams = (MarginLayoutParams) icon.getLayoutParams();
    !// Figure out the x-coordinate and y-coordinate of the icon.
    int x = getPaddingLeft() + layoutParams.leftMargin;
    int y = getPaddingTop() + layoutParams.topMargin;
    }A
    WHAT WE WANT

    View Slide

  72. 72
    @Override
    protected void onLayout(boolean changed,
    int left, int top, int right, int bottom) {A
    MarginLayoutParams layoutParams = (MarginLayoutParams) icon.getLayoutParams();
    !// Figure out the x-coordinate and y-coordinate of the icon.
    int x = getPaddingLeft() + layoutParams.leftMargin;
    int y = getPaddingTop() + layoutParams.topMargin;
    !// Layout the icon.
    icon.layout(x, y,
    x + icon.getMeasuredWidth(), y + icon.getMeasuredHeight());
    }A
    WHAT WE WANT

    View Slide

  73. 73
    @Override
    protected void onLayout(boolean changed,
    int left, int top, int right, int bottom) {A

    !// Calculate the x-coordinate of the title: icon's right coordinate +
    !// the icon's right margin.
    x += icon.getMeasuredWidth() + layoutParams.rightMargin;
    !// Add in the title's left margin.
    layoutParams = (MarginLayoutParams) titleView.getLayoutParams();
    x += layoutParams.leftMargin;
    !// Calculate the y-coordinate of the title: this ViewGroup's top padding +
    !// the title's top margin
    y = getPaddingTop() + layoutParams.topMargin;
    !// Layout the title.
    titleView.layout(x, y,
    x + titleView.getMeasuredWidth(),
    y + titleView.getMeasuredHeight());
    }A
    WHAT WE WANT

    View Slide

  74. 74
    @Override
    protected void onLayout(boolean changed,
    int left, int top, int right, int bottom) {A

    !// The subtitle has the same x-coordinate as the title.
    !// So no more calculating there.
    !// Calculate the y-coordinate of the subtitle: the title's bottom
    !// coordinate + the title's bottom margin.
    y += titleView.getMeasuredHeight() + layoutParams.bottomMargin;
    layoutParams = (MarginLayoutParams) subtitleView.getLayoutParams();
    !// Add in the subtitle's top margin.
    y += layoutParams.topMargin;
    !// Layout the subtitle.
    subtitleView.layout(x, y,
    x + subtitleView.getMeasuredWidth(), y + subtitleView.getMeasuredHeight());
    }A
    WHAT WE WANT

    View Slide

  75. 75
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    !// Measure icon.
    measureChildWithMargins(icon, widthMeasureSpec, 0, heightMeasureSpec, 0);
    !// Figure out how much width the icon used.
    MarginLayoutParams lp = (MarginLayoutParams) icon.getLayoutParams();
    int widthUsed = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int heightUsed = 0;
    !// Measure title
    measureChildWithMargins(
    titleView,
    !// Pass width constraints and width already used.
    widthMeasureSpec, widthUsed,
    !// Pass height constraints and height already used.
    heightMeasureSpec, heightUsed
    );
    !// Measure the Subtitle.
    measureChildWithMargins(
    subtitleView,
    !// Pass width constraints and width already used.
    widthMeasureSpec, widthUsed,
    !// Pass height constraints and height already used.
    heightMeasureSpec, titleView.getMeasuredHeight());
    !// Calculate this view's measured width and height.
    !// Figure out how much total space the icon used.
    int iconWidth = icon.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int iconHeight = icon.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    lp = (MarginLayoutParams) titleView.getLayoutParams();
    !// Figure out how much total space the title used.
    int titleWidth = titleView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int titleHeight = titleView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    lp = (MarginLayoutParams) subtitleView.getLayoutParams();
    !// Figure out how much total space the subtitle used.
    int subtitleWidth = subtitleView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
    int subtitleHeight = subtitleView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
    !// The width taken by the children + padding.
    int width = getPaddingTop() + getPaddingBottom() +
    iconWidth + Math.max(titleWidth, subtitleWidth);
    !// The height taken by the children + padding.
    int height = getPaddingTop() + getPaddingBottom() +
    Math.max(iconHeight, titleHeight + subtitleHeight);
    !// Reconcile the measured dimensions with the this view's constraints and
    !// set the final measured width and height.
    setMeasuredDimension(
    resolveSize(width, widthMeasureSpec),
    resolveSize(height, heightMeasureSpec)
    );
    }
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    MarginLayoutParams layoutParams = (MarginLayoutParams) icon.getLayoutParams();
    !// Figure out the x-coordinate and y-coordinate of the icon.
    int x = getPaddingLeft() + layoutParams.leftMargin;
    int y = getPaddingTop() + layoutParams.topMargin;
    !// Layout the icon.
    icon.layout(x, y, x + icon.getMeasuredWidth(), y + icon.getMeasuredHeight());
    !// Calculate the x-coordinate of the title: icon's right coordinate +
    !// the icon's right margin.
    x += icon.getMeasuredWidth() + layoutParams.rightMargin;
    !// Add in the title's left margin.
    layoutParams = (MarginLayoutParams) titleView.getLayoutParams();
    x += layoutParams.leftMargin;
    !// Calculate the y-coordinate of the title: this ViewGroup's top padding +
    !// the title's top margin
    y = getPaddingTop() + layoutParams.topMargin;
    !// Layout the title.
    titleView.layout(x, y, x + titleView.getMeasuredWidth(), y + titleView.getMeasuredHeight());
    !// The subtitle has the same x-coordinate as the title. So no more calculating there.
    !// Calculate the y-coordinate of the subtitle: the title's bottom coordinate +
    !// the title's bottom margin.
    y += titleView.getMeasuredHeight() + layoutParams.bottomMargin;
    layoutParams = (MarginLayoutParams) subtitleView.getLayoutParams();
    !// Add in the subtitle's top margin.
    y += layoutParams.topMargin;
    !// Layout the subtitle.
    subtitleView.layout(x, y,
    x + subtitleView.getMeasuredWidth(), y + subtitleView.getMeasuredHeight());
    }

    View Slide

  76. 76

    View Slide

  77. WHERE TO GO FROM HERE…
    ACCESSIBILITY. A11Y FTW. ADD IT.
    @KELLYSHUSTER
    CUSTOM DRAWABLES. CUSTOM DRAWABLE STATES.
    @RHARTER @MARCOSPAULOSD
    ALL THE DRAWING OPERATIONS. PATHS. FILTERS. SHADERS. OH MY!
    @ROMAINGUY @CHIUKI
    HARDWARE ACCELERATION. LEARN ABOUT LAYERS + DISPLAY LISTS.
    @WORKINGKILLS
    GO CUSTOM.
    TOUCH. GESTURES.
    @PBREAULT @CALREN24
    PERFORMANCE. MEASUREMENT. TOOLS.
    @QUEENCODEMONKEY @FRANCOISBLAVOET

    View Slide

  78. speakerdeck.com/queencodemonkey | Slides
    https:"//git.io/vFuV3 | Code
    youtube.com/androiddialogs
    randomlytyping.com
    caster.io
    HUYEN TUE DAO
    @QUEENCODEMONKEY
    THANK YOU!

    View Slide

  79. CUSTOM VIEWS/VIEWGROUPS
    Custom Views and ViewGroups (Part 1) https://caster.io/episodes/
    custom-views-and-viewgroups-part-1/
    Creating Custom Views https://developer.android.com/training/custom-
    views/index.html
    A deep dive into Android View constructors http://blog.danlew.net/
    2016/07/19/a-deep-dive-into-android-view-constructors/
    Google I/O 2013 - Writing Custom Views for Android https://youtu.be/
    NYtB6mlu7vA
    Enhancing Android UI with Custom Views https://newcircle.com/s/post/
    1663/
    tutorial_enhancing_android_ui_with_custom_views_dave_smith_video
    79

    View Slide

  80. CUSTOM VIEWS/VIEWGROUPS
    Custom Android Components http://chiuki.github.io/android-custom-
    components
    Custom Views and Performance https://youtu.be/zK2i7ivzK7M
    Taming Android UIs with Eric Burke of Square https://youtu.be/
    jF6Ad4GYjRU
    Custom Layouts on Android http://lucasr.org/2014/05/12/custom-
    layouts-on-android/
    Layout traversals on Android https://youtu.be/sdkcuvZCh1U
    Custom View Groups https://sriramramani.wordpress.com/
    2015/05/06/custom-viewgroups/
    80

    View Slide

  81. TOUCH/GESTURES
    Understanding Android Views and Gestures https://
    academy.realm.io/posts/understanding-android-views-and-
    gestures-android-meetup-chang-2017/
    Making Sense of the Touch System https://speakerdeck.com/
    pbreault/making-sense-of-the-touch-system | https://
    youtu.be/usBaTHZdXSI
    Making the View Interactive https://developer.android.com/
    training/custom-views/making-interactive.html
    81

    View Slide

  82. TEXT + DRAWING
    Typography on Android https://youtu.be/OgiZa22Zpus
    Android Textual Layout https://youtu.be/GZ0eKqvzJa8
    Hinting Around: Text Demystified https://
    www.youtube.com/watch?v=VS7co3TrgKE | https://
    speakerdeck.com/rock3r/hinting-around-android-text-
    demystified
    Calligraphy. Custom fonts in Android the easy way…
    https://github.com/chrisjenx/Calligraphy
    82

    View Slide

  83. DRAWING + DRAWABLES
    Fun with Android Shaders and Filters https://chiuki.github.io/
    android-shaders-filters
    Don’t Fear the Canvas https://youtu.be/KH8Ldp39TUk
    Custom drawable states in Android http://charlesharley.com/2012/
    programming/custom-drawable-states-in-android
    Custom Drawables http://ryanharter.com/blog/2015/04/03/
    custom-drawables/
    The Magic World of Drawables https://youtu.be/1YjB1uUfxgE
    Hardware Acceleration https://developer.android.com/guide/
    topics/graphics/hardware-accel.html
    83

    View Slide

  84. PERFORMANCE
    Loving Lean layouts https://youtu.be/gwqQT5NrhUg
    Let’s Sprinkle some #PerfMatters on your ViewGroups https://
    youtu.be/fEtyhj74JhU
    Eugenio Marletti: Clue + Highly Custom/Performant
    Components https://youtu.be/e-xWYiHAhjI
    84

    View Slide