diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs
index 9ec70a4eb1..76f4cdd106 100644
--- a/samples/XamlTestApplicationPcl/TestScrollable.cs
+++ b/samples/XamlTestApplicationPcl/TestScrollable.cs
@@ -14,6 +14,7 @@ namespace XamlTestApplication
private Size _viewport;
private Size _lineSize;
+ public bool IsLogicalScrollEnabled => true;
public Action InvalidateScroll { get; set; }
Size IScrollable.Extent
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 0077497b10..abb606435d 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -69,7 +69,7 @@ namespace Avalonia.Controls.Presenters
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
- this.GetObservable(ChildProperty).Subscribe(ChildChanged);
+ this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
///
@@ -194,22 +194,25 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize)
{
var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable;
- var offset = default(Vector);
+ var logicalScroll = _scrollableSubscription != null;
- if (_scrollableSubscription == null)
+ if (!logicalScroll)
{
Viewport = finalSize;
Extent = _measuredExtent;
- offset = Offset;
- }
- if (child != null)
- {
- var size = new Size(
+ if (child != null)
+ {
+ var size = new Size(
Math.Max(finalSize.Width, child.DesiredSize.Width),
Math.Max(finalSize.Height, child.DesiredSize.Height));
- child.Arrange(new Rect((Point)(-offset), size));
- return finalSize;
+ child.Arrange(new Rect((Point)(-Offset), size));
+ return finalSize;
+ }
+ }
+ else if (child != null)
+ {
+ child.Arrange(new Rect(finalSize));
}
return new Size();
@@ -222,7 +225,7 @@ namespace Avalonia.Controls.Presenters
{
var scrollable = Child as IScrollable;
- if (scrollable != null)
+ if (scrollable?.IsLogicalScrollEnabled == true)
{
var y = Offset.Y + (-e.Delta.Y * scrollable.ScrollSize.Height);
y = Math.Max(y, 0);
@@ -246,7 +249,7 @@ namespace Avalonia.Controls.Presenters
e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect);
}
- private void ChildChanged(IControl child)
+ private void UpdateScrollableSubscription(IControl child)
{
var scrollable = child as IScrollable;
@@ -256,18 +259,38 @@ namespace Avalonia.Controls.Presenters
if (scrollable != null)
{
scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
- _scrollableSubscription = new CompositeDisposable(
- this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
- Disposable.Create(() => scrollable.InvalidateScroll = null));
- UpdateFromScrollable(scrollable);
+
+ if (scrollable?.IsLogicalScrollEnabled == true)
+ {
+ _scrollableSubscription = new CompositeDisposable(
+ this.GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x),
+ Disposable.Create(() => scrollable.InvalidateScroll = null));
+ UpdateFromScrollable(scrollable);
+ }
}
}
private void UpdateFromScrollable(IScrollable scrollable)
{
- Viewport = scrollable.Viewport;
- Extent = scrollable.Extent;
- Offset = scrollable.Offset;
+ var logicalScroll = _scrollableSubscription != null;
+
+ if (logicalScroll != scrollable.IsLogicalScrollEnabled)
+ {
+ UpdateScrollableSubscription(Child);
+
+ if (!scrollable.IsLogicalScrollEnabled)
+ {
+ Offset = default(Vector);
+ InvalidateMeasure();
+ }
+ }
+
+ if (scrollable.IsLogicalScrollEnabled)
+ {
+ Viewport = scrollable.Viewport;
+ Extent = scrollable.Extent;
+ Offset = scrollable.Offset;
+ }
}
}
}
diff --git a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs
index 00f0c92535..2171e87c51 100644
--- a/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ThingamybobPresenter.cs
@@ -23,6 +23,7 @@ namespace Avalonia.Controls.Presenters
public IPanel Panel => _panel;
+ bool IScrollable.IsLogicalScrollEnabled => true;
Action IScrollable.InvalidateScroll { get; set; }
Size IScrollable.Extent => new Size(1, 100 * AverageItemSize );
diff --git a/src/Avalonia.Controls/Primitives/IScrollable.cs b/src/Avalonia.Controls/Primitives/IScrollable.cs
index a5165fcfb4..d37d1fdcca 100644
--- a/src/Avalonia.Controls/Primitives/IScrollable.cs
+++ b/src/Avalonia.Controls/Primitives/IScrollable.cs
@@ -9,8 +9,20 @@ namespace Avalonia.Controls.Primitives
/// Interface implemented by controls that handle their own scrolling when placed inside a
/// .
///
+ ///
+ /// Controls that implement this interface, when placed inside a
+ /// can override the physical scrolling behavior of the scroll viewer with logical scrolling.
+ /// Physical scrolling means that the scroll viewer is a simple viewport onto a larger canvas
+ /// whereas logical scrolling means that the scrolling is handled by the child control itself
+ /// and it can choose to do handle the scroll information as it sees fit.
+ ///
public interface IScrollable
{
+ ///
+ /// Gets a value indicating whether logical scrolling is enabled on the control.
+ ///
+ bool IsLogicalScrollEnabled { get; }
+
///
/// Gets or sets the scroll invalidation method.
///
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs
index db3b2a4a19..1aea155eaa 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs
@@ -5,6 +5,7 @@ using System;
using System.Reactive.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
+using Avalonia.Layout;
using Xunit;
namespace Avalonia.Controls.UnitTests
@@ -48,6 +49,27 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
}
+ [Fact]
+ public void Arrange_Should_Offset_IScrollable_Bounds_When_Logical_Scroll_Disabled()
+ {
+ var scrollable = new TestScrollable
+ {
+ IsLogicalScrollEnabled = false,
+ };
+
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable,
+ Offset = new Vector(25, 25),
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(new Rect(-25, -25, 150, 150), scrollable.Bounds);
+ }
+
[Fact]
public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable()
{
@@ -169,12 +191,59 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Vector(50, 50), scrollable.Offset);
}
+ [Fact]
+ public void Toggling_IsLogicalScrollEnabled_Should_Update_State()
+ {
+ var scrollable = new TestScrollable
+ {
+ Extent = new Size(100, 100),
+ Offset = new Vector(50, 50),
+ Viewport = new Size(25, 25),
+ };
+
+ var target = new ScrollContentPresenter
+ {
+ Content = scrollable,
+ };
+
+ target.UpdateChild();
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(scrollable.Extent, target.Extent);
+ Assert.Equal(scrollable.Offset, target.Offset);
+ Assert.Equal(scrollable.Viewport, target.Viewport);
+ Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
+
+ scrollable.IsLogicalScrollEnabled = false;
+ scrollable.InvalidateScroll();
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(new Size(150, 150), target.Extent);
+ Assert.Equal(new Vector(0, 0), target.Offset);
+ Assert.Equal(new Size(100, 100), target.Viewport);
+ Assert.Equal(new Rect(0, 0, 150, 150), scrollable.Bounds);
+
+ scrollable.IsLogicalScrollEnabled = true;
+ scrollable.InvalidateScroll();
+ target.Measure(new Size(100, 100));
+ target.Arrange(new Rect(0, 0, 100, 100));
+
+ Assert.Equal(scrollable.Extent, target.Extent);
+ Assert.Equal(scrollable.Offset, target.Offset);
+ Assert.Equal(scrollable.Viewport, target.Viewport);
+ Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds);
+ }
+
+
private class TestScrollable : Control, IScrollable
{
private Size _extent;
private Vector _offset;
private Size _viewport;
+ public bool IsLogicalScrollEnabled { get; set; } = true;
public Size AvailableSize { get; private set; }
public Action InvalidateScroll { get; set; }