Browse Source

Deferred scrolling (#13644)

* Implemented deferred scrolling

* Set IsDeferredScrollingEnabled in themes, where relevant
release/11.0.6
Tom Edwards 2 years ago
committed by Max Katz
parent
commit
078185d7c0
  1. 3
      samples/ControlCatalog/Pages/ScrollViewerPage.xaml
  2. 1
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  3. 1
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  4. 1
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  5. 67
      src/Avalonia.Controls/Primitives/Track.cs
  6. 26
      src/Avalonia.Controls/ScrollViewer.cs
  7. 3
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  8. 1
      src/Avalonia.Themes.Fluent/Controls/ListBox.xaml
  9. 2
      src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml
  10. 1
      src/Avalonia.Themes.Fluent/Controls/TreeView.xaml
  11. 3
      src/Avalonia.Themes.Simple/Controls/ComboBox.xaml
  12. 1
      src/Avalonia.Themes.Simple/Controls/ListBox.xaml
  13. 2
      src/Avalonia.Themes.Simple/Controls/ScrollBar.xaml
  14. 1
      src/Avalonia.Themes.Simple/Controls/TreeView.xaml
  15. 41
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

3
samples/ControlCatalog/Pages/ScrollViewerPage.xaml

@ -17,6 +17,9 @@
Content="Allow auto hide" />
<ToggleSwitch IsChecked="{Binding EnableInertia}"
Content="Enable Inertia" />
<ToggleSwitch IsChecked="{Binding #ScrollViewer.IsDeferredScrollingEnabled}"
ToolTip.Tip="When enabled, dragging a scroll bar thumb won't affect the scrolling content until the pointer is released."
Content="Enable Deferred Scrolling" />
<StackPanel Orientation="Vertical"
Spacing="4">

1
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -96,6 +96,7 @@
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"

1
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -96,6 +96,7 @@
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}"

1
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -226,6 +226,7 @@ namespace Avalonia.Controls.Primitives
{
IfUnset(MaximumProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ScrollBarMaximumProperty, ExtractOrdinate), BindingPriority.Template)),
IfUnset(ValueProperty, p => Bind(p, owner.GetObservable(ScrollViewer.OffsetProperty, ExtractOrdinate), BindingPriority.Template)),
IfUnset(ScrollViewer.IsDeferredScrollingEnabledProperty, p => Bind(p, owner.GetObservable(ScrollViewer.IsDeferredScrollingEnabledProperty), BindingPriority.Template)),
IfUnset(ViewportSizeProperty, p => Bind(p, owner.GetObservable(ScrollViewer.ViewportProperty, ExtractOrdinate), BindingPriority.Template)),
IfUnset(VisibilityProperty, p => Bind(p, owner.GetObservable(visibilitySource), BindingPriority.Template)),
IfUnset(AllowAutoHideProperty, p => Bind(p, owner.GetObservable(ScrollViewer.AllowAutoHideProperty), BindingPriority.Template)),

67
src/Avalonia.Controls/Primitives/Track.cs

