In Avalonia or Xaml in general, how would I implement the ability to use percentage based width or heights in controls - avaloniaui

I find the grid control to be very messy, counter-intuitive, verboose, and breaking the idea of xml that position in the document is important to layout. I spent a lot of time programming in the Adobe Flex framework and found I was incredibly fast at UI development with that ability, and the UI is way easier to parse later on as well to update and maintain. With that in mind how do we bring the ability to make controls like stackpanel, and button that can tolerate percentage widths and heights?

Documenting this here so it might help someone. I came from Adobe Flex, and using percentage based widths and heights is a breeze and I find the grid control to be messy and ruins half of the point of using XML to define a UI by breaking the layout order and style and adds a lot of code for little value. Here is an example:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:s="clr-namespace:Sandbox.Spark"
x:Class="Sandbox.MainWindow" Padding="5">
<s:VGroup>
<Border Background="LightBlue" CornerRadius="5" PercentHeight="30" PercentWidth="50">
<Button Content="Test" HorizontalAlignment="Center"/>
</Border>
<Border Background="Green" CornerRadius="5" Height="200" PercentWidth="75" Padding="5">
<s:VGroup>
<Button Content="Test5" PercentWidth="50"/>
<Button Content="Test8"/>
</s:VGroup>
</Border>
<Border Background="LightGray" CornerRadius="5" PercentHeight="100" PercentWidth="100">
<s:HGroup>
<Button Content="Test2"/>
<Button Content="Test3"/>
</s:HGroup>
</Border>
</s:VGroup>
</Window>
I Created the classes Group, VGroup, and HGroup, which are similar to StackPanel's but better suited to dealing with percentage based layout. Here they are:
/// <summary>
/// A Panel control similar to StackPanel but with greater support for PercentWidth and PercentHeight
/// </summary>
public class Group : Panel
{
public static readonly StyledProperty<Orientation> OrientationProperty = AvaloniaProperty.Register<Group, Orientation>(
"Orientation", Orientation.Vertical);
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
public static readonly StyledProperty<double> GapProperty = AvaloniaProperty.Register<Group, double>(
"Gap", 10);
public double Gap
{
get => GetValue(GapProperty);
set => SetValue(GapProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
return GroupUtils.Measure(availableSize, Children, Orientation, Gap);
}
protected override Size ArrangeOverride(Size finalSize)
{
return GroupUtils.ArrangeGroup(finalSize, Children, Orientation, Gap);
}
}
public class VGroup : Panel
{
public static readonly StyledProperty<double> GapProperty = AvaloniaProperty.Register<Group, double>(
"Gap", 10);
public double Gap
{
get => GetValue(GapProperty);
set => SetValue(GapProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
return GroupUtils.Measure(availableSize, Children, Orientation.Vertical, Gap);
}
protected override Size ArrangeOverride(Size finalSize)
{
return GroupUtils.ArrangeGroup(finalSize, Children, Orientation.Vertical, Gap);
}
}
public class HGroup : Panel
{
public static readonly StyledProperty<double> GapProperty = AvaloniaProperty.Register<Group, double>(
"Gap", 10);
public double Gap
{
get => GetValue(GapProperty);
set => SetValue(GapProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
return GroupUtils.Measure(availableSize, Children, Orientation.Horizontal, Gap);
}
protected override Size ArrangeOverride(Size finalSize)
{
return GroupUtils.ArrangeGroup(finalSize, Children, Orientation.Horizontal, Gap);
}
}
public static class GroupUtils
{
public static Size Measure(Size availableSize, Controls children, Orientation orientation, double gap)
{
Size layoutSlotSize = availableSize;
Size desiredSize = new Size();
bool hasVisibleChild = false;
//In order to handle percentwidth and percentheight scenario's we first have to measure all the children to determine their constrained measurement
//then depending on the orientation we factor in the left over space available and split that up via the percentages and orientation
//we use the measure with the true override to force the child to take our supplied size instead of it's default constrained size
var percentHeightChildrenMap = new Dictionary<Layoutable, double>();
var percentWidthChildrenMap = new Dictionary<Layoutable, double>();
//loop through all children and determine constrained size and check if percent height is set
for (int i = 0, count = children.Count; i < count; ++i)
{
// Get next child.
var child = children[i];
if (child == null) { continue; }
bool isVisible = child.IsVisible;
if (isVisible && !hasVisibleChild)
{
hasVisibleChild = true;
}
if (!double.IsNaN(child.PercentHeight))
{
percentHeightChildrenMap[child] = child.PercentHeight;
}
if (!double.IsNaN(child.PercentWidth))
{
percentWidthChildrenMap[child] = child.PercentWidth;
}
// Measure the child.
child.Measure(layoutSlotSize);
var childDesiredSize = child.DesiredSize;
if (orientation == Orientation.Vertical)
{
//in vertical mode, our width is the max width of the children
desiredSize = desiredSize.WithWidth(Math.Max(desiredSize.Width, childDesiredSize.Width));
//our height is the combine height of the children
desiredSize = desiredSize.WithHeight(desiredSize.Height + (isVisible ? gap : 0) + childDesiredSize.Height);
}
else
{
//in horizontal mode, our height is the max height of the children
desiredSize = desiredSize.WithHeight(Math.Max(desiredSize.Height, childDesiredSize.Height));
//our height is the combine width of the children
desiredSize = desiredSize.WithWidth(desiredSize.Width + (isVisible ? gap : 0) + childDesiredSize.Width);
}
}
if (orientation == Orientation.Vertical)
{
//Handle percent width
foreach (var child in children)
{
if (!double.IsNaN(child.PercentWidth))
{
child.InvalidateMeasure();
child.Measure(child.DesiredSize.WithWidth(child.PercentWidth * 0.01 * availableSize.Width), true);
desiredSize = desiredSize.WithWidth(Math.Max(desiredSize.Width, child.DesiredSize.Width));
}
}
//if we have dont have a visible child then set to 0, otherwise remove the last added gap
desiredSize = desiredSize.WithHeight(desiredSize.Height - (hasVisibleChild ? gap : 0));
if (hasVisibleChild && percentHeightChildrenMap.Count > 0)
{
//for those with percent height set, combine the percent heights together and if above 100, find the scale factor
var totalPercentHeight = percentHeightChildrenMap.Sum(v => v.Value);
totalPercentHeight = totalPercentHeight <= 0 ? 1 : totalPercentHeight;
var scaleRatio = 1 / (totalPercentHeight / 100);
//the available size leftover after the non-percent height children is now used to calculate the percentheight children sizes
var availableHeight = availableSize.Height - desiredSize.Height;
Debug.WriteLine($"Remapping %Height Children, availableHeight: {availableHeight}, scaleRatio: {scaleRatio}" );
foreach (var child in percentHeightChildrenMap.Keys)
{
var originalHeight = child.DesiredSize.Height;
var percentHeight = percentHeightChildrenMap[child];
var heightIncrease = availableHeight * percentHeight * scaleRatio * 0.01;
var recalculatedHeight = child.DesiredSize.Height + heightIncrease;
child.InvalidateMeasure();
child.Measure(child.DesiredSize.WithHeight(recalculatedHeight), true);
desiredSize = desiredSize.WithHeight(desiredSize.Height + child.DesiredSize.Height - originalHeight);
Debug.WriteLine($"$Found Child Height %:{percentHeight}, Original Height: {originalHeight}, New: {recalculatedHeight}" );
}
}
}
else
{
//Handle percent height
foreach (var child in children)
{
if (!double.IsNaN(child.PercentHeight))
{
child.InvalidateMeasure();
child.Measure(child.DesiredSize.WithHeight(child.PercentHeight * 0.01 * availableSize.Height), true);
desiredSize = desiredSize.WithHeight(Math.Max(desiredSize.Height, child.DesiredSize.Height));
}
}
//if we have dont have a visible child then set to 0, otherwise remove the last added gap
desiredSize = desiredSize.WithWidth(desiredSize.Width - (hasVisibleChild ? gap : 0));
if (hasVisibleChild && percentWidthChildrenMap.Count > 0)
{
//for those with percent Width set, combine the percent Widths together and if above 100, find the scale factor
var totalPercentWidth = percentWidthChildrenMap.Sum(v => v.Value);
totalPercentWidth = totalPercentWidth <= 0 ? 1 : totalPercentWidth;
var scaleRatio = 1 / (totalPercentWidth / 100);
//the available size leftover after the non-percent height children is now used to calculate the percentheight children sizes
var availableWidth = availableSize.Width - desiredSize.Width;
Debug.WriteLine($"Remapping %Width Children, availableWidth: {availableWidth}, scaleRatio: {scaleRatio}" );
foreach (var child in percentWidthChildrenMap.Keys)
{
var originalWidth = child.DesiredSize.Width;
var percentWidth = percentWidthChildrenMap[child];
var widthIncrease = availableWidth * percentWidth * scaleRatio * 0.01;
var recalculatedWidth = child.DesiredSize.Width + widthIncrease;
child.InvalidateMeasure();
child.Measure(child.DesiredSize.WithWidth(recalculatedWidth), true);
desiredSize = desiredSize.WithWidth(desiredSize.Width + child.DesiredSize.Width - originalWidth);
Debug.WriteLine($"$Found Child Width %:{percentWidth}, Original Width: {originalWidth}, New: {recalculatedWidth}" );
}
}
}
return desiredSize;
}
public static Size ArrangeGroup(Size finalSize, Controls children, Orientation orientation, double gap)
{
bool fHorizontal = (orientation == Orientation.Horizontal);
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
var spacing = gap;
//
// Arrange and Position Children.
//
for (int i = 0, count = children.Count; i < count; ++i)
{
var child = children[i];
if (child == null || !child.IsVisible)
{
continue;
}
if (fHorizontal)
{
rcChild = rcChild.WithX(rcChild.X + previousChildSize);
previousChildSize = child.DesiredSize.Width;
rcChild = rcChild.WithWidth(previousChildSize);
rcChild = rcChild.WithHeight(child.DesiredSize.Height);
previousChildSize += spacing;
}
else
{
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize);
rcChild = rcChild.WithWidth(child.DesiredSize.Width);
previousChildSize += spacing;
}
child.Arrange(rcChild);
}
return finalSize;
}
}
Finally I had to make a change in the avalonia source class Layoutable
adding
public static readonly StyledProperty<double> PercentWidthProperty = AvaloniaProperty.Register<Layoutable, double>(
"PercentWidth", Double.NaN);
public static readonly StyledProperty<double> PercentHeightProperty = AvaloniaProperty.Register<Layoutable, double>(
"PercentHeight", Double.NaN);
public double PercentHeight
{
get => GetValue(PercentHeightProperty);
set => SetValue(PercentHeightProperty, value);
}
public double PercentWidth
{
get => GetValue(PercentWidthProperty);
set => SetValue(PercentWidthProperty, value);
}
Registering the properties in the constructor for layoutable such as
static Layoutable()
{
AffectsMeasure<Layoutable>(
WidthProperty,
HeightProperty,
MinWidthProperty,
MaxWidthProperty,
MinHeightProperty,
MaxHeightProperty,
MarginProperty,
**PercentHeightProperty,
PercentWidthProperty,**
HorizontalAlignmentProperty,
VerticalAlignmentProperty);
}
and modifying the measure method to accept a boolean 2nd parameter that tells the measure to use all available space and then uses the percentage calculation:
public void Measure(Size availableSize, bool useAvailable = false)
{
if (double.IsNaN(availableSize.Width) || double.IsNaN(availableSize.Height))
{
throw new InvalidOperationException("Cannot call Measure using a size with NaN values.");
}
if (!IsMeasureValid || _previousMeasure != availableSize)
{
var previousDesiredSize = DesiredSize;
var desiredSize = default(Size);
IsMeasureValid = true;
try
{
_measuring = true;
desiredSize = MeasureCore(availableSize);
//used in percentwidth height layout system
if (useAvailable == true)
{
desiredSize = desiredSize.WithHeight(Math.Max(availableSize.Height, desiredSize.Height))
.WithWidth(Math.Max(availableSize.Width, desiredSize.Width));
}
}
finally
{
_measuring = false;
}
if (IsInvalidSize(desiredSize))
{
throw new InvalidOperationException("Invalid size returned for Measure.");
}
DesiredSize = desiredSize;
_previousMeasure = availableSize;
Logger.TryGet(LogEventLevel.Verbose, LogArea.Layout)?.Log(this, "Measure requested {DesiredSize}", DesiredSize);
if (DesiredSize != previousDesiredSize)
{
this.GetVisualParent<Layoutable>()?.ChildDesiredSizeChanged(this);
}
}
}

I'd suggest reading the documentation when picking up a new UI tech. The worst thing you can do is try to bend a new technology to the way another unrelated technology works.
Particularly when what you need already exists.
50% / 50% columns ...
<Grid ColumnDefinitions="1*, 1*">
<Border Grid.Column="0" Background="Red" />
<Border Grid.Column="1" Background="Blue" />
</Grid>
25% / 75%
<Grid ColumnDefinitions="1*, 3*">
<Border Grid.Column="0" Background="Red" />
<Border Grid.Column="1" Background="Blue" />
</Grid>
You typically don't set heights and widths on controls. You define the space they have on the UI and allow them to adapt. Some controls might have a default height in a style that's applied globally.
Try to think in XAML terms when using XAML and Adobe terms when using Adobe. Mixing the two will self-inflict a lot of pain.
I'd advise anyone else finding this question to not use this percentage approach.

Related

How to show a badges count of ToolBarItem Icon in Xamarin Forms

It is not about how to show notification badges nor it's about to show toolbar item icon. It is clear question that how to show a badges count on a toolbar item icon. ?
I am sharing code to create ToolbarItem with icon in XF content page:
In cs File:
ToolbarItem cartItem = new ToolbarItem();
scanItem.Text = "My Cart";
scanItem.Order = ToolbarItemOrder.Primary;
scanItem.Icon = "carticon.png";
ToolbarItems.Add(cartItem );
In Xaml File:
<ContentPage.ToolbarItems>
<ToolbarItem Text="Cart" Priority="0" x:Name="menu1">
</ToolbarItem>
</ContentPage.ToolbarItems>
Now I want to Place a badge count on the above added tool bar item icon. How it can be achieved ?
Placing badge icon's in the native toolbars is actually more effort than its worth. If I need a badge icon, I remove the navigation page.
NavigationPage.SetHasNavigationBar(myPageInstance, false);
Then I create my own toolbar from scratch. In this toolbar, I can overlay an image in there, you can also place a number in it as needed. For example.
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding IconCommand}" />
</Grid.GestureRecognizers>
<iconize:IconImage
Icon="fa-drawer"
IconColor="white"
IconSize="20" />
<Grid Margin="15,-15,0,0">
<iconize:IconImage Grid.Row="0"
HeightRequest="40"
WidthRequest="40"
Icon="fa-circle"
IconColor="red"
IsVisible="{Binding IsCircleVisible}"
IconSize="10" />
</Grid>
</Grid>
I use Iconize wtih FontAwesome for the icons
With the help of Xamarin Forum Discussion, I have achieved it. Read ad understand the complete discussion before implement it. Thank you "Slava Chernikoff", "Emanuele Sabetta", "Mirza Sikander", "Satish" to discuss and yours share code.
Setp 1: Create a Helper Class in PCL and install NGraphics package from nugget.
public class CartIconHelper
{
private static Graphic _svgGraphic = null;
public const string ResourcePath = "ToolBarAndroidBadge.Resources.cartIcon.svg";
private static PathOp[] RoundRect(NGraphics.Rect rect, double radius)
{
return new PathOp[]
{
new NGraphics.MoveTo(rect.X + radius, rect.Y),
new NGraphics.LineTo(rect.X + rect.Width - radius, rect.Y),
new NGraphics.ArcTo(new NGraphics.Size(radius, radius), true, false, new NGraphics.Point(rect.X + rect.Width, rect.Y + radius)),
new NGraphics.LineTo(rect.X + rect.Width, rect.Y + rect.Height - radius),
new NGraphics.ArcTo(new NGraphics.Size(radius, radius), true, false, new NGraphics.Point(rect.X + rect.Width - radius, rect.Y + rect.Height)),
new NGraphics.LineTo(rect.X + radius, rect.Y + rect.Height),
new NGraphics.ArcTo(new NGraphics.Size(radius, radius), true, false, new NGraphics.Point(rect.X, rect.Y + rect.Height - radius)),
new NGraphics.LineTo(rect.X, rect.Y + radius), new NGraphics.ArcTo(new NGraphics.Size(radius, radius), true, false, new NGraphics.Point(rect.X + radius, rect.Y)),
new NGraphics.ClosePath()
};
}
public static string DrawCartIcon(int count, string path, double iconSize = 30, double scale = 2, string fontName = "Arial", double fontSize = 12, double textSpacing = 4)
{
var service = DependencyService.Get<IService>();
var canvas = service.GetCanvas();
if (_svgGraphic == null) using (var stream = typeof(CartIconHelper).GetTypeInfo().Assembly.GetManifestResourceStream(path))
_svgGraphic = new SvgReader(new StreamReader(stream)).Graphic;
//st = ReadFully(stream);
var minSvgScale = Math.Min(canvas.Size.Width / _svgGraphic.Size.Width, canvas.Size.Height / _svgGraphic.Size.Height) / 1.15;
var w = _svgGraphic.Size.Width / minSvgScale;
var h = _svgGraphic.Size.Height / minSvgScale;
_svgGraphic.ViewBox = new NGraphics.Rect(0, -14, w, h);
_svgGraphic.Draw(canvas);
if (count > 0)
{
var text = count > 99 ? "99+" : count.ToString();
var font = new NGraphics.Font(fontName, fontSize);
var textSize = canvas.MeasureText(text, font);
var textRect = new NGraphics.Rect(canvas.Size.Width - textSize.Width - textSpacing, textSpacing, textSize.Width, textSize.Height);
if (count < 10)
{
var side = Math.Max(textSize.Width, textSize.Height);
var elipseRect = new NGraphics.Rect(canvas.Size.Width - side - 2 * textSpacing, 0, side + 2 * textSpacing, side + 2 * textSpacing);
canvas.FillEllipse(elipseRect, NGraphics.Colors.Red);
textRect -= new NGraphics.Point(side - textSize.Width, side - textSize.Height) / 2.0;
}
else
{
var elipseRect = new NGraphics.Rect(textRect.Left - textSpacing, textRect.Top - textSpacing, textRect.Width + 2 * textSpacing, textSize.Height + 2 * textSpacing);
canvas.FillPath(RoundRect(elipseRect, 6), NGraphics.Colors.Red);
}
var testReact1= new NGraphics.Rect(20,12,0,0);
// canvas.DrawText(text, textRect + new NGraphics.Point(0, textSize.Height), font, NGraphics.TextAlignment.Center, NGraphics.Colors.Black);
canvas.DrawText("5", testReact1, font, NGraphics.TextAlignment.Left, NGraphics.Colors.White);
}
service.SaveImage(canvas.GetImage());
string imagePath = service.GetImage();
return imagePath;
// return st;
}
}
Step 2: Create a interface to IService in PCL
public interface IService
{
IImageCanvas GetCanvas();
void SaveImage(NGraphics.IImage image);
string GetImage();
}
Step 3 : Implement this interface in your Android project
class CanvasServices:IService
{
private readonly AndroidPlatform _platform;
public CanvasServices()
{
_platform = new AndroidPlatform();
}
public void SaveImage(IImage image)
{
var dir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
var filePath = System.IO.Path.Combine(dir, "cart.png");
var stream = new FileStream(filePath, FileMode.Create);
image.SaveAsPng(stream);
//bitmap.Compress(image., 100, stream);
stream.Close();
}
public string GetImage()
{
var dir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);
var filePath = System.IO.Path.Combine(dir, "cart.png");
using (var streamReader = new StreamReader(filePath))
{
string content = streamReader.ReadToEnd();
System.Diagnostics.Debug.WriteLine(content);
}
return filePath;
}
public IImageCanvas GetCanvas()
{
NGraphics.Size size = new NGraphics.Size(30);
return _platform.CreateImageCanvas(size);
}
public NGraphics.AndroidPlatform GetPlatform()
{
return _platform;
}
}
Setp 4: Now, use CartIcon Helper in your PCL project to show badges in TabBarItem.
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
var imagePath = CartIconHelper.DrawCartIcon(2, "ToolBarAndroidBadge.Resources.cartIcon.svg");
string deviceSepecificFolderPath = Device.OnPlatform(null, imagePath, null);
object convertedObject = new FileImageSourceConverter().ConvertFromInvariantString(deviceSepecificFolderPath);
FileImageSource fileImageSource = (FileImageSource)convertedObject;
ToolbarItem cartItem = new ToolbarItem();
cartItem.Text = "My Cart";
cartItem.Order = ToolbarItemOrder.Primary;
cartItem.Icon = fileImageSource;
ToolbarItems.Add(cartItem);
}
}
For any one who wants to add badge on toolbar item using custom ui try,
Instead of using default toolbar item, you can hide the default navigation bar by NavigationPage.SetHasNavigationBar(this, false);
in the constructor.
Then prepare the custom navigation bar with toolbar item with badge as mentioned in above answers.
If you are using master detail page, hiding default navigation bar will hide hamburger icon, so need to slide from left to see sliding menu. Alternate method would be place a button with hamburger icon in custom navigation bar, on button click use messaging center to present the sliding menu.
Example: On page in which hamburger button is clicked
private void Button_Clicked(object sender, System.EventArgs e)
{
MessagingCenter.Send(this, "presnt");
}
On MasterDetail page
MessagingCenter.Subscribe<YourPage>(this, "presnt", (sender) =>
{
IsPresented = true;
});
Before making IsPresented=true, check for sliding menu is not all-ready presented.
Check https://github.com/LeslieCorrea/Xamarin-Forms-Shopping-Cart for badge on toolbar item.
Implement below code to draw a ground circle with text over toolbar icon
BarButtonItemExtensions.cs
using CoreAnimation;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using UIKit;
namespace TeamCollaXform.Views.Services
{
public static class BarButtonItemExtensions
{
enum AssociationPolicy
{
ASSIGN = 0,
RETAIN_NONATOMIC = 1,
COPY_NONATOMIC = 3,
RETAIN = 01401,
COPY = 01403,
}
static NSString BadgeKey = new NSString(#"BadgeKey");
[DllImport(Constants.ObjectiveCLibrary)]
static extern void objc_setAssociatedObject(IntPtr obj, IntPtr key, IntPtr value, AssociationPolicy policy);
[DllImport(Constants.ObjectiveCLibrary)]
static extern IntPtr objc_getAssociatedObject(IntPtr obj, IntPtr key);
static CAShapeLayer GetBadgeLayer(UIBarButtonItem barButtonItem)
{
var handle = objc_getAssociatedObject(barButtonItem.Handle, BadgeKey.Handle);
if (handle != IntPtr.Zero)
{
var value = ObjCRuntime.Runtime.GetNSObject(handle);
if (value != null)
return value as CAShapeLayer;
else
return null;
}
return null;
}
static void DrawRoundedRect(CAShapeLayer layer, CGRect rect, float radius, UIColor color, bool filled)
{
layer.FillColor = filled ? color.CGColor : UIColor.White.CGColor;
layer.StrokeColor = color.CGColor;
layer.Path = UIBezierPath.FromRoundedRect(rect, radius).CGPath;
}
public static void AddBadge(this UIBarButtonItem barButtonItem, string text, UIColor backgroundColor, UIColor textColor, bool filled = true, float fontSize = 11.0f)
{
if (string.IsNullOrEmpty(text))
{
return;
}
CGPoint offset = CGPoint.Empty;
if (backgroundColor == null)
backgroundColor = UIColor.Red;
var font = UIFont.SystemFontOfSize(fontSize);
if (UIDevice.CurrentDevice.CheckSystemVersion(9, 0))
{
font = UIFont.MonospacedDigitSystemFontOfSize(fontSize, UIFontWeight.Regular);
}
var view = barButtonItem.ValueForKey(new NSString(#"view")) as UIView;
var bLayer = GetBadgeLayer(barButtonItem);
bLayer?.RemoveFromSuperLayer();
var badgeSize = text.StringSize(font);
var height = badgeSize.Height;
var width = badgeSize.Width + 5; /* padding */
//make sure we have at least a circle
if (width < height)
{
width = height;
}
//x position is offset from right-hand side
var x = view.Frame.Width - width + offset.X;
var badgeFrame = new CGRect(new CGPoint(x: x - 4, y: offset.Y + 5), size: new CGSize(width: width, height: height));
bLayer = new CAShapeLayer();
DrawRoundedRect(bLayer, badgeFrame, 7.0f, backgroundColor, filled);
view.Layer.AddSublayer(bLayer);
// Initialiaze Badge's label
var label = new CATextLayer();
label.String = text;
label.TextAlignmentMode = CATextLayerAlignmentMode.Center;
label.SetFont(CGFont.CreateWithFontName(font.Name));
label.FontSize = font.PointSize;
label.Frame = badgeFrame;
label.ForegroundColor = filled ? textColor.CGColor : UIColor.White.CGColor;
label.BackgroundColor = UIColor.Clear.CGColor;
label.ContentsScale = UIScreen.MainScreen.Scale;
bLayer.AddSublayer(label);
// Save Badge as UIBarButtonItem property
objc_setAssociatedObject(barButtonItem.Handle, BadgeKey.Handle, bLayer.Handle, AssociationPolicy.RETAIN_NONATOMIC);
}
public static void UpdateBadge(this UIBarButtonItem barButtonItem, string text, UIColor backgroundColor, UIColor textColor)
{
var bLayer = GetBadgeLayer(barButtonItem);
if (string.IsNullOrEmpty(text) || text == "0")
{
bLayer?.RemoveFromSuperLayer();
objc_setAssociatedObject(barButtonItem.Handle, BadgeKey.Handle, new CAShapeLayer().Handle, AssociationPolicy.ASSIGN);
return;
}
var textLayer = bLayer?.Sublayers?.First(p => p is CATextLayer) as CATextLayer;
if (textLayer != null)
{
textLayer.String = text;
}
else
{
barButtonItem.AddBadge(text, backgroundColor, textColor);
}
}
}
}
ToolbarItemBadgeService.cs
using TeamCollaXform.Views.Services;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: Dependency(typeof(ToolbarItemBadgeService))]
namespace TeamCollaXform.Views.Services
{
/// <summary>
///
/// </summary>
public interface IToolbarItemBadgeService
{
void SetBadge(Page page, ToolbarItem item, string value, Color backgroundColor, Color textColor);
}
/// <summary>
///
/// </summary>
public class ToolbarItemBadgeService : IToolbarItemBadgeService
{
public void SetBadge(Page page, ToolbarItem item, string value, Color backgroundColor, Color textColor)
{
Device.BeginInvokeOnMainThread(() =>
{
var renderer = Platform.GetRenderer(page);
if (renderer == null)
{
renderer = Platform.CreateRenderer(page);
Platform.SetRenderer(page, renderer);
}
var vc = renderer.ViewController;
var rightButtomItems = vc?.ParentViewController?.NavigationItem?.RightBarButtonItems;
var idx = rightButtomItems.Length - page.ToolbarItems.IndexOf(item) - 1; //Revert
if (rightButtomItems != null && rightButtomItems.Length > idx)
{
var barItem = rightButtomItems[idx];
if (barItem != null)
{
barItem.UpdateBadge(value, backgroundColor.ToUIColor(), textColor.ToUIColor());
}
}
});
}
}
}
Usage
void OnAttachClicked(object sender, EventArgs e)
{
//var answer = await DisplayAlert("Question?", "Would you like to play a game", "Yes", "No");
//Debug.WriteLine("Answer: " + answer);
ToolbarItem cmdItem = sender as ToolbarItem;
DependencyService.Get<IToolbarItemBadgeService>().SetBadge(this, cmdItem, $"2", Color.DarkOrange, Color.White);
}
Links: 1) for instruction and 2) for sample code
https://www.xamboy.com/2018/03/08/adding-badge-to-toolbaritem-in-xamarin-forms/
https://github.com/CrossGeeks/ToolbarItemBadgeSample

