Motivation
I recently faced the challenge to add Items of different sizes to a Page on a Xamarin.Forms App. The result should be some kind of a Dashboard with Diagrams and info boxes on it. Some diagrams are smaller, some bigger.
The first thing, that came in mind, was a FlexLayout, but this looks a little bit odd to me, because the controls on the main axis share the same size in the secondary axis. That is okay if all controls have the same height or width, but this is not the case.
But what I actually want is something like this:
It’s very tough to arrange the children of a Layout, when every control has a free size defined. To solve this problem, we need to divide the available area in uniform sections. Using this approach, each control can define it’s size in units, and will be placed on the next available free spot.
The Algorithm
- The FlowLayout needs a UnitSizeRequested in pixels, we need to calculate how many items can fit vertically and horizontally based on a size of a unit. Afterwards we need to find the actual UnitWidth and UnitHeight so that the whole area of the control is used.
- There need to be two attached properties for the child elements, so each element can specify its HorizontalUnits and VerticalUnits.
- Finally we create an array as a representation of the FlowLayout so we can arrange the Controls on it.
The Code
using System; using Xamarin.Forms; namespace Dashboard { public class FlowLayout : AbsoluteLayout { #region attached properties: HorizontalUnitsProperty public static readonly BindableProperty HorizontalUnitsProperty = BindableProperty.CreateAttached("HorizontalUnits", typeof(int), typeof(FlowLayout), 1); public static int GetHorizontalUnits(BindableObject view) { return (int)view.GetValue(HorizontalUnitsProperty); } public static void SetHorizontalUnits(BindableObject view, int value) { view.SetValue(HorizontalUnitsProperty, value); } #endregion #region attached properties: VerticalUnitsProperty public static readonly BindableProperty VerticalUnitsProperty = BindableProperty.CreateAttached("VerticalUnits", typeof(int), typeof(FlowLayout), 1); public static int GetVerticalUnits(BindableObject view) { return (int)view.GetValue(VerticalUnitsProperty); } public static void SetVerticalUnits(BindableObject view, int value) { view.SetValue(VerticalUnitsProperty, value); } #endregion #region properties public double UnitSizeRequested { get; set; } = 100; public double UnitWidth { get; private set; } public double UnitHeight { get; private set; } #endregion protected override void OnSizeAllocated(double width, double height) { // when the control allocates the size, we will arrange the children base.OnSizeAllocated(width, height); if (width>0 && height>0) { ArrangeChildren(); } } public void ArrangeChildren() { // do the calculation (step 1) int horizontalUnitCount = (int)(Width / UnitSizeRequested); int verticalUnitCount = (int)(Height / UnitSizeRequested); bool[,] dashArray = new bool[horizontalUnitCount, verticalUnitCount]; UnitWidth = UnitSizeRequested + (Width % UnitSizeRequested) / horizontalUnitCount; UnitHeight = UnitSizeRequested + (Height % UnitSizeRequested) / verticalUnitCount; foreach (var child in Children) { // for each child - find the var rect = FindFreeRectangle(dashArray, GetHorizontalUnits(child), GetVerticalUnits(child)); if (rect != null) { AbsoluteLayout.SetLayoutBounds(child, rect.Value); } else { // this control can not be placed ... so just skip it AbsoluteLayout.SetLayoutBounds(child, new Rectangle(0, 0, 0, 0)); } } } private Rectangle? FindFreeRectangle(bool[,] dashArray, int xCount, int yCount) { Rectangle res; for (int y = 0; y dashArray.GetLength(1) ) return false; for (int xx = x; xx < x + xCount; xx++) { for (int yy = y; yy < y + yCount; yy++) { if (dashArray[xx, yy] == true) return false; } } // now reserve fields for (var xx = x; xx < x + xCount; xx++) { for (var yy = y; yy < y + yCount; yy++) { dashArray[xx, yy] = true; } } return true; } } }
Caution: This code is not complete, perhaps you can not use it with the BindableLayout-Extension. Feel free to copy the code and change it as needed. Be careful in choosing the right UnitSizeRequested, a small unit will probably result in a rather bad performance.
Usage
<FlowLayout BackgroundColor="Black" > <Frame BorderColor="Yellow" BackgroundColor="Gray" CornerRadius="0" FlowLayout.HorizontalUnits="1" FlowLayout.VerticalUnits="1" /> <Frame BorderColor="Gray" BackgroundColor="HotPink" CornerRadius="0" FlowLayout.HorizontalUnits="1" FlowLayout.VerticalUnits="1" /> <Frame BorderColor="Blue" BackgroundColor="Honeydew" CornerRadius="0" FlowLayout.HorizontalUnits="1" FlowLayout.VerticalUnits="1" /> <Frame BorderColor="Red" BackgroundColor="Bisque" CornerRadius="0" FlowLayout.HorizontalUnits="4" FlowLayout.VerticalUnits="1" /> <Frame BorderColor="Green" BackgroundColor="CadetBlue" CornerRadius="0" FlowLayout.HorizontalUnits="3" FlowLayout.VerticalUnits="3" /> </FlowLayout>