Bläddra i källkod

Deferred scrolling (#13644)

* Implemented deferred scrolling

* Set IsDeferredScrollingEnabled in themes, where relevant
Tom Edwards 1 år sedan
förälder
incheckning
d62ff00f85

+ 3 - 0
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 - 0
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 - 0
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 - 0
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)),

+ 58 - 9
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));
@@ -291,6 +306,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)
@@ -315,7 +337,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;
 
@@ -348,7 +370,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;
@@ -407,7 +429,7 @@ namespace Avalonia.Controls.Primitives
             if (oldThumb != null)
             {
                 oldThumb.DragDelta -= ThumbDragged;
-
+                oldThumb.DragCompleted -= ThumbDragCompleted;
                 LogicalChildren.Remove(oldThumb);
                 VisualChildren.Remove(oldThumb);
             }
@@ -415,6 +437,7 @@ namespace Avalonia.Controls.Primitives
             if (newThumb != null)
             {
                 newThumb.DragDelta += ThumbDragged;
+                newThumb.DragCompleted += ThumbDragCompleted;
                 LogicalChildren.Add(newThumb);
                 VisualChildren.Add(newThumb);
             }
@@ -443,17 +466,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 - 0
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)
         {

+ 2 - 1
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 - 0
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 - 0
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 - 0
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"

+ 2 - 1
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 - 0
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 - 0
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 - 0
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 - 0
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),