@ -45,6 +45,10 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<bool> IgnoreThumbDragProperty =
AvaloniaProperty.Register<Track, bool>(nameof(IgnoreThumbDrag));
public static readonly StyledProperty<bool> DeferThumbDragProperty =
AvaloniaProperty.Register<Track, bool>(nameof(DeferThumbDrag));
private VectorEventArgs? _deferredThumbDrag;
private Vector _lastDrag;
static Track()
@ -78,6 +82,11 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ValueProperty, value);
}
/// <summary>
/// Gets the value of the <see cref="Thumb"/>'s current position. This can differ from <see cref="Value"/> when <see cref="ScrollViewer.IsDeferredScrollingEnabled"/> is true.
/// </summary>
private double ThumbValue => Value + (_deferredThumbDrag == null ? 0 : ValueFromDistance(_deferredThumbDrag.Vector.X, _deferredThumbDrag.Vector.Y));
public double ViewportSize
{
get => GetValue(ViewportSizeProperty);
@ -121,6 +130,12 @@ namespace Avalonia.Controls.Primitives
set => SetValue(IgnoreThumbDragProperty, value);
}
public bool DeferThumbDrag
{
get => GetValue(DeferThumbDragProperty);
set => SetValue(DeferThumbDragProperty, value);
}
private double ThumbCenterOffset { get; set; }
private double Density { get; set; }
@ -139,11 +154,11 @@ namespace Avalonia.Controls.Primitives
// Find distance from center of thumb to given point.
if (Orientation == Orientation.Horizontal)
{
val = Value + ValueFromDistance(point.X - ThumbCenterOffset, point.Y - (Bounds.Height * 0.5));
val = ThumbValue + ValueFromDistance(point.X - ThumbCenterOffset, point.Y - (Bounds.Height * 0.5));
}
else
{
val = Value + ValueFromDistance(point.X - (Bounds.Width * 0.5), point.Y - ThumbCenterOffset);
val = ThumbValue + ValueFromDistance(point.X - (Bounds.Width * 0.5), point.Y - ThumbCenterOffset);
}
return Math.Max(Minimum, Math.Min(Maximum, val));
@ -303,6 +318,13 @@ namespace Avalonia.Controls.Primitives
{
UpdatePseudoClasses(change.GetNewValue<Orientation>());
}
else if (change.Property == DeferThumbDragProperty)
{
if (!change.GetNewValue<bool>())
{
ApplyDeferredThumbDrag();
}
}
}
private Vector CalculateThumbAdjustment(Thumb thumb, Rect newThumbBounds)
@ -327,7 +349,7 @@ namespace Avalonia.Controls.Primitives
{
double min = Minimum;
double range = Math.Max(0.0, Maximum - min);
double offset = Math.Min(range, Value - min);
double offset = Math.Min(range, ThumbValue - min);
double trackLength;
@ -360,7 +382,7 @@ namespace Avalonia.Controls.Primitives
{
var min = Minimum;
var range = Math.Max(0.0, Maximum - min);
var offset = Math.Min(range, Value - min);
var offset = Math.Min(range, ThumbValue - min);
var extent = Math.Max(0.0, range) + viewportSize;
var trackLength = isVertical ? arrangeSize.Height : arrangeSize.Width;
double thumbMinLength = 10;
@ -419,7 +441,7 @@ namespace Avalonia.Controls.Primitives
if (oldThumb != null)
{
oldThumb.DragDelta -= ThumbDragged;
oldThumb.DragCompleted -= ThumbDragCompleted;
LogicalChildren.Remove(oldThumb);
VisualChildren.Remove(oldThumb);
}
@ -427,6 +449,7 @@ namespace Avalonia.Controls.Primitives
if (newThumb != null)
{
newThumb.DragDelta += ThumbDragged;
newThumb.DragCompleted += ThumbDragCompleted;
LogicalChildren.Add(newThumb);
VisualChildren.Add(newThumb);
}
@ -455,17 +478,43 @@ namespace Avalonia.Controls.Primitives
if (IgnoreThumbDrag)
return;
var value = Value;
if (DeferThumbDrag)
{
_deferredThumbDrag = e;
InvalidateArrange();
}
else
{
ApplyThumbDrag(e);
}
}
private void ApplyThumbDrag(VectorEventArgs e)
{
var delta = ValueFromDistance(e.Vector.X, e.Vector.Y);
var factor = e.Vector / delta;
var oldValue = Value;
SetCurrentValue(ValueProperty, MathUtilities.Clamp(
value + delta,
Value + delta,
Minimum,
Maximum));
// Record the part of the drag that actually had effect as the last drag delta.
_lastDrag = (Value - value) * factor;
// Due to clamping, we need to compare the two values instead of using the drag delta.
_lastDrag = (Value - oldValue) * factor;
}
private void ThumbDragCompleted(object? sender, EventArgs e) => ApplyDeferredThumbDrag();
private void ApplyDeferredThumbDrag()
{
if (_deferredThumbDrag != null)
{
ApplyThumbDrag(_deferredThumbDrag);
_deferredThumbDrag = null;
}
}
private void ShowChildren(bool visible)

26
src/Avalonia.Controls/ScrollViewer.cs

@ -140,6 +140,13 @@ namespace Avalonia.Controls
nameof(IsScrollInertiaEnabled),
defaultValue: true);
/// <summary>
/// Defines the <see cref="IsDeferredScrollingEnabled"/> property.
/// </summary>
public static readonly AttachedProperty<bool> IsDeferredScrollingEnabledProperty =
AvaloniaProperty.RegisterAttached<ScrollViewer, Control, bool>(
nameof(IsDeferredScrollingEnabled));
/// <summary>
/// Defines the <see cref="ScrollChanged"/> event.
/// </summary>
@ -370,6 +377,15 @@ namespace Avalonia.Controls
set => SetValue(IsScrollInertiaEnabledProperty, value);
}
/// <summary>
/// Gets or sets whether dragging of <see cref="Thumb"/> elements should update the <see cref="ScrollViewer"/> only when the user releases the mouse.
/// </summary>
public bool IsDeferredScrollingEnabled
{
get => GetValue(IsDeferredScrollingEnabledProperty);
set => SetValue(IsDeferredScrollingEnabledProperty, value);
}
/// <summary>
/// Scrolls the content up one line.
/// </summary>
@ -626,6 +642,16 @@ namespace Avalonia.Controls
control.SetValue(IsScrollInertiaEnabledProperty, value);
}
/// <summary>
/// Gets whether dragging of <see cref="Thumb"/> elements should update the <see cref="ScrollViewer"/> only when the user releases the mouse.
/// </summary>
public static bool GetIsDeferredScrollingEnabled(Control control) => control.GetValue(IsDeferredScrollingEnabledProperty);
/// <summary>
/// Sets whether dragging of <see cref="Thumb"/> elements should update the <see cref="ScrollViewer"/> only when the user releases the mouse.
/// </summary>
public static void SetIsDeferredScrollingEnabled(Control control, bool value) => control.SetValue(IsDeferredScrollingEnabledProperty, value);
/// <inheritdoc/>
public void RegisterAnchorCandidate(Control element)
{

3
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -127,7 +127,8 @@
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{DynamicResource ComboBoxDropdownContentMargin}"
ItemsPanel="{TemplateBinding ItemsPanel}" />

1
src/Avalonia.Themes.Fluent/Controls/ListBox.xaml

@ -37,6 +37,7 @@
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
IsScrollInertiaEnabled="{TemplateBinding (ScrollViewer.IsScrollInertiaEnabled)}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
<ItemsPresenter Name="PART_ItemsPresenter"

2
src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml

@ -146,6 +146,7 @@
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
DeferThumbDrag="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
ViewportSize="{TemplateBinding ViewportSize}"
Orientation="{TemplateBinding Orientation}"
IsDirectionReversed="True">
@ -223,6 +224,7 @@
Minimum="{TemplateBinding Minimum}"
Maximum="{TemplateBinding Maximum}"
Value="{TemplateBinding Value, Mode=TwoWay}"
DeferThumbDrag="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
ViewportSize="{TemplateBinding ViewportSize}"
Orientation="{TemplateBinding Orientation}">
<Track.DecreaseButton>

1
src/Avalonia.Themes.Fluent/Controls/TreeView.xaml

@ -31,6 +31,7 @@
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
AllowAutoHide="{TemplateBinding (ScrollViewer.AllowAutoHide)}"
BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
<ItemsPresenter Name="PART_ItemsPresenter"

3
src/Avalonia.Themes.Simple/Controls/ComboBox.xaml

@ -62,7 +62,8 @@
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="1">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>

1
src/Avalonia.Themes.Simple/Controls/ListBox.xaml

@ -22,6 +22,7 @@
Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
VerticalSnapPointsType="{TemplateBinding (ScrollViewer.VerticalSnapPointsType)}"
HorizontalSnapPointsType="{TemplateBinding (ScrollViewer.HorizontalSnapPointsType)}">

2
src/Avalonia.Themes.Simple/Controls/ScrollBar.xaml

@ -27,6 +27,7 @@
Minimum="{TemplateBinding Minimum}"
Orientation="{TemplateBinding Orientation}"
ViewportSize="{TemplateBinding ViewportSize}"
DeferThumbDrag="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
Value="{TemplateBinding Value,
Mode=TwoWay}">
<Track.DecreaseButton>
@ -77,6 +78,7 @@
Minimum="{TemplateBinding Minimum}"
Orientation="{TemplateBinding Orientation}"
ViewportSize="{TemplateBinding ViewportSize}"
DeferThumbDrag="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
Value="{TemplateBinding Value,
Mode=TwoWay}">
<Track.DecreaseButton>

