Skip to content

Instantly share code, notes, and snippets.

@scolestock
Forked from NicoVermeir/AwesomeWrappanel
Last active August 29, 2015 14:20
Show Gist options
  • Save scolestock/88fa8290c965476d802f to your computer and use it in GitHub Desktop.
Save scolestock/88fa8290c965476d802f to your computer and use it in GitHub Desktop.
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using Xamarin.Forms;
namespace WrapPanelDemo.Controls
{
public class AwesomeWrappanel : Layout<View>
{
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create<WrapPanel, double>(
w => w.Spacing,
6,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapPanel)bindable).OnSizeChanged());
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create<WrapPanel, DataTemplate>(
w => w.ItemTemplate,
null,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapPanel)bindable).OnSizeChanged());
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create<WrapPanel, IEnumerable>(
w => w.ItemsSource,
null,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (oldValue != null)
{
var coll = (INotifyCollectionChanged)oldValue;
// Unsubscribe from CollectionChanged on the old collection
coll.CollectionChanged -= ItemsSource_OnItemChanged;
}
if (newValue != null)
{
var coll = (INotifyCollectionChanged)newValue;
// Subscribe to CollectionChanged on the new collection
coll.CollectionChanged += ItemsSource_OnItemChanged;
// We might already have items in the collection prior to it being bound, so let's feed them into the control.
foreach (var item in newValue)
{
ItemsSource_OnItemChanged(bindable, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
}
});
public static readonly BindableProperty OrientationProperty =
BindableProperty.Create<WrapPanel, StackOrientation>(
w => w.Orientation,
StackOrientation.Vertical,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapPanel)bindable).OnSizeChanged());
public AwesomeWrappanel()
{
// Make sure this instance of this class hears about changes to the bindable items source.
_collectionChanged += this.OnCollectionChanged;
}
private static event EventHandler<NotifyCollectionChangedEventArgs> _collectionChanged = delegate { };
public int MaxChildrenDisplayed { get; set; }
/// <summary>
/// Gets or sets the orientation (Horizontal or Vertical)
/// </summary>
public StackOrientation Orientation
{
get
{
return (StackOrientation)GetValue(OrientationProperty);
}
set
{
this.SetValue(OrientationProperty, value);
}
}
/// <summary>
/// Gets or sets the spacing added between elements (both directions)
/// </summary>
public double Spacing
{
get
{
return (double)GetValue(SpacingProperty);
}
set
{
this.SetValue(SpacingProperty, value);
}
}
/// <summary>
/// Gets or sets the item template to use
/// </summary>
public DataTemplate ItemTemplate
{
get
{
return (DataTemplate)GetValue(ItemTemplateProperty);
}
set
{
this.SetValue(ItemTemplateProperty, value);
}
}
/// <summary>
/// Gets or sets itemsSource to use
/// </summary>
public IEnumerable ItemsSource
{
get
{
return (IEnumerable)GetValue(ItemsSourceProperty);
}
set
{
this.SetValue(ItemsSourceProperty, value);
}
}
/// <summary>
/// This method is called during the measure pass of a layout cycle to get the desired size of an element.
/// </summary>
/// <param name="widthConstraint">The available width for the element to use.</param>
/// <param name="heightConstraint">The available height for the element to use.</param>
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
if (this.WidthRequest > 0)
{
widthConstraint = Math.Min(widthConstraint, this.WidthRequest);
}
if (this.HeightRequest > 0)
{
heightConstraint = Math.Min(heightConstraint, this.HeightRequest);
}
double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
double internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint);
return this.Orientation == StackOrientation.Vertical
? this.DoVerticalMeasure(internalWidth, internalHeight)
: this.DoHorizontalMeasure(internalWidth, internalHeight);
}
/// <summary>
/// Positions and sizes the children of a Layout.
/// </summary>
/// <param name="x">A value representing the x coordinate of the child region bounding box.</param>
/// <param name="y">A value representing the y coordinate of the child region bounding box.</param>
/// <param name="width">A value representing the width of the child region bounding box.</param>
/// <param name="height">A value representing the height of the child region bounding box.</param>
protected override void LayoutChildren(double x, double y, double width, double height)
{
if (this.Orientation == StackOrientation.Vertical)
{
double colWidth = 0;
double positionY = y, positionX = x;
foreach (var child in Children.Where(c => c.IsVisible))
{
var request = child.GetSizeRequest(width, height);
double childWidth = request.Request.Width;
double childHeight = request.Request.Height;
colWidth = Math.Max(colWidth, childWidth);
if (positionY + childHeight > height)
{
positionY = y;
positionX += colWidth + this.Spacing;
colWidth = 0;
}
var region = new Rectangle(positionX, positionY, childWidth, childHeight);
WrapPanel.LayoutChildIntoBoundingRegion(child, region);
positionY += region.Height + this.Spacing;
}
}
else
{
double rowHeight = 0;
double positionY = y, positionX = x;
foreach (var child in Children.Where(c => c.IsVisible))
{
var request = child.GetSizeRequest(width, height);
double childWidth = request.Request.Width;
double childHeight = request.Request.Height;
rowHeight = Math.Max(rowHeight, childHeight);
if (positionX + childWidth > width)
{
positionX = x;
positionY += rowHeight + this.Spacing;
rowHeight = 0;
}
var region = new Rectangle(positionX, positionY, childWidth, childHeight);
WrapPanel.LayoutChildIntoBoundingRegion(child, region);
positionX += region.Width + this.Spacing;
}
}
}
private static void ItemsSource_OnItemChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// This will wind up delivering to a given WrapPanel instance OnCollectionChanged method.
_collectionChanged(sender, e);
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (sender != this)
{
// This is an event for a collection associated with a different instance of the wrap panel.
return;
}
if (args.Action == NotifyCollectionChangedAction.Reset)
{
this.Children.Clear();
}
else
{
foreach (object item in args.NewItems)
{
var child = this.ItemTemplate.CreateContent() as View;
if (child == null)
{
return;
}
child.BindingContext = item;
Children.Add(child);
}
}
}
/// <summary>
/// This is called when the spacing or orientation properties are changed - it forces
/// the control to go back through a layout pass.
/// </summary>
private void OnSizeChanged()
{
this.ForceLayout();
}
/// <summary>
/// Does the vertical measure.
/// </summary>
/// <returns>The vertical measure.</returns>
/// <param name="widthConstraint">Width constraint.</param>
/// <param name="heightConstraint">Height constraint.</param>
private SizeRequest DoVerticalMeasure(double widthConstraint, double heightConstraint)
{
int columnCount = 1;
double width = 0;
double height = 0;
double minWidth = 0;
double minHeight = 0;
double heightUsed = 0;
foreach (var item in this.Children)
{
var size = item.GetSizeRequest(widthConstraint, heightConstraint);
width = Math.Max(width, size.Request.Width);
var newHeight = height + size.Request.Height + this.Spacing;
if (newHeight > heightConstraint)
{
columnCount++;
heightUsed = Math.Max(height, heightUsed);
height = size.Request.Height;
}
else
{
height = newHeight;
}
minHeight = Math.Max(minHeight, size.Minimum.Height);
minWidth = Math.Max(minWidth, size.Minimum.Width);
}
if (columnCount > 1)
{
height = Math.Max(height, heightUsed);
width *= columnCount; // take max width
}
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
}
/// <summary>
/// Does the horizontal measure.
/// </summary>
/// <returns>The horizontal measure.</returns>
/// <param name="widthConstraint">Width constraint.</param>
/// <param name="heightConstraint">Height constraint.</param>
private SizeRequest DoHorizontalMeasure(double widthConstraint, double heightConstraint)
{
int rowCount = 1;
double width = 0;
double height = 0;
double minWidth = 0;
double minHeight = 0;
double widthUsed = 0;
foreach (var item in this.Children)
{
var size = item.GetSizeRequest(widthConstraint, heightConstraint);
height = Math.Max(height, size.Request.Height);
var newWidth = width + size.Request.Width + this.Spacing;
if (newWidth > widthConstraint)
{
rowCount++;
widthUsed = Math.Max(width, widthUsed);
width = size.Request.Width;
}
else
{
width = newWidth;
}
minHeight = Math.Max(minHeight, size.Minimum.Height);
minWidth = Math.Max(minWidth, size.Minimum.Width);
}
if (rowCount > 1)
{
width = Math.Max(width, widthUsed);
height = ((height + this.Spacing) * rowCount) - this.Spacing; // via MitchMilam
}
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment