瀏覽代碼

Correctly handle partially obscured items.

And move logical for selecting horizontal/vertical components of
extent/offset/viewport into `ItemVirtualizer` base class.
Steven Kirk 9 年之前
父節點
當前提交
7f09154020

+ 10 - 0
src/Avalonia.Controls/IVirtualizingPanel.cs

@@ -30,6 +30,16 @@ namespace Avalonia.Controls
         /// </summary>
         double AverageItemSize { get; }
 
+        /// <summary>
+        /// Gets or sets a size in pixels by which the content is overflowing the panel, in the
+        /// direction of scroll.
+        /// </summary>
+        /// <remarks>
+        /// This may be non-zero even when <see cref="OverflowCount"/> is zero if the last item
+        /// overflows the panel bounds.
+        /// </remarks>
+        double PixelOverflow { get; }
+
         /// <summary>
         /// Gets or sets the current pixel offset of the items in the direction of scroll.
         /// </summary>

+ 24 - 4
src/Avalonia.Controls/Presenters/ItemVirtualizer.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections;
 using System.Collections.Specialized;
 using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Utils;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Presenters
@@ -19,14 +20,32 @@ namespace Avalonia.Controls.Presenters
         public ItemsPresenter Owner { get; }
         public IVirtualizingPanel VirtualizingPanel => Owner.Panel as IVirtualizingPanel;
         public IEnumerable Items { get; private set; }
+        public int ItemCount { get; private set; }
         public int FirstIndex { get; set; }
-        public int LastIndex { get; set; } = -1;
+        public int NextIndex { get; set; }
+        public bool Vertical => VirtualizingPanel.ScrollDirection == Orientation.Vertical;
 
         public abstract bool IsLogicalScrollEnabled { get; }
-        public abstract Size Extent { get; }
-        public abstract Vector Offset { get; set; }
-        public abstract Size Viewport { get; }
+        public abstract double ExtentValue { get; }
+        public abstract double OffsetValue { get; set; }
+        public abstract double ViewportValue { get; }
 
+        public Size Extent => Vertical ? new Size(0, ExtentValue) : new Size(ExtentValue, 0);
+        public Size Viewport => Vertical ? new Size(0, ViewportValue) : new Size(ViewportValue, 0);
+
+        public Vector Offset
+        {
+            get
+            {
+                return Vertical ? new Vector(0, OffsetValue) : new Vector(OffsetValue, 0);
+            }
+
+            set
+            {
+                OffsetValue = Vertical ? value.Y : value.X;
+            }
+        }
+        
         public static ItemVirtualizer Create(ItemsPresenter owner)
         {
             var virtualizingPanel = owner.Panel as IVirtualizingPanel;
@@ -54,6 +73,7 @@ namespace Avalonia.Controls.Presenters
         public virtual void ItemsChanged(IEnumerable items, NotifyCollectionChangedEventArgs e)
         {
             Items = items;
+            ItemCount = items.Count();
         }
     }
 }

+ 3 - 3
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@@ -19,18 +19,18 @@ namespace Avalonia.Controls.Presenters
 
         public override bool IsLogicalScrollEnabled => false;
 
-        public override Size Extent
+        public override double ExtentValue
         {
             get { throw new NotSupportedException(); }
         }
 
-        public override Vector Offset
+        public override double OffsetValue
         {
             get { throw new NotSupportedException(); }
             set { throw new NotSupportedException(); }
         }
 
-        public override Size Viewport
+        public override double ViewportValue
         {
             get { throw new NotSupportedException(); }
         }

+ 40 - 45
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -19,64 +19,59 @@ namespace Avalonia.Controls.Presenters
 
         public override bool IsLogicalScrollEnabled => true;
 
-        public override Size Extent
-        {
-            get
-            {
-                if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
-                {
-                    return new Size(0, Items.Count());
-                }
-                else
-                {
-                    return new Size(Items.Count(), 0);
-                }
-            }
-        }
+        public override double ExtentValue => ItemCount;
 
-        public override Vector Offset
+        public override double OffsetValue
         {
             get
             {
-                if (VirtualizingPanel.ScrollDirection == Orientation.Vertical)
-                {
-                    return new Vector(0, FirstIndex);
-                }
-                else
-                {
-                    return new Vector(FirstIndex, 0);
-                }
+                var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
+                return FirstIndex + offset;
             }
 
             set
             {
-                var scroll = (VirtualizingPanel.ScrollDirection == Orientation.Vertical) ?
-                    value.Y : value.X;
-                var delta = (int)(scroll - FirstIndex);
+                var panel = VirtualizingPanel;
+                var offset = VirtualizingPanel.PixelOffset > 0 ? 1 : 0;
+                var delta = (int)(value - (FirstIndex + offset));
 
                 if (delta != 0)
                 {
-                    RecycleContainers(delta);
-                    FirstIndex += delta;
-                    LastIndex += delta;
+                    if ((NextIndex - 1) + delta < ItemCount)
+                    {
+                        if (panel.PixelOffset > 0)
+                        {
+                            panel.PixelOffset = 0;
+                            delta += 1;                           
+                        }
+
+                        if (delta != 0)
+                        {
+                            RecycleContainers(delta);
+                            FirstIndex += delta;
+                            NextIndex += delta;
+                        }
+                    }
+                    else
+                    {
+                        // We're moving to a partially obscured item at the end of the list.
+                        var firstIndex = ItemCount - panel.Children.Count;
+                        RecycleContainers(firstIndex - FirstIndex);
+                        NextIndex = ItemCount;
+                        FirstIndex = NextIndex - panel.Children.Count;
+                        panel.PixelOffset = VirtualizingPanel.PixelOverflow;
+                    }
                 }
             }
         }
 
