Last active
May 19, 2017 22:37
-
-
Save tonholis/a7b26d82a0dafc7e5853d745ff688ea9 to your computer and use it in GitHub Desktop.
My Xamarin.Forms Bindable WrapPanel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Collections.Specialized; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using Xamarin.Forms; | |
namespace TudousMobile.Controls | |
{ | |
public class WrapPanel : Layout<View> | |
{ | |
/// <summary> | |
/// Backing Storage for the Orientation property | |
/// </summary> | |
public static readonly BindableProperty OrientationProperty = BindableProperty.Create(nameof(Orientation), | |
typeof(StackOrientation), | |
typeof(WrapPanel), | |
StackOrientation.Vertical, | |
BindingMode.OneWay, | |
null, | |
propertyChanged: (bindable, value, newValue) => ((WrapPanel)bindable).OnSizeChanged() ); | |
/// <summary> | |
/// Orientation (Horizontal or Vertical) | |
/// </summary> | |
public StackOrientation Orientation | |
{ | |
get { return (StackOrientation)GetValue(OrientationProperty); } | |
set { SetValue(OrientationProperty, value); } | |
} | |
/// <summary> | |
/// Backing Storage for the Spacing property | |
/// </summary> | |
public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), | |
typeof(double), | |
typeof(WrapPanel), | |
6.0, | |
BindingMode.OneWay, | |
null, | |
propertyChanged: (bindable, value, newValue) => ((WrapPanel)bindable).OnSizeChanged()); | |
/// <summary> | |
/// Spacing added between elements (both directions) | |
/// </summary> | |
/// <value>The spacing.</value> | |
public double Spacing | |
{ | |
get { return (double)GetValue(SpacingProperty); } | |
set { SetValue(SpacingProperty, value); } | |
} | |
/// <summary> | |
/// Backing Storage for the Spacing property | |
/// </summary> | |
public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), | |
typeof(DataTemplate), | |
typeof(WrapPanel), | |
null, | |
BindingMode.OneWay, | |
null, | |
propertyChanged: (bindable, value, newValue) => ((WrapPanel)bindable).OnSizeChanged()); | |
/// <summary> | |
/// Spacing added between elements (both directions) | |
/// </summary> | |
/// <value>The spacing.</value> | |
public DataTemplate ItemTemplate | |
{ | |
get { return (DataTemplate)GetValue(ItemTemplateProperty); } | |
set { SetValue(ItemTemplateProperty, value); } | |
} | |
/// <summary> | |
/// Backing Storage for the Spacing property | |
/// </summary> | |
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(ItemsSource), | |
typeof(IEnumerable), | |
typeof(WrapPanel), | |
null, | |
BindingMode.OneWay, | |
null, | |
propertyChanged: ItemsSource_OnPropertyChanged); | |
/// <summary> | |
/// Spacing added between elements (both directions) | |
/// </summary> | |
/// <value>The spacing.</value> | |
public IEnumerable ItemsSource | |
{ | |
get { return (IEnumerable)GetValue(ItemsSourceProperty); } | |
set { SetValue(ItemsSourceProperty, value); } | |
} | |
private static void ItemsSource_OnPropertyChanged(BindableObject bindable, object oldValue, object newValue) | |
{ | |
IEnumerable newValueAsEnumerable; | |
try | |
{ | |
newValueAsEnumerable = newValue as IEnumerable; | |
} | |
catch (Exception e) | |
{ | |
throw e; | |
} | |
var control = (WrapPanel)bindable; | |
var oldObservableCollection = oldValue as INotifyCollectionChanged; | |
if (oldObservableCollection != null) | |
{ | |
oldObservableCollection.CollectionChanged -= control.ItemsSource_Changed; | |
} | |
var newObservableCollection = newValue as INotifyCollectionChanged; | |
if (newObservableCollection != null) | |
{ | |
newObservableCollection.CollectionChanged += control.ItemsSource_Changed; | |
} | |
control.Children.Clear(); | |
if (newValueAsEnumerable != null) | |
{ | |
foreach (var item in newValueAsEnumerable) | |
{ | |
var view = control.CreateChildViewFor(item); | |
control.Children.Add(view); | |
} | |
} | |
} | |
private void ItemsSource_Changed(object sender, NotifyCollectionChangedEventArgs e) | |
{ | |
if (e.Action == NotifyCollectionChangedAction.Add) | |
{ | |
if (e.NewItems != null) | |
{ | |
for (var i = 0; i < e.NewItems.Count; ++i) | |
{ | |
var item = e.NewItems[i]; | |
var view = CreateChildViewFor(item); | |
this.Children.Insert(i + e.NewStartingIndex, view); | |
} | |
} | |
} | |
else if (e.Action == NotifyCollectionChangedAction.Replace) | |
{ | |
this.Children.RemoveAt(e.OldStartingIndex); | |
var item = e.NewItems[e.NewStartingIndex]; | |
var view = CreateChildViewFor(item); | |
Children.Insert(e.NewStartingIndex, view); | |
} | |
else if (e.Action == NotifyCollectionChangedAction.Remove) | |
{ | |
if (e.OldItems != null) | |
{ | |
Children.RemoveAt(e.OldStartingIndex); | |
} | |
} | |
else if (e.Action == NotifyCollectionChangedAction.Reset) | |
{ | |
this.Children.Clear(); | |
} | |
} | |
private View CreateChildViewFor(object item) | |
{ | |
DataTemplateSelector templateSelector = null; | |
if (ItemTemplate is DataTemplateSelector) | |
templateSelector = (DataTemplateSelector)ItemTemplate; | |
object child; | |
if (templateSelector != null) | |
{ | |
var template = templateSelector.SelectTemplate(item, null); | |
child = template.CreateContent(); | |
} | |
else | |
{ | |
child = ((DataTemplate)item).CreateContent(); | |
} | |
View view; | |
var vc = child as ViewCell; | |
if (vc != null) | |
{ | |
view = vc.View; | |
} | |
else | |
{ | |
view = (View)child; | |
} | |
var bindableView = (BindableObject)view; | |
if (bindableView != null) | |
bindableView.BindingContext = item; | |
return view; | |
} | |
/// <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() | |
{ | |
ForceLayout(); | |
} | |
/// <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 OnMeasure(double widthConstraint, double heightConstraint) | |
{ | |
if (WidthRequest > 0) | |
widthConstraint = Math.Min(widthConstraint, WidthRequest); | |
if (HeightRequest > 0) | |
heightConstraint = Math.Min(heightConstraint, HeightRequest); | |
var internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint); | |
var internalHeight = double.IsPositiveInfinity(heightConstraint) ? double.PositiveInfinity : Math.Max(0, heightConstraint); | |
return Orientation == StackOrientation.Vertical | |
? DoVerticalMeasure(internalWidth, internalHeight) | |
: DoHorizontalMeasure(internalWidth, internalHeight); | |
} | |
/// <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 Children) | |
{ | |
var size = item.Measure(widthConstraint, heightConstraint); | |
width = Math.Max(width, size.Request.Width); | |
var newHeight = height + size.Request.Height + 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 Children) | |
{ | |
var size = item.Measure(widthConstraint, heightConstraint); | |
height = Math.Max(height, size.Request.Height); | |
var newWidth = width + size.Request.Width + 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 + Spacing) * rowCount - Spacing; // via MitchMilam | |
} | |
return new SizeRequest(new Size(width, height), new Size(minWidth, minHeight)); | |
} | |
/// <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 (Orientation == StackOrientation.Vertical) | |
{ | |
double colWidth = 0; | |
double yPos = y, xPos = x; | |
foreach (var child in Children.Where(c => c.IsVisible)) | |
{ | |
var request = child.Measure(width, height); | |
var childWidth = request.Request.Width; | |
var childHeight = request.Request.Height; | |
colWidth = Math.Max(colWidth, childWidth); | |
if (yPos + childHeight > height) | |
{ | |
yPos = y; | |
xPos += colWidth + Spacing; | |
colWidth = 0; | |
} | |
var region = new Rectangle(xPos, yPos, childWidth, childHeight); | |
LayoutChildIntoBoundingRegion(child, region); | |
yPos += region.Height + Spacing; | |
} | |
} | |
else | |
{ | |
double rowHeight = 0; | |
double yPos = y, xPos = x; | |
foreach (var child in Children.Where(c => c.IsVisible)) | |
{ | |
var request = child.Measure(width, height); | |
var childWidth = request.Request.Width; | |
var childHeight = request.Request.Height; | |
rowHeight = Math.Max(rowHeight, childHeight); | |
if (xPos + childWidth > width) | |
{ | |
xPos = x; | |
yPos += rowHeight + Spacing; | |
rowHeight = 0; | |
} | |
var region = new Rectangle(xPos, yPos, childWidth, childHeight); | |
LayoutChildIntoBoundingRegion(child, region); | |
xPos += region.Width + Spacing; | |
} | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment