Creating a Custom Layout for Xamarin Forms

First off, there’s an excellent post on this subject at Creating a Custom Layout. My intention with this post is to just go through my experience creating a layout to organise controls into a grid of squares, i.e. the intention was to layout buttons in a 3×3 matrix for a simple TicTacToe game board.

Getting Started

Your class needs to be derived from Layout or Layout and override the OnMeasure method and the LayoutChildren method.

My class will be named Squared grid and it will require the number of rows and columns that we want to display our View in, so for example here’s the XAML I want to use (excluding namespace and view bindings)

<SquareGrid Rows="3" Columns="3">
   <Button />
   <Button />
   <Button />
   <Button />
   <Button />
   <Button />
   <Button />
   <Button />
   <Button />
</SquareGrid>

So my expectation would be that we will have three rows and three columns of equal width/height and display buttons in each “cell” of the grid.

Note: I’m not going to handle situations where the number of controls doesn’t match the expected rows/columns etc.

Here’s the code (excluding OnMeasure and LayoutChildren for now)

public class SquareGrid : Layout<View>
{
   public static readonly BindableProperty RowsProperty =
      BindableProperty.Create("Rows",
         typeof(int),
         typeof(SquareGrid),
         0,
         propertyChanged: (bindable, oldValue, newValue) =>
            ((SquareGrid)bindable).NativeSizeChanged(),
         validateValue: Validate);

   public static readonly BindableProperty ColumnsProperty = 
      BindableProperty.Create("Columns", 
         typeof(int), 
         typeof(SquareGrid),
         0,
         propertyChanged: (bindable, oldValue, newValue) => 
            ((SquareGrid)bindable).NativeSizeChanged(),
            validateValue: Validate);

   public int Rows
   {
      get => (int)GetValue(RowsProperty);
      set => SetValue(RowsProperty, value);
   }

   public int Columns
   {
      get => (int)GetValue(ColumnsProperty);
      set => SetValue(ColumnsProperty, value);
   }

   private static bool Validate(BindableObject bindable, object value)
   {
      return (int)value >= 0;
   }
}

If you’re used to WPF then, whilst the name BindableProperty don’t exist in WPF (they’re DependencyProperty instead) you’ll probably understand what’s going on, for others reading this – basically we’ve created two properties that are available in XAML, Rows and Columns. We’ve created BindableProperty static’s to handle the storage, binding etc. to these properties. The default values for both Rows and Columns is set to 0 and in both cases, if there’s a change to the property then we call the NativeSizeChanged to alert the layout of changes and also ensure, using validation methods, that the Validate method to check the supplied Rows and Columns properties are within certain bounds (in this case that they’re greater than or equal to 0).

Children

The controls/views (from the example above, the Button objects) become the Children of the layout. In some (maybe most cases) we will want to ask the children for their measurements, i.e. their minimum size requirements and hence would use code like this

foreach (var child in Children)
{
   var childSizeRequest = child.Measure(widthConstraint, heightConstraint);
   // handle any childSizeRequest
}

In our SquareLayout we’re actually going to ignore what the control/view requires and instead force it into the available space, but this gives you an example of how you might handle measurements from the children during the next section, the OnMeasure method.

OnMeasure

Here’s the method signature

protected override SizeRequest OnMeasure(
   double widthConstraint, double heightConstraint)
{
}

The OnMeasure method may be called, depending upon where our SquareGrid is placed and depending upon constraints of any outer layout. For example, if the SquareGrid is within a Grid.Row and that Row height is “*” then OnMeasure is not called. OnMeasure is called when the outer layout is asking “how much space do you require?”. In the case of “*”, we can think of it more like “this is how much space you’ve got”.

In cases where OnMeasure is called, the widthConstraint or heightConstaint might be set to infinity. For example, if the SquareGrid is within a StackLayout, the StackLayout, in portrait orientation, will have not constrain height, hence heightConstraint will by set to infinity. Likewise with a landscape orientation the widthConstraint will be set to infinity. Therefore, when you are calculating the SizeRequest to return from OnMeasure, you will need to handle infinity situations.

This SquareLayout will ignore the child controls measurement requirements and instead will take all the available width or height to create a square of layout space. Hence in a scenario where this layout in within a GridLayout with “Auto” sizing, the SquareGrid will just say it requires an equal width and height based upon the minimum of the two.

Here’s the code

protected override SizeRequest OnMeasure(
   double widthConstraint, 
   double heightConstraint)
{
   var w = double.IsInfinity(widthConstraint) ? 
      double.MaxValue : widthConstraint;
   var h = double.IsInfinity(heightConstraint) ? 
      double.MaxValue : heightConstraint;

   var square = Math.Min(w, h);
   return new SizeRequest(new Size(square, square));
}

LayoutChildren

So OnMeasure is called when the parent wants to ask how much space do you require, and the LayoutChildren (as the name suggests) is when the Layout control is asked to layout it’s children given the x, y, width and height as the bounding rectangle where it should layout it’s children. Here’s a simple example of the code I’m using

protected override void LayoutChildren(
   double x, double y, 
   double width, double height)
{
   var square = Math.Min(width / Columns, height / Rows);

   var startX = x + (width - square * Columns) / 2;
   var startY = y;

   var rect = new Rectangle(startX, startY, square, square);
   var c = 0;
   foreach (var child in Children)
   {
      LayoutChildIntoBoundingRegion(child, rect);

      if (child.IsVisible)
      {
         rect.X += square;
         if (++c >= Columns)
         {
            rect.Y += rect.Height;
            rect.X = startX;
            c = 0;
         }
      }
   }
}

Notice we use the LayoutChildIntoBoundingRegion which ultimately calls the child.Layout but applies margins etc. for us.