Here are steps involved in creating a custom layout

Inherit from ViewGroup
Override OnMeasure
  Use a pencil and paper to figure out your algorithm
  Use ViewGroup.measureChild(). Don't directly use child.measure()
  (Unless you know what you are doing)
  Use layout params to stuff the origin of each child view
  Take into accound padding
Override OnLayout
  Just retrieve the layout param object and use its origin
Implement custom LayoutParams
Override layout params construction methods
Test it with exact, match parent, wrap content, and weights

satya - Wed Nov 07 2012 11:27:34 GMT-0500 (Eastern Standard Time)

Here is how LinearLayout implements its own layout params


public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public float weight;
    public int gravity = -1;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a =
                c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

        weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
        gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

        a.recycle();
    }

    public LayoutParams(int width, int height) {
        super(width, height);
        weight = 0;
    }

    public LayoutParams(int width, int height, float weight) {
        super(width, height);
        this.weight = weight;
    }

    public LayoutParams(ViewGroup.LayoutParams p) {
        super(p);
    }

    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }

    @Override
    public String debug(String output) {
        return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) +
                ", height=" + sizeToString(height) + " weight=" + weight +  "}";
    }
}

satya - Wed Nov 07 2012 11:31:20 GMT-0500 (Eastern Standard Time)

Here are some overriden methods in the custom layout for layout params


@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LinearLayout.LayoutParams(getContext(), attrs);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    if (mOrientation == HORIZONTAL) {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    } else if (mOrientation == VERTICAL) {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    }
    return null;
}

@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p);
}


// Override to allow type-checking of LayoutParams.
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof LinearLayout.LayoutParams;
}

satya - Wed Nov 07 2012 11:44:29 GMT-0500 (Eastern Standard Time)

Key methods in the measure pass from Romain


child.measure()
ViewGroup.measureChild()
ViewGroup.getChildMeasureSpec()

satya - Wed Nov 07 2012 11:45:27 GMT-0500 (Eastern Standard Time)

Why to use ViewGroup.measureChild?

if you call measure on a child, you are responsible for figuring out the measurespec "appropriately" yourself. You may want to use measureChild().

satya - Wed Nov 07 2012 15:25:37 GMT-0500 (Eastern Standard Time)

LinearLayout.java

LinearLayout.java

Search for: LinearLayout.java

satya - Wed Nov 07 2012 15:27:04 GMT-0500 (Eastern Standard Time)

Source code of LienarLayout.java from gitorious

Source code of LienarLayout.java from gitorious

you can examine the onMeasure and other methods of a custom layout as an example

satya - Fri Nov 09 2012 09:48:41 GMT-0500 (Eastern Standard Time)

Here is how debug is implemented for layouts


public String debug(String output) {
    return output + "ViewGroup.LayoutParams={ width="
            + sizeToString(width) + ", height=" + sizeToString(height) + " }";
}

protected static String sizeToString(int size) {
    if (size == WRAP_CONTENT) {
        return "wrap-content";
    }
    if (size == MATCH_PARENT) {
        return "match-parent";
    }
    return String.valueOf(size);
}

satya - Fri Nov 09 2012 13:43:00 GMT-0500 (Eastern Standard Time)

Here is a complete implementation of a FlowLayout

This is very basic to tell you the structure and nature of a custom layout. I haven't used line breaks. I haven't used padding. I haven't used custom spacing etc. You can easily add those.

The onmeasure() implementation you have to get a pencil and paper and layout the children yourself. Otherwise explaining the logic will not help much.

From Romain Guy's presentation I have picked up how to store the (x,y) coordinates for views in the view layout params itself. That is a nice trick.


