Xamarin.Forms
is a very young layout system, meaning that the number of layouts is quite limited. There are times when we will need to implement our own custom layouts to give us control over exactly where and how our views and controls appear on screen. The requirement will come from situations where you need to improve performance on screens that display a lot of views and controls, and sometimes the standard layouts are not good enough. We want to implement our custom layouts to carry out the absolute minimum amount of work required to produce the required layout.
Let's start by adding a new folder called Controls
in the FireStorable
project. Add a new file called CarouselLayout.cs
and implement the first part as follows:
public class CarouselLayout : Layout<View> { #region Private Properties private IDisposable dataChangesSubscription; public double LayoutWidth; #endregion }
All layouts must inherit the Layout
framework. Xamarin.Forms.Layout<T>
provides a publicly exposed IList<T>
Children that end users can access. We want all children of this collection to be of type View
.
We have two private
properties, one for the layout width and an IDisposable
for handling data change subscriptions.
Let's add in some more properties:
#region Public Properties public Object this[int index] { get { return index < ItemsSource.Count() ? ItemsSource.ToList()[index] : null; } } public DataTemplate ItemTemplate { get; set; } public IEnumerable<Object> ItemsSource { get; set; } #endregion
We have an indexing reference that will return an array element from the
ItemsSource IEnumerable
, and the ItemTemplate
property, which is used to render a view layout for every child in ItemsSource
. We have to use the Linq
function ToList
to allow us to access an IEnumerable
via an index value.
Now we are going to add some overrides to the Layout
framework. Every custom layout must override the LayoutChildren
method. This is responsible for positioning children on screen:
protected override void LayoutChildren(double x, double y, double width, double height) { var layout = ComputeLayout(width, height); var i = 0; foreach (var region in layout) { var child = Children[i]; i++; LayoutChildIntoBoundingRegion(child, region); } }
The preceding function will call another method, ComputeLayout
, which will return an IEnumerable
of Rectangles (also known as regions). We then iterate through the IEnumerable
and call LayoutChildIntoBoundingRegion
for each region. This method will handle positioning the element relative to the bounding region.
Our layout must also implement the OnMeasure
function. This is required to make sure the new layout is sized correctly when placed inside other layouts. During layout cycles, this method may be called many times depending on the layout above it and how many layout exceptions are required to resolve the current layout hierarchy. Add the following below the LayoutChildren
function:
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { List<Row> layout = ComputeNiaveLayout(widthConstraint, heightConstraint); var last = layout[layout.Count - 1]; var width = (last.Count > 0) ? last[0].X + last.Width : 0; var height = (last.Count > 0) ? last[0].Y + last.Height : 0; return new SizeRequest(new Size(width, height)); }
The ComputeNiaveLayout
will return a list of rows. We then retrieve the last row from this list and use this for the max x-value and max y-value to determine the total width and height by calculating the difference between the first and last element on both the x-axis and y-axis. Finally, we return a new SizeRequest
object with the calculated width and height, which will be used to resize the layout.
Let's add the missing functions ComputeNiaveLayout
and ComputeLayout
as follows:
public IEnumerable<Rectangle> ComputeLayout(double widthConstraint, double heightConstraint) { List<Row> layout = ComputeNiaveLayout(widthConstraint, heightConstraint); return layout.SelectMany(s => s); }
This function is used simply to perform the SelectMany
query. The ComputeNiaveLayout
layout is where all the work is done. This will iterate through all children; it will create one row, and one rectangle inside this row that will size to the height of the layout and the width will equal the total of all children widths. All children will be positioned horizontally next to one another to the right of the screen, as shown in the following screenshot:
But only one child will be visible on screen at any one time as each child is sized to the full height and width of the layout:
private List<Row> ComputeNiaveLayout(double widthConstraint, double heightConstraint) { var result = new List<Row>(); var row = new Row(); result.Add(row); var spacing = 20; double y = 0; foreach (var child in Children) { var request = child.Measure(double.PositiveInfinity, double.PositiveInfinity); if (row.Count == 0) { row.Add(new Rectangle(0, y, LayoutWidth, Height)); row.Height = request.Request.Height; continue; } var last = row[row.Count - 1]; var x = last.Right + spacing; var childWidth = LayoutWidth; var childHeight = request.Request.Height; row.Add(new Rectangle(x, y, childWidth, Height)); row.Width = x + childWidth; row.Height = Math.Max(row.Height, Height); } return result; }
Hold on! What if I have a lot of children? This means that they will be stacked horizontally past the width of the screen. What do we do now?
The idea of a carousel view is to only show one view at a time, when the user swipes left and right; the view on the left/right side of the current view will come onto screen while the current view will move out of view, as shown in the following screenshot:
Even though we have a custom layout that presents children horizontally, how are we going to handle the swipe events and scroll control?
We will achieve scroll control via a ScrollView
and create a custom renderer for handling swipe events.
3.22.51.241