1
src/Avalonia.Themes.Simple/Controls/TreeView.xaml

@ -20,6 +20,7 @@
Background="{TemplateBinding Background}"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
IsScrollChainingEnabled="{TemplateBinding (ScrollViewer.IsScrollChainingEnabled)}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{TemplateBinding Padding}"

41
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -358,6 +358,46 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(100, thumb.Bounds.Top);
}
[Fact]
public void Deferred_Scrolling_Defers_Scrolling_Until_Pointer_Up()
{
var content = new TestContent();
var target = new ScrollViewer
{
Template = new FuncControlTemplate<ScrollViewer>(CreateTemplate),
IsDeferredScrollingEnabled = true,
Content = content,
};
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
// We're working in absolute coordinates (i.e. relative to the root) and clicking on
// the center of the vertical thumb.
var thumb = GetVerticalThumb(target);
var p = GetRootPoint(thumb, thumb.Bounds.Center);
Assert.Equal(Vector.Zero, target.Offset);
Assert.Equal(0, thumb.Bounds.Top);
// Press the mouse button in the center of the thumb.
_mouse.Down(thumb, position: p);
root.LayoutManager.ExecuteLayoutPass();
// Drag the thumb down 100 pixels.
_mouse.Move(thumb, p += new Vector(0, 100));
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(Vector.Zero, target.Offset); // no change to scroll...
Assert.Equal(100, thumb.Bounds.Top); // ...but the Thumb has moved
// Release the mouse
_mouse.Up(thumb, position: p);
Assert.Equal(new Vector(0, 200), target.Offset);
Assert.Equal(100, thumb.Bounds.Top);
}
[Fact]
public void BringIntoViewOnFocusChange_Scrolls_Child_Control_Into_View_When_Focused()
{
@ -493,6 +533,7 @@ namespace Avalonia.Controls.UnitTests
[!!Track.ValueProperty] = scrollBar[!!RangeBase.ValueProperty],
[!Track.ViewportSizeProperty] = scrollBar[!ScrollBar.ViewportSizeProperty],
[!Track.OrientationProperty] = scrollBar[!ScrollBar.OrientationProperty],
[!Track.DeferThumbDragProperty] = scrollBar.TemplatedParent[!ScrollViewer.IsDeferredScrollingEnabledProperty],
Thumb = new Thumb
{
Template = new FuncControlTemplate<Thumb>(CreateThumbTemplate),

Loading…
Cancel
Save