-        public override Size Viewport
+        public override double ViewportValue
         {
             get
             {
-                var panel = VirtualizingPanel;
-
-                if (panel.ScrollDirection == Orientation.Vertical)
-                {
-                    return new Size(0, panel.Children.Count);
-                }
-                else
-                {
-                    return new Size(panel.Children.Count, 0);
-                }
+                // If we can't fit the last item in the panel fully, subtract 1 from the viewport.
+                var overflow = VirtualizingPanel.PixelOverflow > 0 ? 1 : 0;
+                return VirtualizingPanel.Children.Count - overflow;
             }
         }
 
@@ -96,8 +91,7 @@ namespace Avalonia.Controls.Presenters
                 // Reset indicates a large change and should (?) be quite rare.
                 VirtualizingPanel.Children.Clear();
                 Owner.ItemContainerGenerator.Clear();
-                FirstIndex = 0;
-                LastIndex = -1;
+                FirstIndex = NextIndex = 0;
                 CreateRemoveContainers();
             }
 
@@ -111,7 +105,7 @@ namespace Avalonia.Controls.Presenters
 
             if (!panel.IsFull && Items != null)
             {
-                var index = LastIndex + 1;
+                var index = NextIndex;
                 var items = Items.Cast<object>().Skip(index);
                 var memberSelector = Owner.MemberSelector;
 
@@ -126,7 +120,7 @@ namespace Avalonia.Controls.Presenters
                     }
                 }
 
-                LastIndex = index - 1;
+                NextIndex = index;
             }
 
             if (panel.OverflowCount > 0)
@@ -137,7 +131,7 @@ namespace Avalonia.Controls.Presenters
                 panel.Children.RemoveRange(index, count);
                 generator.Dematerialize(FirstIndex + index, count);
 
-                LastIndex -= count;
+                NextIndex -= count;
             }
         }
 
@@ -156,6 +150,7 @@ namespace Avalonia.Controls.Presenters
             {
                 var oldItemIndex = FirstIndex + first + i;
                 var newItemIndex = oldItemIndex + delta + ((panel.Children.Count - count) * sign);
+
                 var item = Items.ElementAt(newItemIndex);
 
                 if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))

+ 10 - 0
src/Avalonia.Controls/VirtualizingStackPanel.cs

@@ -30,6 +30,16 @@ namespace Avalonia.Controls
 
         double IVirtualizingPanel.AverageItemSize => _averageItemSize;
 
+        double IVirtualizingPanel.PixelOverflow
+        {
+            get
+            {
+                var bounds = Orientation == Orientation.Horizontal ? 
+                    Bounds.Width : Bounds.Height;
+                return Math.Max(0, (_takenSpace - _pixelOffset) - bounds);
+            }
+        }
+
         double IVirtualizingPanel.PixelOffset
         {
             get { return _pixelOffset; }

+ 15 - 6
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@@ -213,23 +213,32 @@ namespace Avalonia.Controls.UnitTests.Presenters
             [Fact]
             public void Moving_To_And_From_The_End_With_Partial_Item_Should_Set_Panel_PixelOffset()
             {
-                var target = CreateTarget();
+                var target = CreateTarget(itemCount: 20);
 
                 target.ApplyTemplate();
                 target.Measure(new Size(100, 95));
                 target.Arrange(new Rect(0, 0, 100, 95));
 
-                ((ILogicalScrollable)target).Offset = new Vector(0, 91);
+                ((ILogicalScrollable)target).Offset = new Vector(0, 11);
 
                 var minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
-                Assert.Equal(90, minIndex);
-                Assert.Equal(6, ((IVirtualizingPanel)target.Panel).PixelOffset);
+                Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset);
+                Assert.Equal(10, minIndex);
+                Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset);
 
-                ((ILogicalScrollable)target).Offset = new Vector(0, 90);
+                ((ILogicalScrollable)target).Offset = new Vector(0, 10);
 
                 minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
-                Assert.Equal(90, minIndex);
+                Assert.Equal(new Vector(0, 10), ((ILogicalScrollable)target).Offset);
+                Assert.Equal(10, minIndex);
                 Assert.Equal(0, ((IVirtualizingPanel)target.Panel).PixelOffset);
+
+                ((ILogicalScrollable)target).Offset = new Vector(0, 11);
+
+                minIndex = target.ItemContainerGenerator.Containers.Min(x => x.Index);
+                Assert.Equal(new Vector(0, 11), ((ILogicalScrollable)target).Offset);
+                Assert.Equal(10, minIndex);
+                Assert.Equal(5, ((IVirtualizingPanel)target.Panel).PixelOffset);
             }
 
             public class WithContainers