Xamarin.Forms need WrapLayout

I'm struggling to figure out how to wrap my listview items horizontally and fill up available space within my listview.
So far, the items within my list view do not wrap.
XAML:
<Grid Grid.Row="4" Grid.RowSpacing="3" Grid.ColumnSpacing="3" BackgroundColor="Silver">
<ListView ItemsSource="{Binding EmployeeIds}" SelectedItem="{Binding SelectedEmployeeId}"
BackgroundColor="Black" >
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<customcontrols:WrapLayoutOld Orientation="Horizontal">
<Label Text="{Binding Value}" TextColor="Yellow" XAlign="Start" />
</customcontrols:WrapLayoutOld>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
WrapLayout:
using System;
using System.Linq;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Custom.Controls
{
/// <summary>
/// New WrapLayout
/// </summary>
/// <author>Jason Smith</author>
public class WrapLayout : Layout<View>
{
Dictionary<View, SizeRequest> layoutCache = new Dictionary<View, SizeRequest>();
/// <summary>
/// Backing Storage for the Spacing property
/// </summary>
public static readonly BindableProperty SpacingProperty =
BindableProperty.Create<WrapLayout, double>(w => w.Spacing, 5,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayout)bindable).layoutCache.Clear());
/// <summary>
/// Spacing added between elements (both directions)
/// </summary>
/// <value>The spacing.</value>
public double Spacing
{
get { return (double)GetValue(SpacingProperty); }
set { SetValue(SpacingProperty, value); }
}
public WrapLayout()
{
VerticalOptions = HorizontalOptions = LayoutOptions.FillAndExpand;
}
protected override void OnChildMeasureInvalidated()
{
base.OnChildMeasureInvalidated();
layoutCache.Clear();
}
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
double lastX;
double lastY;
var layout = NaiveLayout(widthConstraint, heightConstraint, out lastX, out lastY);
return new SizeRequest(new Size(lastX, lastY));
}
protected override void LayoutChildren(double x, double y, double width, double height)
{
double lastX, lastY;
var layout = NaiveLayout(width, height, out lastX, out lastY);
foreach (var t in layout)
{
var offset = (int)((width - t.Last().Item2.Right) / 2);
foreach (var dingus in t)
{
var location = new Rectangle(dingus.Item2.X + x + offset, dingus.Item2.Y + y, dingus.Item2.Width, dingus.Item2.Height);
LayoutChildIntoBoundingRegion(dingus.Item1, location);
}
}
}
private List<List<Tuple<View, Rectangle>>> NaiveLayout(double width, double height, out double lastX, out double lastY)
{
double startX = 0;
double startY = 0;
double right = width;
double nextY = 0;
lastX = 0;
lastY = 0;
var result = new List<List<Tuple<View, Rectangle>>>();
var currentList = new List<Tuple<View, Rectangle>>();
foreach (var child in Children)
{
SizeRequest sizeRequest;
if (!layoutCache.TryGetValue(child, out sizeRequest))
{
layoutCache[child] = sizeRequest = child.GetSizeRequest(double.PositiveInfinity, double.PositiveInfinity);
}
var paddedWidth = sizeRequest.Request.Width + Spacing;
var paddedHeight = sizeRequest.Request.Height + Spacing;
if (startX + paddedWidth > right)
{
startX = 0;
startY += nextY;
if (currentList.Count > 0)
{
result.Add(currentList);
currentList = new List<Tuple<View, Rectangle>>();
}
}
currentList.Add(new Tuple<View, Rectangle>(child, new Rectangle(startX, startY, sizeRequest.Request.Width, sizeRequest.Request.Height)));
lastX = Math.Max(lastX, startX + paddedWidth);
lastY = Math.Max(lastY, startY + paddedHeight);
nextY = Math.Max(nextY, paddedHeight);
startX += paddedWidth;
}
result.Add(currentList);
return result;
}
}
/// <summary>
/// Simple Layout panel which performs wrapping on the boundaries.
/// </summary>
public class WrapLayoutOld : Layout<View>
{
/// <summary>
/// Backing Storage for the Orientation property
/// </summary>
public static readonly BindableProperty OrientationProperty =
BindableProperty.Create<WrapLayoutOld, StackOrientation>(w => w.Orientation, StackOrientation.Vertical,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayoutOld)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<WrapLayoutOld, double>(w => w.Spacing, 6,
propertyChanged: (bindable, oldvalue, newvalue) => ((WrapLayoutOld)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>
/// 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();
}
//http://forums.xamarin.com/discussion/17961/stacklayout-with-horizontal-orientation-how-to-wrap-vertically#latest
// protected override void OnPropertyChanged
// (string propertyName = null)
// {
// base.OnPropertyChanged(propertyName);
// if ((propertyName == WrapLayout.OrientationProperty.PropertyName) ||
// (propertyName == WrapLayout.SpacingProperty.PropertyName)) {
// this.OnSizeChanged();
// }
// }
/// <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 (WidthRequest > 0)
widthConstraint = Math.Min(widthConstraint, WidthRequest);
if (HeightRequest > 0)
heightConstraint = Math.Min(heightConstraint, HeightRequest);
double internalWidth = double.IsPositiveInfinity(widthConstraint) ? double.PositiveInfinity : Math.Max(0, widthConstraint);
double 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.GetSizeRequest(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.GetSizeRequest(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.GetSizeRequest(width, height);
double childWidth = request.Request.Width;
double 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.GetSizeRequest(width, height);
double childWidth = request.Request.Width;
double 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;
}
}
}
}
}
FlexLayout has been introduced in Xamarin.Forms 3.0, so if you can upgrade you should definitely use it:
FlexLayout is similar to the Xamarin.Forms StackLayout in that it can
arrange its children horizontally and vertically in a stack. However,
the FlexLayout is also capable of wrapping its children if there are
too many to fit in a single row or column, and also has many options
for orientation, alignment, and adapting to various screen sizes.
Official docs.
I have this list of images and they wrap correctly using the same class you are using, it may be related to LayoutOptions:
WrapLayout container = new WrapLayout
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.StartAndExpand,
Orientation = StackOrientation.Horizontal,
Padding = 10,
Spacing = 10
};
Image thumbnail = new Image
{
HeightRequest = 80,
WidthRequest = 80,
HorizontalOptions = LayoutOptions.Start,
VerticalOptions = LayoutOptions.Start,
//BackgroundColor = Color.Black
Source = item.ImageText,
Aspect = Aspect.AspectFit
};

