Browse Source

Initial implementation of EffectiveViewportChanged.

pull/4173/head
Steven Kirk 6 years ago
parent
commit
9c7aeaf713
  1. 24
      src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs
  2. 12
      src/Avalonia.Layout/ILayoutManager.cs
  3. 7
      src/Avalonia.Layout/ILayoutable.cs
  4. 124
      src/Avalonia.Layout/LayoutManager.cs
  5. 70
      src/Avalonia.Layout/Layoutable.cs
  6. 333
      tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs

24
src/Avalonia.Layout/EffectiveViewportChangedEventArgs.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Layout
{
/// <summary>
/// Provides data for the <see cref="Layoutable.EffectiveViewportChanged"/> event.
/// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs
{
public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{
EffectiveViewport = effectiveViewport;
}
/// <summary>
/// Gets the <see cref="Rect"/> representing the effective viewport.
/// </summary>
/// <remarks>
/// The viewport is expressed in coordinates relative to the control that the event is
/// raised on.
/// </remarks>
public Rect EffectiveViewport { get; }
}
}

12
src/Avalonia.Layout/ILayoutManager.cs

@ -54,5 +54,17 @@ namespace Avalonia.Layout
/// </remarks>
[Obsolete("Call ExecuteInitialLayoutPass without parameter")]
void ExecuteInitialLayoutPass(ILayoutRoot root);
/// <summary>
/// Registers a control as wanting to receive effective viewport notifications.
/// </summary>
/// <param name="control">The control.</param>
void RegisterEffectiveViewportListener(ILayoutable control);
/// <summary>
/// Registers a control as no longer wanting to receive effective viewport notifications.
/// </summary>
/// <param name="control">The control.</param>
void UnregisterEffectiveViewportListener(ILayoutable control);
}
}

7
src/Avalonia.Layout/ILayoutable.cs

@ -111,5 +111,12 @@ namespace Avalonia.Layout
/// </summary>
/// <param name="control">The child control.</param>
void ChildDesiredSizeChanged(ILayoutable control);
/// <summary>
/// Used by the <see cref="LayoutManager"/> to notify the control that its effective
/// viewport is changed.
/// </summary>
/// <param name="e">The viewport information.</param>
void EffectiveViewportChanged(EffectiveViewportChangedEventArgs e);
}
}

124
src/Avalonia.Layout/LayoutManager.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Logging;
using Avalonia.Threading;
using Avalonia.VisualTree;
#nullable enable
@ -12,10 +14,12 @@ namespace Avalonia.Layout
/// </summary>
public class LayoutManager : ILayoutManager, IDisposable
{
private const int MaxPasses = 3;
private readonly ILayoutRoot _owner;
private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
private readonly Action _executeLayoutPass;
private List<EffectiveViewportChangedListener>? _effectiveViewportChangedListeners;
private bool _disposed;
private bool _queued;
private bool _running;
@ -92,8 +96,6 @@ namespace Avalonia.Layout
/// <inheritdoc/>
public virtual void ExecuteLayoutPass()
{
const int MaxPasses = 3;
Dispatcher.UIThread.VerifyAccess();
if (_disposed)
@ -125,23 +127,15 @@ namespace Avalonia.Layout
_toMeasure.BeginLoop(MaxPasses);
_toArrange.BeginLoop(MaxPasses);
try
for (var pass = 0; pass < MaxPasses; ++pass)
{
for (var pass = 0; pass < MaxPasses; ++pass)
{
ExecuteMeasurePass();
ExecuteArrangePass();
InnerLayoutPass();
if (_toMeasure.Count == 0)
{
break;
}
if (!RaiseEffectiveViewportChanged())
{
break;
}
}
finally
{
_running = false;
}
_toMeasure.EndLoop();
_toArrange.EndLoop();
@ -202,6 +196,47 @@ namespace Avalonia.Layout
_toArrange.Dispose();
}
void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control)
{
_effectiveViewportChangedListeners ??= new List<EffectiveViewportChangedListener>();
_effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener(control));
}
void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control)
{
if (_effectiveViewportChangedListeners is object)
{
for (var i = 0; i < _effectiveViewportChangedListeners.Count; ++i)
{
if (_effectiveViewportChangedListeners[i].Listener == control)
{
_effectiveViewportChangedListeners.RemoveAt(i);
}
}
}
}
private void InnerLayoutPass()
{
try
{
for (var pass = 0; pass < MaxPasses; ++pass)
{
ExecuteMeasurePass();
ExecuteArrangePass();
if (_toMeasure.Count == 0)
{
break;
}
}
}
finally
{
_running = false;
}
}
private void ExecuteMeasurePass()
{
while (_toMeasure.Count > 0)
@ -285,5 +320,64 @@ namespace Avalonia.Layout
_queued = true;
}
}
private bool RaiseEffectiveViewportChanged()
{
var startCount = _toMeasure.Count + _toArrange.Count;
if (_effectiveViewportChangedListeners is object)
{
// TODO: This may not work correctly if listener is removed in event handler.
for (var i = 0; i < _effectiveViewportChangedListeners.Count; ++i)
{
var l = _effectiveViewportChangedListeners[i];
var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity);
CalculateEffectiveViewport(l.Listener, ref viewport);
if (viewport != l.Viewport)
{
l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
_effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
}
}
}
return startCount != _toMeasure.Count + _toMeasure.Count;
}
private void CalculateEffectiveViewport(IVisual control, ref Rect viewport)
{
if (control.VisualParent is object)
{
CalculateEffectiveViewport(control.VisualParent, ref viewport);
}
if (control.ClipToBounds || control.VisualParent is null)
{
viewport = control.Bounds;
}
else
{
viewport = viewport.Translate(-control.Bounds.Position);
}
}
private readonly struct EffectiveViewportChangedListener
{
public EffectiveViewportChangedListener(ILayoutable listener)
{
Listener = listener;
Viewport = new Rect(double.NaN, double.NaN, double.NaN, double.NaN);
}
public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
{
Listener = listener;
Viewport = viewport;
}
public ILayoutable Listener { get; }
public Rect Viewport { get; }
}
}
}

