Custom Views - II

This is part 2 of two part series

In part 1, I have discussed core view classes and view types of view constructor. This article focuses on view drawing in its layout in parent view.

At high level, a view is created in two phases.

  1. Layout Phase
  2. Drawing Phase

Layout Phase

In layout phase, view parent determines the size and layout of a view, it calculates the size of view and place for laying out view. Layout phase is completed in 2 passes.

  • Measure Pass
  • Layout Pass

Measure Pass

In measure pass, size of a view is calculated (when it wants to know how big a view can be). It starts when a view parent calls

measure(int widthMeasureSpec, int heightMeasureSpec)

method of view with appropriate MeasureSpecs. This method does some work and calls

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

in custom view.

MeasureSpecs

Parameters passed to onMeasure(...) are special parameters. They have integer type but they are actually two parameters encoded in a single integer. Each parameter has a mode and size value and these values can be retrieved by passing it to MeasureSpec.getMode(int) and MeasureSpec.getSize(int).

     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // decode width values
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);

        // decode width values
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
    }

 

MeasureSpecs Modes

Mode gives you a clue about how big a view should be. Mode can be one of the following

  MeasureSpec.AT_MOST
  MeasureSpec.EXACTLY
  MeasureSpec.UNSPECIFIED

MeasureSpec.EXACTLY

This mode tells that parent has measure the size of child view and view should have this size. onMeasure is called with this mode when view is specified in xml with size equals to match_parent or exact size in dps e.g. 40dp.

if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
    // do something
}

MeasureSpec.AT_MOST

onMeasure is called with his mode bit if view is specified in xml with size equals to wrap_content. With this mode bit, android tells that I have this size and you can draw your view with in this size.

if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
    // do something
}

MeasureSpec.UNSPECIFIED

This mode is used when android system wants to query how big this view can be. It’s our responsibliy to provide the system with appropriate size.

if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
    // do something
}

Setting size in onMeasure(...)

In measure pass, after calling onMeasure, parent views expects us to set the size of view using

setMeasuredDimension(width, height);

After calculating size (based on mode bit or any other logic) you must pass this size to parent using above method, failing to call this method will trigger IllegalStateException at runtime.

  java.lang.IllegalStateException: View with id -1: io.github.allaudin.customviews.MyView#onMeasure() did not set the measured dimension by calling setMeasuredDimension()

Default implementation of onMeasure()

The default implementation of onMeasure calls setMeasuredDimension by getting width and height from getSuggestedMinimumWidth() and getSuggestedMinimumHeight().

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

       public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

Note that getDefaultSize returns the same size for both MeasureSpec.AT_MOST and MeasureSpec.EXACTLY.

Layout Pass

Layout pass sets the size of view by using dimensions set in onMeasure. This pass is started when parent view calls layout(...) method of view followed by calling onLayout(...) in derived view.

public void layout(int left, int top, int right, int bottom)
  @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

After setting the size on view, it calls onSizeChanged(..) if the size of view is changed.

 @Override
    protected void onSizeChanged(int width, int height, int oldWidth, int oldWidth) {
        super.onSizeChanged(width, height, oldWidth, oldWidth);
    }

Default implementation of both onSizeChanged and onLayout is no-op.

top