зеркало из https://github.com/DeGsoft/maui-linux.git
437 строки
12 KiB
C#
437 строки
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using Xamarin.Forms.Internals;
|
|
|
|
namespace Xamarin.Forms
|
|
{
|
|
[ContentProperty("Children")]
|
|
public abstract class Layout<T> : Layout, IViewContainer<T> where T : View
|
|
{
|
|
readonly ElementCollection<T> _children;
|
|
|
|
protected Layout()
|
|
{
|
|
_children = new ElementCollection<T>(InternalChildren);
|
|
}
|
|
|
|
public IList<T> Children
|
|
{
|
|
get { return _children; }
|
|
}
|
|
|
|
protected virtual void OnAdded(T view)
|
|
{
|
|
}
|
|
|
|
protected override void OnChildAdded(Element child)
|
|
{
|
|
base.OnChildAdded(child);
|
|
|
|
var typedChild = child as T;
|
|
if (typedChild != null)
|
|
OnAdded(typedChild);
|
|
}
|
|
|
|
protected override void OnChildRemoved(Element child)
|
|
{
|
|
base.OnChildRemoved(child);
|
|
|
|
var typedChild = child as T;
|
|
if (typedChild != null)
|
|
OnRemoved(typedChild);
|
|
}
|
|
|
|
protected virtual void OnRemoved(T view)
|
|
{
|
|
}
|
|
}
|
|
|
|
public abstract class Layout : View, ILayout, ILayoutController
|
|
{
|
|
public static readonly BindableProperty IsClippedToBoundsProperty = BindableProperty.Create("IsClippedToBounds", typeof(bool), typeof(Layout), false);
|
|
|
|
public static readonly BindableProperty PaddingProperty = BindableProperty.Create("Padding", typeof(Thickness), typeof(Layout), default(Thickness), propertyChanged: (bindable, old, newValue) =>
|
|
{
|
|
var layout = (Layout)bindable;
|
|
layout.UpdateChildrenLayout();
|
|
});
|
|
|
|
static IList<KeyValuePair<Layout, int>> s_resolutionList = new List<KeyValuePair<Layout, int>>();
|
|
static bool s_relayoutInProgress;
|
|
bool _allocatedFlag;
|
|
|
|
bool _hasDoneLayout;
|
|
Size _lastLayoutSize = new Size(-1, -1);
|
|
|
|
ReadOnlyCollection<Element> _logicalChildren;
|
|
|
|
protected Layout()
|
|
{
|
|
InternalChildren.CollectionChanged += InternalChildrenOnCollectionChanged;
|
|
}
|
|
|
|
public bool IsClippedToBounds
|
|
{
|
|
get { return (bool)GetValue(IsClippedToBoundsProperty); }
|
|
set { SetValue(IsClippedToBoundsProperty, value); }
|
|
}
|
|
|
|
public Thickness Padding
|
|
{
|
|
get { return (Thickness)GetValue(PaddingProperty); }
|
|
set { SetValue(PaddingProperty, value); }
|
|
}
|
|
|
|
internal ObservableCollection<Element> InternalChildren { get; } = new ObservableCollection<Element>();
|
|
|
|
internal override ReadOnlyCollection<Element> LogicalChildrenInternal
|
|
{
|
|
get { return _logicalChildren ?? (_logicalChildren = new ReadOnlyCollection<Element>(InternalChildren)); }
|
|
}
|
|
|
|
public event EventHandler LayoutChanged;
|
|
|
|
IReadOnlyList<Element> ILayoutController.Children
|
|
{
|
|
get { return InternalChildren; }
|
|
}
|
|
|
|
public void ForceLayout()
|
|
{
|
|
SizeAllocated(Width, Height);
|
|
}
|
|
|
|
[Obsolete("Use Measure")]
|
|
public sealed override SizeRequest GetSizeRequest(double widthConstraint, double heightConstraint)
|
|
{
|
|
SizeRequest size = base.GetSizeRequest(widthConstraint - Padding.HorizontalThickness, heightConstraint - Padding.VerticalThickness);
|
|
return new SizeRequest(new Size(size.Request.Width + Padding.HorizontalThickness, size.Request.Height + Padding.VerticalThickness),
|
|
new Size(size.Minimum.Width + Padding.HorizontalThickness, size.Minimum.Height + Padding.VerticalThickness));
|
|
}
|
|
|
|
public static void LayoutChildIntoBoundingRegion(VisualElement child, Rectangle region)
|
|
{
|
|
var view = child as View;
|
|
if (view == null)
|
|
{
|
|
child.Layout(region);
|
|
return;
|
|
}
|
|
|
|
LayoutOptions horizontalOptions = view.HorizontalOptions;
|
|
if (horizontalOptions.Alignment != LayoutAlignment.Fill)
|
|
{
|
|
SizeRequest request = child.Measure(region.Width, region.Height, MeasureFlags.IncludeMargins);
|
|
double diff = Math.Max(0, region.Width - request.Request.Width);
|
|
region.X += (int)(diff * horizontalOptions.Alignment.ToDouble());
|
|
region.Width -= diff;
|
|
}
|
|
|
|
LayoutOptions verticalOptions = view.VerticalOptions;
|
|
if (verticalOptions.Alignment != LayoutAlignment.Fill)
|
|
{
|
|
SizeRequest request = child.Measure(region.Width, region.Height, MeasureFlags.IncludeMargins);
|
|
double diff = Math.Max(0, region.Height - request.Request.Height);
|
|
region.Y += (int)(diff * verticalOptions.Alignment.ToDouble());
|
|
region.Height -= diff;
|
|
}
|
|
|
|
Thickness margin = view.Margin;
|
|
region.X += margin.Left;
|
|
region.Width -= margin.HorizontalThickness;
|
|
region.Y += margin.Top;
|
|
region.Height -= margin.VerticalThickness;
|
|
|
|
child.Layout(region);
|
|
}
|
|
|
|
public void LowerChild(View view)
|
|
{
|
|
if (!InternalChildren.Contains(view) || InternalChildren.First() == view)
|
|
return;
|
|
|
|
InternalChildren.Move(InternalChildren.IndexOf(view), 0);
|
|
OnChildrenReordered();
|
|
}
|
|
|
|
public void RaiseChild(View view)
|
|
{
|
|
if (!InternalChildren.Contains(view) || InternalChildren.Last() == view)
|
|
return;
|
|
|
|
InternalChildren.Move(InternalChildren.IndexOf(view), InternalChildren.Count - 1);
|
|
OnChildrenReordered();
|
|
}
|
|
|
|
protected virtual void InvalidateLayout()
|
|
{
|
|
_hasDoneLayout = false;
|
|
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
|
|
if (!_hasDoneLayout)
|
|
ForceLayout();
|
|
}
|
|
|
|
protected abstract void LayoutChildren(double x, double y, double width, double height);
|
|
|
|
protected void OnChildMeasureInvalidated(object sender, EventArgs e)
|
|
{
|
|
InvalidationTrigger trigger = (e as InvalidationEventArgs)?.Trigger ?? InvalidationTrigger.Undefined;
|
|
OnChildMeasureInvalidated((VisualElement)sender, trigger);
|
|
OnChildMeasureInvalidated();
|
|
}
|
|
|
|
protected virtual void OnChildMeasureInvalidated()
|
|
{
|
|
}
|
|
|
|
protected override void OnSizeAllocated(double width, double height)
|
|
{
|
|
_allocatedFlag = true;
|
|
base.OnSizeAllocated(width, height);
|
|
UpdateChildrenLayout();
|
|
}
|
|
|
|
protected virtual bool ShouldInvalidateOnChildAdded(View child)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
protected virtual bool ShouldInvalidateOnChildRemoved(View child)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
protected void UpdateChildrenLayout()
|
|
{
|
|
_hasDoneLayout = true;
|
|
|
|
if (!ShouldLayoutChildren())
|
|
return;
|
|
|
|
var oldBounds = new Rectangle[LogicalChildrenInternal.Count];
|
|
for (var index = 0; index < oldBounds.Length; index++)
|
|
{
|
|
var c = (VisualElement)LogicalChildrenInternal[index];
|
|
oldBounds[index] = c.Bounds;
|
|
}
|
|
|
|
double width = Width;
|
|
double height = Height;
|
|
|
|
double x = Padding.Left;
|
|
double y = Padding.Top;
|
|
double w = Math.Max(0, width - Padding.HorizontalThickness);
|
|
double h = Math.Max(0, height - Padding.VerticalThickness);
|
|
|
|
LayoutChildren(x, y, w, h);
|
|
|
|
for (var i = 0; i < oldBounds.Length; i++)
|
|
{
|
|
Rectangle oldBound = oldBounds[i];
|
|
Rectangle newBound = ((VisualElement)LogicalChildrenInternal[i]).Bounds;
|
|
if (oldBound != newBound)
|
|
{
|
|
EventHandler handler = LayoutChanged;
|
|
if (handler != null)
|
|
handler(this, EventArgs.Empty);
|
|
return;
|
|
}
|
|
}
|
|
|
|
_lastLayoutSize = new Size(width, height);
|
|
}
|
|
|
|
internal static void LayoutChildIntoBoundingRegion(View child, Rectangle region, SizeRequest childSizeRequest)
|
|
{
|
|
if (region.Size != childSizeRequest.Request)
|
|
{
|
|
bool canUseAlreadyDoneRequest = region.Width >= childSizeRequest.Request.Width && region.Height >= childSizeRequest.Request.Height;
|
|
|
|
if (child.HorizontalOptions.Alignment != LayoutAlignment.Fill)
|
|
{
|
|
SizeRequest request = canUseAlreadyDoneRequest ? childSizeRequest : child.Measure(region.Width, region.Height, MeasureFlags.IncludeMargins);
|
|
double diff = Math.Max(0, region.Width - request.Request.Width);
|
|
region.X += (int)(diff * child.HorizontalOptions.Alignment.ToDouble());
|
|
region.Width -= diff;
|
|
}
|
|
|
|
if (child.VerticalOptions.Alignment != LayoutAlignment.Fill)
|
|
{
|
|
SizeRequest request = canUseAlreadyDoneRequest ? childSizeRequest : child.Measure(region.Width, region.Height, MeasureFlags.IncludeMargins);
|
|
double diff = Math.Max(0, region.Height - request.Request.Height);
|
|
region.Y += (int)(diff * child.VerticalOptions.Alignment.ToDouble());
|
|
region.Height -= diff;
|
|
}
|
|
}
|
|
|
|
Thickness margin = child.Margin;
|
|
region.X += margin.Left;
|
|
region.Width -= margin.HorizontalThickness;
|
|
region.Y += margin.Top;
|
|
region.Height -= margin.VerticalThickness;
|
|
|
|
child.Layout(region);
|
|
}
|
|
|
|
internal virtual void OnChildMeasureInvalidated(VisualElement child, InvalidationTrigger trigger)
|
|
{
|
|
ReadOnlyCollection<Element> children = LogicalChildrenInternal;
|
|
int count = children.Count;
|
|
for (var index = 0; index < count; index++)
|
|
{
|
|
var v = LogicalChildrenInternal[index] as VisualElement;
|
|
if (v != null && v.IsVisible && (!v.IsPlatformEnabled || !v.IsNativeStateConsistent))
|
|
return;
|
|
}
|
|
|
|
var view = child as View;
|
|
if (view != null)
|
|
{
|
|
// we can ignore the request if we are either fully constrained or when the size request changes and we were already fully constrainted
|
|
if ((trigger == InvalidationTrigger.MeasureChanged && view.Constraint == LayoutConstraint.Fixed) ||
|
|
(trigger == InvalidationTrigger.SizeRequestChanged && view.ComputedConstraint == LayoutConstraint.Fixed))
|
|
{
|
|
return;
|
|
}
|
|
if (trigger == InvalidationTrigger.HorizontalOptionsChanged || trigger == InvalidationTrigger.VerticalOptionsChanged)
|
|
{
|
|
ComputeConstraintForView(view);
|
|
}
|
|
}
|
|
|
|
_allocatedFlag = false;
|
|
if (trigger == InvalidationTrigger.RendererReady)
|
|
{
|
|
InvalidateMeasureInternal(InvalidationTrigger.RendererReady);
|
|
}
|
|
else
|
|
{
|
|
InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
|
|
}
|
|
|
|
s_resolutionList.Add(new KeyValuePair<Layout, int>(this, GetElementDepth(this)));
|
|
if (!s_relayoutInProgress)
|
|
{
|
|
s_relayoutInProgress = true;
|
|
Device.BeginInvokeOnMainThread(() =>
|
|
{
|
|
// if thread safety mattered we would need to lock this and compareexchange above
|
|
IList<KeyValuePair<Layout, int>> copy = s_resolutionList;
|
|
s_resolutionList = new List<KeyValuePair<Layout, int>>();
|
|
s_relayoutInProgress = false;
|
|
|
|
foreach (KeyValuePair<Layout, int> kvp in copy.OrderBy(kvp => kvp.Value))
|
|
{
|
|
Layout layout = kvp.Key;
|
|
double width = layout.Width, height = layout.Height;
|
|
if (!layout._allocatedFlag && width >= 0 && height >= 0)
|
|
{
|
|
layout.SizeAllocated(width, height);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
internal override void OnIsVisibleChanged(bool oldValue, bool newValue)
|
|
{
|
|
base.OnIsVisibleChanged(oldValue, newValue);
|
|
if (newValue)
|
|
{
|
|
if (_lastLayoutSize != new Size(Width, Height))
|
|
{
|
|
UpdateChildrenLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
static int GetElementDepth(Element view)
|
|
{
|
|
var result = 0;
|
|
while (view.Parent != null)
|
|
{
|
|
result++;
|
|
view = view.Parent;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void InternalChildrenOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
if (e.Action == NotifyCollectionChangedAction.Move)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (e.OldItems != null)
|
|
{
|
|
foreach (object item in e.OldItems)
|
|
{
|
|
var v = item as View;
|
|
if (v == null)
|
|
continue;
|
|
|
|
OnInternalRemoved(v);
|
|
}
|
|
}
|
|
|
|
if (e.NewItems != null)
|
|
{
|
|
foreach (object item in e.NewItems)
|
|
{
|
|
var v = item as View;
|
|
if (v == null)
|
|
continue;
|
|
|
|
if (item == this)
|
|
throw new InvalidOperationException("Can not add self to own child collection.");
|
|
|
|
OnInternalAdded(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
void OnInternalAdded(View view)
|
|
{
|
|
var parent = view.Parent as Layout;
|
|
parent?.InternalChildren.Remove(view);
|
|
|
|
OnChildAdded(view);
|
|
if (ShouldInvalidateOnChildAdded(view))
|
|
InvalidateLayout();
|
|
|
|
view.MeasureInvalidated += OnChildMeasureInvalidated;
|
|
}
|
|
|
|
void OnInternalRemoved(View view)
|
|
{
|
|
view.MeasureInvalidated -= OnChildMeasureInvalidated;
|
|
|
|
OnChildRemoved(view);
|
|
if (ShouldInvalidateOnChildRemoved(view))
|
|
InvalidateLayout();
|
|
}
|
|
|
|
bool ShouldLayoutChildren()
|
|
{
|
|
if (Width <= 0 || Height <= 0 || !LogicalChildrenInternal.Any() || !IsVisible || !IsNativeStateConsistent || DisableLayout)
|
|
return false;
|
|
|
|
foreach (Element element in VisibleDescendants())
|
|
{
|
|
var visual = element as VisualElement;
|
|
if (visual == null || !visual.IsVisible)
|
|
continue;
|
|
|
|
if (!visual.IsPlatformEnabled || !visual.IsNativeStateConsistent)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
} |