android expandableListView animate slide up?

I want to animate slide down and slide up on expandablelistview when I click the groupItem.Then I have finish the slide down.
public class ExpandAnimation extends Animation {
private static final String TAG = "ExpandAnimation";
private View mAnimatedView;
private LayoutParams mViewLayoutParams;
private int mMarginStart, mMarginEnd;
private boolean mIsVisibleAfter = false;
private boolean mWasEndedAlready = false;
/**
* Initialize the animation
* #param view The layout we want to animate
* #param duration The duration of the animation, in ms
*/
public ExpandAnimation(View view, int duration) {
setDuration(duration);
mAnimatedView = view;
mViewLayoutParams = (LayoutParams) view.getLayoutParams();
// if the bottom margin is 0,
// then after the animation will end it'll be negative, and invisible.
mIsVisibleAfter = (mViewLayoutParams.bottomMargin == 0);
mMarginStart = mViewLayoutParams.bottomMargin;
Log.i(TAG, "mMarginStart:>>>>>>>"+mMarginStart);
mMarginEnd = (mMarginStart == 0 ? (0- view.getHeight()) : 0);
Log.i(TAG, "mMarginEnd:>>>>>>>"+mMarginEnd);
view.setVisibility(View.VISIBLE);
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
super.applyTransformation(interpolatedTime, t);
Log.i(TAG, "applyTransformation-->"+interpolatedTime);
if (interpolatedTime < 1.0f) {
// Calculating the new bottom margin, and setting it
mViewLayoutParams.bottomMargin = mMarginStart
+ (int) ((mMarginEnd - mMarginStart) * interpolatedTime);
// Invalidating the layout, making us seeing the changes we made
mAnimatedView.requestLayout();
// Making sure we didn't run the ending before (it happens!)
} else if (!mWasEndedAlready) {
mViewLayoutParams.bottomMargin = mMarginEnd;
mAnimatedView.requestLayout();
if (mIsVisibleAfter) {
mAnimatedView.setVisibility(View.GONE);
}
mWasEndedAlready = true;
}
}
}
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
Log.i(TAG, "getChildView");
#SuppressWarnings("unchecked")
String text = ((Map<String, String>) getChild(groupPosition,
childPosition)).get("child");
if (convertView == null) {
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = layoutInflater.inflate(R.layout.child, null);
}
View toolbar = convertView.findViewById(R.id.toolbar);
setAnimationView(toolbar);
((LinearLayout.LayoutParams) toolbar.getLayoutParams()).bottomMargin = -75;
toolbar.setVisibility(View.GONE);
ExpandAnimation expandAni = new ExpandAnimation(toolbar, 1000);
toolbar.startAnimation(expandAni);
TextView tv = (TextView) convertView.findViewById(R.id.childTo);
tv.setText(text);
return convertView;
}
But when I click the groupItem to collapse the group,it doesn't call the getChildView() method.So how can I to call the getChildView() and let it slide up?
I believe that you want to extend BaseExpandableListAdapter if you want to call (or #Override) getChildView.
http://developer.android.com/reference/android/widget/BaseExpandableListAdapter.html

WP7: LayoutTransform a ListBoxItem

Here's my XAML:
<ListBox x:Name="MyListBox" FontSize="40"
SelectionChanged="MyListBox_SelectionChanged">
</ListBox>
In this code-behind (below), I am attempting to animate a delete action. When the item is selected I delete it. I visually animate it with a ScaleTransform. In WPF I would use a LayoutTransform, but since I only have RenderTransform in WP/SL, I am using RenderTransform - and as a result the surrounding layout is not responding to the change in size. The record is still correctly deleted, but the visual effect is diminished.
Is there a way to do this in WP? Is there a way to resize a ListBoxItem so that the surrounding content responds?
ObservableCollection<string> m_Data;
public MainPage()
{
InitializeComponent();
m_Data = new ObservableCollection<string>
{ "One", "Two", "Three", "Four" };
MyListBox.ItemsSource = m_Data;
}
private void MyListBox_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
// fetch ListBoxItem
if (e.AddedItems.Count == 0)
return;
var _Data = e.AddedItems[0] as string;
var _Item = MyListBox.ItemContainerGenerator
.ContainerFromItem(_Data) as ListBoxItem;
// setup to resize using scale transform
var _Scale = new ScaleTransform
{
CenterX = _Item.RenderSize.Width / 2,
CenterY = _Item.RenderSize.Height / 2,
ScaleX = .99,
ScaleY = .99
};
_Item.RenderTransform = _Scale;
// setup storyboard
var _Story = new Storyboard();
_Story.Completed += (s, e1) =>
{
// remove data from collection
m_Data.Remove(_Data);
};
// animate scale X
var _AnimationX = new DoubleAnimation
{
To = .01,
Duration = TimeSpan.FromSeconds(2),
};
_Story.Children.Add(_AnimationX);
Storyboard.SetTarget(_AnimationX, _Scale);
Storyboard.SetTargetProperty(_AnimationX,
new PropertyPath(ScaleTransform.ScaleXProperty));
// animate scale Y
var _AnimationY = new DoubleAnimation
{
To = .01,
Duration = TimeSpan.FromSeconds(2),
};
_Story.Children.Add(_AnimationY);
Storyboard.SetTarget(_AnimationY, _Scale);
Storyboard.SetTargetProperty(_AnimationY,
new PropertyPath(ScaleTransform.ScaleYProperty));
_Story.Begin();
}
You can also use LayoutTransform on Windows Phone so I would just use that. .