70
src/Avalonia.Layout/Layoutable.cs

@ -132,6 +132,7 @@ namespace Avalonia.Layout
private bool _measuring;
private Size? _previousMeasure;
private Rect? _previousArrange;
private EventHandler<EffectiveViewportChangedEventArgs>? _effectiveViewportChanged;
private EventHandler? _layoutUpdated;
/// <summary>
@ -152,6 +153,32 @@ namespace Avalonia.Layout
VerticalAlignmentProperty);
}
/// <summary>
/// Occurs when the element's effective viewport changes.
/// </summary>
public event EventHandler<EffectiveViewportChangedEventArgs>? EffectiveViewportChanged
{
add
{
if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.RegisterEffectiveViewportListener(this);
}
_effectiveViewportChanged += value;
}
remove
{
_effectiveViewportChanged -= value;
if (_effectiveViewportChanged is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
}
}
/// <summary>
/// Occurs when a layout pass completes for the control.
/// </summary>
@ -384,13 +411,6 @@ namespace Avalonia.Layout
}
}
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
protected virtual void OnMeasureInvalidated()
{
}
/// <summary>
/// Invalidates the measurement of the control and queues a new layout pass.
/// </summary>
@ -436,6 +456,11 @@ namespace Avalonia.Layout
}
}
void ILayoutable.EffectiveViewportChanged(EffectiveViewportChangedEventArgs e)
{
_effectiveViewportChanged?.Invoke(this, e);
}
/// <summary>
/// Marks a property as affecting the control's measurement.
/// </summary>
@ -717,9 +742,17 @@ namespace Avalonia.Layout
{
base.OnAttachedToVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
if (e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
if (_layoutUpdated is object)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
if (_effectiveViewportChanged is object)
{
r.LayoutManager.RegisterEffectiveViewportListener(this);
}
}
}
@ -727,12 +760,27 @@ namespace Avalonia.Layout
{
base.OnDetachedFromVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
if (e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
if (_layoutUpdated is object)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
if (_effectiveViewportChanged is object)
{
r.LayoutManager.UnregisterEffectiveViewportListener(this);
}
}
}
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
protected virtual void OnMeasureInvalidated()
{
}
/// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{

333
tests/Avalonia.Layout.UnitTests/LayoutableTests_EffectiveViewportChanged.cs