public class FlowLayout 
extends ViewGroup 
{
   private int hspace=10;
   private int vspace=10;
   public FlowLayout(Context context) {
      super(context);
      initialize(context);
   }

   public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
      super(context, attrs, defStyle);
      TypedArray t = context.obtainStyledAttributes(attrs,
            R.styleable.FlowLayout, 0, 0);
      hspace = t.getInt(R.styleable.FlowLayout_hspace,
            hspace);
      vspace = t.getInt(R.styleable.FlowLayout_vspace,
            vspace);
      t.recycle();
      initialize(context);
   }

   public FlowLayout(Context context, AttributeSet attrs) {
      this(context, attrs, 0);
   }

   private void initialize(Context context) {
   }

   //This is very basic
   //doesn't take into account padding
   //You can easily modify it to account for padding
   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) 
   {
      //********************
      //Initialize
      //********************
      int rw = MeasureSpec.getSize(widthMeasureSpec);
      int rh = MeasureSpec.getSize(heightMeasureSpec);
      int h = 0; //current height
      int w = 0; //current width
      int h1 = 0, w1=0; //Current point to hook the child to
      
      //********************
      //Loop through children
      //********************
      int numOfChildren = this.getChildCount();
      for (int i=0; i < numOfChildren; i++ )
      {
         //********************
         //Front of the loop
         //********************
         View child = this.getChildAt(i);
         this.measureChild(child,widthMeasureSpec, heightMeasureSpec);
         int vw = child.getMeasuredWidth();
         int vh = child.getMeasuredHeight();
         
         if (w1 + vw > rw)
         {
            //new line: max of current width and current width position
            //when multiple lines are in play w could be maxed out
            //or in uneven sizes is the max of the right side lines
            //all lines don't have to have the same width
            //some may be larger than others
            w = Math.max(w,w1);
            //reposition the point on the next line
            w1 = 0; //start of the line
            h1 = h1 + vh; //add view height to the current heigh
         }
         //********************
         //Middle of the loop
         //********************
         int w2 = 0, h2 = 0; //new point for the next view
         w2 = w1 + vw;
         h2 = h1;
         //latest height: current point + height of the view
         //however if the previous height is larger use that one
         h = Math.max(h,h1 + vh);
         
         //********************
         //Save the current coords for the view 
         //in its layout
         //********************
         LayoutParams lp = (LayoutParams)child.getLayoutParams();
         lp.x = w1;
         lp.y = h1;
      
         //********************
         //Restart the loop 
         //********************
         w1=w2;
         h1=h2;
      }
   //********************
   //End of for 
   //********************
      w = Math.max(w1,w);
      //h = h;
      setMeasuredDimension(
            resolveSize(w, widthMeasureSpec), 
            resolveSize(h,heightMeasureSpec));
   };
   @Override
   protected void onLayout(boolean arg0, int arg1, int arg2, int arg3, int arg4) 
   {
      //Call layout() on children
      int numOfChildren = this.getChildCount();
      for (int i=0; i < numOfChildren; i++ )
      {
         View child = this.getChildAt(i);
         LayoutParams lp = (LayoutParams)child.getLayoutParams();
         child.layout(lp.x,
               lp.y,
               lp.x + child.getMeasuredWidth(), 
               lp.y + child.getMeasuredHeight());
      }
   }
   
   //*********************************************************
   //Layout Param Support 
   //*********************************************************
   @Override
   public LayoutParams generateLayoutParams(AttributeSet attrs) {
       return new FlowLayout.LayoutParams(getContext(), attrs);
   }

   @Override
   protected LayoutParams generateDefaultLayoutParams() {
       return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   }

   @Override
   protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
       return new LayoutParams(p);
   }


   // Override to allow type-checking of LayoutParams.
   @Override
   protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
       return p instanceof FlowLayout.LayoutParams;
   }   
   //*********************************************************
   //Custom Layout Definition 
   //*********************************************************
   public static class LayoutParams extends ViewGroup.MarginLayoutParams {
       public int spacing = -1;
       public int x =0;
       public int y =0;

       public LayoutParams(Context c, AttributeSet attrs) {
           super(c, attrs);
           TypedArray a =
                   c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);

           spacing = a.getInt(R.styleable.FlowLayout_Layout_space, 0);
           a.recycle();
       }

       public LayoutParams(int width, int height) {
           super(width, height);
           spacing = 0;
       }

       public LayoutParams(ViewGroup.LayoutParams p) {
           super(p);
       }

       public LayoutParams(MarginLayoutParams source) {
           super(source);
       }
   }//eof-layout-params   

}// eof-class

satya - Fri Nov 09 2012 13:43:51 GMT-0500 (Eastern Standard Time)

Here is how I have used it


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cc="http://schemas.android.com/apk/res/com.androidbook.compoundControls"    
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="match_parent"
    >
<TextView  
   android:id="@+id/text2"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Welcome to the Compound Controls"
    />
<com.androidbook.compoundControls.FlowLayout
   android:id="@+id/durationControlId"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content"
    >
   <Button android:text="Button1"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="Button2"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="Button3"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="Button4"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="Button5"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="B1"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="B2"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="B3"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="B4"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
   <Button android:text="B5"
       android:layout_width="wrap_content" 
       android:layout_height="wrap_content"
    />
</com.androidbook.compoundControls.FlowLayout>
 
<TextView  
   android:id="@+id/text1"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="Scratch for debug text"
    />
</LinearLayout>

satya - Fri Nov 09 2012 13:54:32 GMT-0500 (Eastern Standard Time)

Implementation onMeasure() is simple but understanding why is very tricky

First of all you know that you receive 3 types of measure specs. at most (for wrap_content), unspecified (for scrolling perhaps), and exact (match parent or exact size).

If you noticed in our implementation we passed that measure spec as is to the child.

If a flow layout has an exact size specified of 100 pixels high, then the measure spec will be 100 exact to the flow layout. But flow layout sends 100 exact to the first child and every child. If the child is not cautious it could use all that size and will leave no space for the children. (But if you use measureChild it seem to make some adjustment. I just saw its soruce code. I will post it in a minute)

Another advise is you can add up all the child sizes to come up with a big size that might exceed the specified size for the view group. This is ok, because the clipping will happen when we call the resolveSize() before setting the measured dimension.

satya - Fri Nov 09 2012 13:55:04 GMT-0500 (Eastern Standard Time)

getChildMeasureSpec (used by measureChild of ViewGroup)


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

satya - Fri Nov 09 2012 13:57:53 GMT-0500 (Eastern Standard Time)

Key observation is you may come in as exact into a view group

But might become at most when gets translated. This is how the view group protects a child from taking the entire space offered.

satya - Fri Nov 09 2012 13:58:27 GMT-0500 (Eastern Standard Time)

For all this to work you have to call ViewGroup.measureChild()

For all this to work you have to call ViewGroup.measureChild()

satya - Fri Nov 09 2012 14:02:16 GMT-0500 (Eastern Standard Time)

This means....

The behavior of measuring the children based on a viewgroup's measure spec is BAKED into view group.

satya - Fri Nov 09 2012 15:35:48 GMT-0500 (Eastern Standard Time)

Here is what it looks like

satya - Fri Nov 09 2012 15:42:32 GMT-0500 (Eastern Standard Time)

At a high level you need a pencil and a paper to figure out your on measure