I need an algorithm that can fit n rectangles of any size in a larger one minimizing its area

I need an algorithm that would take n rectangles of any sizes, and calculate a rectangle big enough to fit them all, minimizing its area so the wasted area is minimum, and also returning the position of all the smaller rectangles within.
The specific task I need this to implement on is in a sprite sheet compiler that would take individual PNG files and make a large PNG with all the images in it, so individual frames can be blitted from this surface at run time.
A nice to have feature would be that it aims to a specific given width/height ratio, but it's not mandatory.
I'd prefer simple, generic code I can port to another language.
This is what I put together for my own needs. The T parameter is whatever object you want associated with the results (think of it like the Tag property). It takes a list of sizes and returns a list of Rects that are arranged
static class LayoutHelper
{
/// <summary>
/// Determines the best fit of a List of Sizes, into the desired rectangle shape
/// </summary>
/// <typeparam name="T">Holder for an associated object (e.g., window, UserControl, etc.)</typeparam>
/// <param name="desiredWidthToHeightRatio">the target rectangle shape</param>
/// <param name="rectsToArrange">List of sizes that have to fit in the rectangle</param>
/// <param name="lossiness">1 = non-lossy (slow). Greater numbers improve speed, but miss some best fits</param>
/// <returns>list of arranged rects</returns>
static public List<Tuple<T, Rect>> BestFitRects<T>(double desiredWidthToHeightRatio,
List<Tuple<Size, T>> rectsToArrange, int lossiness = 10)
{
// helper anonymous function that tests for rectangle intersections or boundary violations
var CheckIfRectsIntersect = new Func<Rect, List<Rect>, double, bool>((one, list, containerHeight) =>
{
if (one.Y + one.Height > containerHeight) return true;
return list.Any(two =>
{
if ((one.Top > two.Bottom) ||
(one.Bottom < two.Top) ||
(one.Left > two.Right) ||
(one.Right < two.Left)) return false; // no intersection
return true; // intersection found
});
});
// helper anonymous function for adding drop points
var AddNewPotentialDropPoints = new Action<SortedDictionary<Point, object>, Rect>(
(potentialDropPoints, newRect) =>
{
// Only two locations make sense for placing a new rectangle, underneath the
// bottom left corner or to the right of a top right corner
potentialDropPoints[new Point(newRect.X + newRect.Width + 1,
newRect.Y)] = null;
potentialDropPoints[new Point(newRect.X,
newRect.Y + newRect.Height + 1)] = null;
});
var sync = new object();
// the outer boundary that limits how high the rectangles can stack vertically
var containingRectHeight = Convert.ToInt32(rectsToArrange.Max(a => a.Item1.Height));
// always try packing using the tallest rectangle first, working down in height
var largestToSmallest = rectsToArrange.OrderByDescending(a => a.Item1.Height).ToList();
// find the maximum possible container height needed
var totalHeight = Convert.ToInt32(rectsToArrange.Sum(a => a.Item1.Height));
List<Tuple<T, Rect>> bestResults = null;
// used to find the best packing arrangement that approximates the target container dimensions ratio
var bestResultsProximityToDesiredRatio = double.MaxValue;
// try all arrangements for all suitable container sizes
Parallel.For(0, ((totalHeight + 1) - containingRectHeight) / lossiness,
//new ParallelOptions() { MaxDegreeOfParallelism = 1},
currentHeight =>
{
var potentialDropPoints = new SortedDictionary<Point, object>(Comparer<Point>.Create((p1, p2) =>
{
// choose the leftmost, then highest point as earlier in the sort order
if (p1.X != p2.X) return p1.X.CompareTo(p2.X);
return p1.Y.CompareTo(p2.Y);
}));
var localResults = new List<Tuple<T, Rect>>();
// iterate through the rectangles from largest to smallest
largestToSmallest.ForEach(currentSize =>
{
// check to see if the next rectangle fits in with the currently arranged rectangles
if (!potentialDropPoints.Any(dropPoint =>
{
var workingPoint = dropPoint.Key;
Rect? lastFittingRect = null;
var lowY = workingPoint.Y;
var highY = workingPoint.Y - 1;
var boundaryFound = false;
// check if it fits in the current arrangement of rects
do
{
// create a positioned rectangle out of the size dimensions
var workingRect = new Rect(workingPoint,
new Point(workingPoint.X + currentSize.Item1.Width,
workingPoint.Y + currentSize.Item1.Height));
// keep moving it up in binary search fashion until it bumps the higher rect
if (!CheckIfRectsIntersect(workingRect, localResults.Select(a => a.Item2).ToList(),
containingRectHeight + (currentHeight * lossiness)))
{
lastFittingRect = workingRect;
if (!boundaryFound)
{
highY = Math.Max(lowY - ((lowY - highY) * 2), 0);
if (highY == 0) boundaryFound = true;
}
else
{
lowY = workingPoint.Y;
}
}
else
{
boundaryFound = true;
highY = workingPoint.Y;
}
workingPoint = new Point(workingPoint.X, lowY - (lowY - highY) / 2);
} while (lowY - highY > 1);
if (lastFittingRect.HasValue) // found the sweet spot for this rect
{
var newRect = lastFittingRect.Value;
potentialDropPoints.Remove(dropPoint.Key);
// successfully found the best location for the new rectangle, so add it to the pending results
localResults.Add(Tuple.Create(currentSize.Item2, newRect));
AddNewPotentialDropPoints(potentialDropPoints, newRect);
return true;
}
return false;
}))
{
// this only occurs on the first square
var newRect = new Rect(0, 0, currentSize.Item1.Width, currentSize.Item1.Height);
localResults.Add(Tuple.Create(currentSize.Item2, newRect));
AddNewPotentialDropPoints(potentialDropPoints, newRect);
}
});
// layout is complete, now see if this layout is the best one found so far
var layoutHeight = localResults.Max(a => a.Item2.Y + a.Item2.Height);
var layoutWidth = localResults.Max(a => a.Item2.X + a.Item2.Width);
var widthMatchingDesiredRatio = desiredWidthToHeightRatio * layoutHeight;
double ratioProximity;
if (layoutWidth < widthMatchingDesiredRatio)
ratioProximity = widthMatchingDesiredRatio / layoutWidth;
else
ratioProximity = layoutWidth / widthMatchingDesiredRatio;
lock (sync)
{
if (ratioProximity < bestResultsProximityToDesiredRatio)
{
// this layout is the best approximation of the desired container dimensions, so far
bestResults = localResults;
bestResultsProximityToDesiredRatio = ratioProximity;
}
}
});
return bestResults ?? new List<Tuple<T, Rect>>() {Tuple.Create(rectsToArrange[0].Item2,
new Rect(new Point(0, 0), new Point(rectsToArrange[0].Item1.Width, rectsToArrange[0].Item1.Height))) };
}
}