@ -0,0 +1,333 @@
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Layout.UnitTests
{
public class LayoutableTests_EffectiveViewportChanged
{
[Fact]
public void EffectiveViewportChanged_Not_Raised_When_Control_Added_To_Tree()
{
var root = new TestRoot();
var canvas = new Canvas();
var raised = 0;
canvas.EffectiveViewportChanged += (s, e) =>
{
++raised;
};
root.Child = canvas;
Assert.Equal(0, raised);
}
[Fact]
public void EffectiveViewportChanged_Raised_Before_LayoutUpdated()
{
var root = new TestRoot();
var canvas = new Canvas();
var raised = 0;
var layoutUpdatedRaised = 0;
canvas.LayoutUpdated += (s, e) =>
{
Assert.Equal(1, raised);
++layoutUpdatedRaised;
};
canvas.EffectiveViewportChanged += (s, e) =>
{
++raised;
};
root.Child = canvas;
root.LayoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(1, layoutUpdatedRaised);
Assert.Equal(1, raised);
}
[Fact]
public void Invalidating_In_Handler_Causes_Layout_To_Be_Rerun_Before_LayoutUpdated()
{
var root = new TestRoot();
var canvas = new TestCanvas();
var raised = 0;
var layoutUpdatedRaised = 0;
canvas.LayoutUpdated += (s, e) =>
{
Assert.Equal(2, canvas.MeasureCount);
Assert.Equal(2, canvas.ArrangeCount);
++layoutUpdatedRaised;
};
canvas.EffectiveViewportChanged += (s, e) =>
{
canvas.InvalidateMeasure();
++raised;
};
root.Child = canvas;
root.LayoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
Assert.Equal(1, layoutUpdatedRaised);
}
[Fact]
public void Viewport_Extends_Beyond_Centered_Control()
{
var root = new TestRoot
{
Width = 1200,
Height = 900,
};
var canvas = new Canvas
{
Width = 52,
Height = 52,
};
var raised = 0;
canvas.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
++raised;
};
root.Child = canvas;
root.LayoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
}
[Fact]
public void Viewport_Extends_Beyond_Nested_Centered_Control()
{
var root = new TestRoot
{
Width = 1200,
Height = 900,
};
var canvas = new Canvas
{
Width = 52,
Height = 52,
};
var outer = new Border
{
Width = 100,
Height = 100,
Child = canvas,
};
var raised = 0;
canvas.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-574, -424, 1200, 900), e.EffectiveViewport);
++raised;
};
root.Child = outer;
root.LayoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
}
[Fact]
public void ScrollViewer_Determines_EffectiveViewport()
{
var root = new TestRoot
{
Width = 1200,
Height = 900,
};
var canvas = new Canvas
{
Width = 200,
Height = 200,
};
var outer = new ScrollViewer
{
Width = 100,
Height = 100,
Content = canvas,
Template = ScrollViewerTemplate(),
};
var raised = 0;
canvas.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(0, 0, 100, 100), e.EffectiveViewport);
++raised;
};
root.Child = outer;
root.LayoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(1, raised);
}
[Fact]
public void Scrolled_ScrollViewer_Determines_EffectiveViewport()
{
var root = new TestRoot
{
Width = 1200,
Height = 900,
};
var canvas = new Canvas
{
Width = 200,
Height = 200,
};
var outer = new ScrollViewer
{
Width = 100,
Height = 100,
Content = canvas,
Template = ScrollViewerTemplate(),
};
var raised = 0;
root.Child = outer;
root.LayoutManager.ExecuteInitialLayoutPass(root);
canvas.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(0, 10, 100, 100), e.EffectiveViewport);
++raised;
};
outer.Offset = new Vector(0, 10);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(1, raised);
}
[Fact]
public void Moving_Parent_Updates_EffectiveViewport()
{
var root = new TestRoot
{
Width = 1200,
Height = 900,
};
var canvas = new Canvas
{
Width = 100,
Height = 100,
};
var outer = new Border
{
Width = 200,
Height = 200,
Child = canvas,
};
var raised = 0;
root.Child = outer;
root.LayoutManager.ExecuteInitialLayoutPass(root);
canvas.EffectiveViewportChanged += (s, e) =>
{
Assert.Equal(new Rect(-554, -400, 1200, 900), e.EffectiveViewport);
++raised;
};
// Change the parent margin to move it.
outer.Margin = new Thickness(8, 0, 0, 0);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(1, raised);
}
private IControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((control, scope) => new Grid
{
ColumnDefinitions = new ColumnDefinitions
{
new ColumnDefinition(1, GridUnitType.Star),
new ColumnDefinition(GridLength.Auto),
},
RowDefinitions = new RowDefinitions
{
new RowDefinition(1, GridUnitType.Star),
new RowDefinition(GridLength.Auto),
},
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty],
[~~ScrollContentPresenter.ExtentProperty] = control[~~ScrollViewer.ExtentProperty],
[~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty],
[~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty],
[~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty],
[~ScrollContentPresenter.CanVerticallyScrollProperty] = control[~ScrollViewer.CanVerticallyScrollProperty],
}.RegisterInNameScope(scope),
new ScrollBar
{
Name = "horizontalScrollBar",
Orientation = Orientation.Horizontal,
[~RangeBase.MaximumProperty] = control[~ScrollViewer.HorizontalScrollBarMaximumProperty],
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.HorizontalScrollBarValueProperty],
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty],
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty],
[Grid.RowProperty] = 1,
}.RegisterInNameScope(scope),
new ScrollBar
{
Name = "verticalScrollBar",
Orientation = Orientation.Vertical,
[~RangeBase.MaximumProperty] = control[~ScrollViewer.VerticalScrollBarMaximumProperty],
[~~RangeBase.ValueProperty] = control[~~ScrollViewer.VerticalScrollBarValueProperty],
[~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty],
[~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty],
[Grid.ColumnProperty] = 1,
}.RegisterInNameScope(scope),
},
});
}
private class TestCanvas : Canvas
{
public int MeasureCount { get; private set; }
public int ArrangeCount { get; private set; }
protected override Size MeasureOverride(Size availableSize)
{
++MeasureCount;
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
++ArrangeCount;
return base.ArrangeOverride(finalSize);
}
}
}
}
Loading…
Cancel
Save