From 17fdde66d94155fc62a034fa6658995bef6fd6e5 Mon Sep 17 00:00:00 2001 From: Jason Smith Date: Tue, 22 Mar 2016 13:02:25 -0700 Subject: Initial import --- Xamarin.Forms.Core/Layout.cs | 433 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 Xamarin.Forms.Core/Layout.cs (limited to 'Xamarin.Forms.Core/Layout.cs') diff --git a/Xamarin.Forms.Core/Layout.cs b/Xamarin.Forms.Core/Layout.cs new file mode 100644 index 00000000..c611777c --- /dev/null +++ b/Xamarin.Forms.Core/Layout.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace Xamarin.Forms +{ + [ContentProperty("Children")] + public abstract class Layout : Layout, IViewContainer where T : View + { + readonly ElementCollection _children; + + protected Layout() + { + _children = new ElementCollection(InternalChildren); + } + + public IList 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> s_resolutionList = new List>(); + static bool s_relayoutInProgress; + bool _allocatedFlag; + + bool _hasDoneLayout; + Size _lastLayoutSize = new Size(-1, -1); + + ReadOnlyCollection _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 InternalChildren { get; } = new ObservableCollection(); + + internal override ReadOnlyCollection LogicalChildren + { + get { return _logicalChildren ?? (_logicalChildren = new ReadOnlyCollection(InternalChildren)); } + } + + public event EventHandler LayoutChanged; + + IReadOnlyList 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; + InvalidateMeasure(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[LogicalChildren.Count]; + for (var index = 0; index < oldBounds.Length; index++) + { + var c = (VisualElement)LogicalChildren[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)LogicalChildren[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 children = LogicalChildren; + int count = children.Count; + for (var index = 0; index < count; index++) + { + var v = LogicalChildren[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) + { + InvalidateMeasure(InvalidationTrigger.RendererReady); + } + else + { + InvalidateMeasure(InvalidationTrigger.MeasureChanged); + } + + s_resolutionList.Add(new KeyValuePair(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> copy = s_resolutionList; + s_resolutionList = new List>(); + s_relayoutInProgress = false; + + foreach (KeyValuePair 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) + { + 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 (!LogicalChildren.Any() || Width <= 0 || Height <= 0 || !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; + } + } +} \ No newline at end of file -- cgit v1.2.3