Principles of Custom Layouts: A semi article

satya - Sat Nov 10 2012 07:58:59 GMT-0500 (Eastern Standard Time)

Here are the general steps at a high level


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 with any additional layout attributes

Override layout params construction methods

Test it with exact, match parent, wrap content, and weights

satya - Sat Nov 10 2012 08:03:50 GMT-0500 (Eastern Standard Time)

Goal of the sample that I am going to cover

To document all the basics of creating a custom layout like a linear layout. I will use flow layout as an example based on the flow layout explained by Romain in on his presentations. The link to this presentation is on the right hand side of this page.

Although it is based on Romain's presentation I will go into lot more fundamentals around onMeasure(). I will explain the obvious questions a beginner would have around using viewgroup.measureChild. This is the meat of understanding custom layouts. I have also used a slightly different algorithm for measuring.

Otherwise I will essentially take a series of buttons and lay them out horizontally and wrap them around when we run out of space.

satya - Sat Nov 10 2012 08:04:39 GMT-0500 (Eastern Standard Time)

The layout will look like this when done

satya - Sat Nov 10 2012 08:07:54 GMT-0500 (Eastern Standard Time)

So here is how flow layout extends and uses its constructors


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) {
      //Do any common initializations you may have
      //It is empty in our implementation
   }

satya - Sat Nov 10 2012 08:10:36 GMT-0500 (Eastern Standard Time)

Explanation

There is quite like any other extension of custom views. See the other examples on this site how custom views work or custom compound views work. They all work the same way. Extend a base class (View or an existing layout like a LinearLayout or here the ViewGroup)

In all cases you can have custom attributes. And we define them in attrs.xml and read them in constructors.

in our case we have horizontal spacing and vertical spacing for our lines as we wrap the controls around.

I will show you the attrs.xml to support this constructor

satya - Sat Nov 10 2012 08:13:20 GMT-0500 (Eastern Standard Time)

attrs.xml


<resources>
<declare-styleable name="CircleView">
    <attr name="strokeWidth" format="integer"/>
    <attr name="strokeColor" format="color|reference" />
</declare-styleable>
<declare-styleable name="DurationComponent">
    <attr name="durationUnits">
      <enum name="days" value="1"/>
      <enum name="weeks" value="2"/>
    </attr>
</declare-styleable>
<declare-styleable name="FlowLayout">
    <attr name="hspace" format="dimension"/>
    <attr name="vspace" format="dimension" />
</declare-styleable>
<declare-styleable name="FlowLayout_Layout">
    <attr name="space" format="dimension"/>
</declare-styleable>
</resources>

satya - Sat Nov 10 2012 08:17:07 GMT-0500 (Eastern Standard Time)

I have included here custom attributes for other custom components as well

Specific to the flow layout are hspace and vspace.

Shortly I will cover the layout params for out flow layout. they are also in this source code in one place.

The layout params are used in the children view definitions for flow layout in the layout xml files.

By convention we call styleables the name of the layout and the layout name followed by _layout. these names are arbitrary. If you have a defying logic you can defy!

satya - Sat Nov 10 2012 08:26:46 GMT-0500 (Eastern Standard Time)

Now we need to show you onMeasure

There are a number of things we want to tell you about onmeasure.

However none of these you will understand until you actually sit down and code at least one onmeasure yourself. You have to get your mind to focus on the mechanics of onmeasure.

Key things are these:

when you extend the viewgroup make sure your measure is a sum of measures of your childen in some manner. this means you have to measure your children. You will legitimately ask why you shouldn't use child.measure(). Look for an answer to this question FULLY. Then you understand onMeasure and what is required.

The short answer is use ViewGroup.measureChild() and then just add those measures together. This is because the measureChild() is a smart method that takes into the measure spec of the ViewGroup (at most, unspecified, exact) and properly asks the children with a different measurespec if required. If you call child.measur() then you have to do this logic yourself.

See our research log on custom layouts for the source code ViewGroup.measureChild() to understand this.

Then we know that we also need to oveeride onLayout() where we position the child views. the exercise is identical to measuring. So why do this twice. If only we can remember the origin of each child then we can just use that in the onLayout(). Romain showed a trick to use the layout parameters class to stuff the origin and retrieve them in the onlayout phase.

Once you have the total size of the FlowLayout which is the sum of all its children, you call he prefabricated method of resolveSize() to figure out its real size. this method also takes into accound the measure spec variation and clips the size to the appropriate size.

satya - Sat Nov 10 2012 08:27:44 GMT-0500 (Eastern Standard Time)

So here is how you use a pencil and paper to place your controls

here is a slightly better picture of this to match the code below

satya - Sat Nov 10 2012 08:28:35 GMT-0500 (Eastern Standard Time)

Here is onMeasure implementation of this picture


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

satya - Sat Nov 10 2012 08:29:23 GMT-0500 (Eastern Standard Time)

Here is how we override onLayout


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

satya - Sat Nov 10 2012 08:32:29 GMT-0500 (Eastern Standard Time)

Lets see the custom layout params to support this stuffing


//*********************************************************
   //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-param

See the "x" and "y" public members on the layout class. Also notice how we have read the child specific layout parameter space using a similar constructor and typed array approach.

satya - Sat Nov 10 2012 08:33:22 GMT-0500 (Eastern Standard Time)

You also have to override a number of layout construction methods for the FLowLayout


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

satya - Sat Nov 10 2012 08:36:30 GMT-0500 (Eastern Standard Time)

Some explanation

the first method is called to instantiate the derived layouts layout parameter class. if we dont override this then the base view group will instantiate its implementation the MarginLayoutParams. And that guy or gal will not have the "x" and "y" in it nor it will read the custom "space" variable.

the rest of the methods are appropriately calls at various times. But this is the standard code that can be copied and pasted but use your implementation of the layout parameters.

satya - Sat Nov 10 2012 08:37:52 GMT-0500 (Eastern Standard Time)

Here is the entire source code in one place


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 - Sat Nov 10 2012 08:38:33 GMT-0500 (Eastern Standard Time)

Here is how we use children in a layout definition


<?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 - Sat Nov 10 2012 08:41:49 GMT-0500 (Eastern Standard Time)

References

Most of the references used for this exercise is on the right hand side of this page.

I have two previous research logs and articles on custom views (extending a view class) and compound controls (extending a layout). They cover lot of the basics for custom attributes and the inner workings of onMeasure(). So this knowledge forms the basis for this work.

I am immensely thankful for Romains presentation at Parleys to lay the ground and authoritatively state a position on how these custom layouts are planned.

Finally do look at the base class source code for ViewGroup to understand the onMeasure(). we will include it here for completion of this topic.

satya - Sat Nov 10 2012 08:42:30 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);
    }