diff --git a/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs b/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs index 0709df7cf1..4282e1842b 100644 --- a/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs @@ -103,40 +103,56 @@ namespace Avalonia.Controls.Primitives var children = Children; var offY = Offset.Y; - //Item arranging: SelectedItem is placed in the middle of the viewport - //There are _totalItemsInViewport above & below - //In default behavior of Date/Time Picker, 9 items are visible in viewport, - //so 19 items are arrange, 9 above, 9 below, and the selected item inbetween - //The exception is if we are NOT looping & we're near the ends of the available - //items, then we only have what's left before/after selecteditem visible - if (_owner.ShouldLoop) + //When not looping, currentSet will always be 0 + //When looping, we measure for 10x _owner.ItemCount, so we need to figure out + //which "set" of items we're in based on the offset so we know where to properly + //place items + var singleExtent = _owner.ItemCount * itemHgt; + var currentSet = Math.Truncate(offY / singleExtent); + + int selIndex = _owner.SelectedIndex; + selIndex = selIndex == -1 ? 0 : selIndex; + + //When looping, the selected item should always be the middle item, equivalent in index + //to _totalItemsInViewport + //When not looping, if we're near the beginning of the list, our first item may be less than + //_totalItemsInViewport, so we need to make sure in that case to make the selected item, the + //actual selected index + var childIndexOfSelected = _totalItemsInViewport; + if (!_owner.ShouldLoop && selIndex < _totalItemsInViewport) + childIndexOfSelected = selIndex; + + //Our selected container forms our "anchor" and all other containers are placed around this + IControl containerOfSelected = children[childIndexOfSelected]; + + //Then we need to know how many containers are above and below the selected item + //Should be _totalItemsInViewport for both, unless not looping & near items start + //# containers above is just the index of the selected item container + var numContainersAboveSelected = childIndexOfSelected; + + //Move initY to where we actually want to start placing the items + initY += (singleExtent * currentSet) + (selIndex * itemHgt); + + //We first arrange the selected item + Rect rc = new Rect(0, initY - offY, itemWid, itemHgt); + containerOfSelected.Arrange(rc); + + //Arrange all items above + var prevY = initY - itemHgt; + for (int i = numContainersAboveSelected - 1; i >= 0; i--) { - //Correct our starting Y for not starting in the middle when looping - initY -= (itemHgt * _totalItemsInViewport); - - //Sort of limitation when looping, we just place the containers & - //swap the content. With logical scrolling enabled, and the way its - //handled in Avalonia currently (both pointer wheel & gestures) - //you won't notice a difference. Though this may need updating later - //if this changes - Rect rc; - for (int i = 0; i < children.Count; i++) - { - rc = new Rect(0, initY, itemWid, itemHgt); - children[i].Arrange(rc); - initY += itemHgt; - } + rc = new Rect(0, prevY - offY, itemWid, itemHgt); + children[i].Arrange(rc); + prevY -= itemHgt; } - else + + //Finally arrange all items below + var nextY = initY + itemHgt; + for (int i = childIndexOfSelected + 1; i < children.Count; i++) { - int firstIndex = Math.Max(0, _owner.SelectedIndex - _totalItemsInViewport); - Rect rc; - for (int i = 0; i < children.Count; i++) - { - rc = new Rect(0, initY - offY + firstIndex * itemHgt, itemWid, itemHgt); - children[i].Arrange(rc); - initY += itemHgt; - } + rc = new Rect(0, nextY - offY, itemWid, itemHgt); + children[i].Arrange(rc); + nextY += itemHgt; } return new Size(itemWid, _extent.Height); @@ -160,25 +176,20 @@ namespace Avalonia.Controls.Primitives get => _offset; set { - //If we try setting the offset before we've intialized - //store the value now & set it when MeasureOverride is called if (Extent.Height == 0) { initOffset = value.Y; return; } - var old = _offset; + var old = _offset.Y; _offset = value; if (Children.Count == 0) return; - _owner.SetSelectedIndexFromOffset(value.Y); - var itemHgt = _owner.ItemHeight; var totalItemCount = _owner.ItemCount; - var initY = (Bounds.Height / 2) - (itemHgt / 2); if (_owner.ShouldLoop) { @@ -187,62 +198,28 @@ namespace Avalonia.Controls.Primitives //to make sure we always have scrolling //To do this, since we plan for 10x total items, we move if we're //in the first or last "block" of items & return it to somewhere near the middle - if (value.Y > old.Y) //Scrolling Down + if (value.Y > old) //Scrolling Down { var extentOne = totalItemCount * itemHgt; var scrollableHeight = (_extent.Height - _viewport.Height); if (value.Y >= scrollableHeight - extentOne) + { _offset = new Vector(0, value.Y - (extentOne * 5)); + old = old - (extentOne * 5); + } } - else if (value.Y < old.Y) //Scrolling Up + else if (value.Y < old) //Scrolling Up { var extentOne = totalItemCount * itemHgt; - var scrollableHeight = (_extent.Height - _viewport.Height); if (value.Y < extentOne) + { _offset = new Vector(0, value.Y + (extentOne * 5)); + old = old + (extentOne * 5); + } } - - firstIndex = _owner.SelectedIndex - _totalItemsInViewport; } - else - { - - var numItemsAboveSelected = (int)Math.Ceiling(initY / itemHgt); - int logicalOffset = (int)(_offset.Y / itemHgt); - firstIndex = Math.Max(0, Math.Min(logicalOffset - numItemsAboveSelected, totalItemCount)); - - //When not looping, we actually move the containers, so if we get one - //out of bounds, recycle it to the other side - if (_offset.Y > old.Y) - { - var recycleThreshold = initY - (_totalItemsInViewport * itemHgt); - - //ScrollDown - var ct = Children.Count; - for (int i = ct - 1; i >= 0; i--) - { - if (Children[i].Bounds.Bottom <= recycleThreshold) - { - Children.Move(i, ct - 1); - } - } - } - else if (_offset.Y < old.Y) - { - var recycleThreshold = (initY + itemHgt) + (_totalItemsInViewport * itemHgt); - //ScrollUp - var ct = Children.Count; - var bottom = Bounds.Height; - for (int i = ct - 1; i >= 0; i--) - { - if (Children[i].Bounds.Top >= recycleThreshold) - { - Children.Move(i, 0); - } - } - } - } + _owner.SetSelectedIndexFromOffset(old, _offset.Y); RaiseScrollInvalidated(EventArgs.Empty); InvalidateArrange(); @@ -268,8 +245,8 @@ namespace Avalonia.Controls.Primitives ScrollInvalidated?.Invoke(this, e); } + private double initOffset = double.NaN; - int firstIndex = 0; private LoopingSelector _owner; private Size _extent; private Size _viewport; diff --git a/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs b/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs index e2fb5a36b3..5baaead11f 100644 --- a/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs +++ b/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs @@ -184,6 +184,12 @@ namespace Avalonia.Controls.Primitives set { SetAndRaise(ItemHeightProperty, ref _itemHeight, value); + + _totalItemsInViewport = (int)Math.Ceiling(Bounds.Height / (value == 0 ? 1 : value)); + if (_totalItemsInViewport % 2 == 0) + _totalItemsInViewport += 1; + + UpdateOffset(); } } @@ -215,7 +221,7 @@ namespace Avalonia.Controls.Primitives protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if(_scroller != null) + if (_scroller != null) { _scroller.Content = null; } @@ -301,9 +307,15 @@ namespace Avalonia.Controls.Primitives { var selIndex = SelectedIndex; if (selIndex == ItemCount - 1) - SelectedIndex = 0; + { + if (ShouldLoop) + SelectedIndex = 0; + } else + { SelectedIndex++; + } + e.Handled = true; } @@ -311,9 +323,15 @@ namespace Avalonia.Controls.Primitives { var selIndex = SelectedIndex; if (selIndex == 0) - SelectedIndex = ItemCount - 1; + { + if (ShouldLoop) + SelectedIndex = ItemCount - 1; + } else + { SelectedIndex--; + } + e.Handled = true; } @@ -352,10 +370,7 @@ namespace Avalonia.Controls.Primitives case NotifyCollectionChangedAction.Add: ItemCount += e.NewItems.Count; var index = e.NewStartingIndex; - //if (IsContainerIndexLoaded(index)) - //{ - // AddContainers(index, e.NewItems); - //} + EnsureContainers(); UpdateOffset(); break; @@ -383,7 +398,7 @@ namespace Avalonia.Controls.Primitives /// Ensures we have the correct number of containers in the LoopingSelectorPanel /// This will add, remove, or clear the panel as necessary /// - private void EnsureContainers() + private void EnsureContainers(bool setContent = true) { if (Bounds.Height == 0) return; @@ -391,7 +406,7 @@ namespace Avalonia.Controls.Primitives int itemCount = ItemCount; //How many containers we ideally want int desiredItemsLoaded = (_totalItemsInViewport * 2) + 1; - + var realizedContainerCount = _panel.Children.Count; if (ShouldLoop) @@ -414,7 +429,7 @@ namespace Avalonia.Controls.Primitives else if (realizedContainerCount > desiredItemsLoaded) //Remove extra containers { //Technically not needed now as we don't move the containers when looping, just - //swap content, but is here in case of future improvements + //swap content, but may be called if resized _panel.Children.RemoveRange(realizedContainerCount - delta, delta); } } @@ -441,7 +456,7 @@ namespace Avalonia.Controls.Primitives //Do we need containers? var numContsToAddRemove = neededContainers - currentCount; - + if (numContsToAddRemove > 0) //Add Containers { List panelItems = new List(); @@ -462,7 +477,7 @@ namespace Avalonia.Controls.Primitives } } - if (ItemCount > 0) + if (setContent && ItemCount > 0) SetItemContent(); } @@ -519,6 +534,44 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Handles recycling of containers + /// + private void RecycleContainersIfNecessaryOnScroll(double newOffset, double oldOffset) + { + var children = _panel.Children; + var initY = (Bounds.Height / 2.0) - (ItemHeight / 2.0); + var recThresTop = initY - (_totalItemsInViewport * ItemHeight); + var recThresBot = initY + (_totalItemsInViewport * ItemHeight); + var scrollChange = newOffset - oldOffset; + + var numContsAbove = children.Where(x => (x.Bounds.Bottom - scrollChange) <= recThresTop).Count(); + var numContsBelow = children.Where(x => (x.Bounds.Top - scrollChange) >= recThresBot).Count(); + + if (numContsAbove > 0) + { + var recycleCount = numContsAbove; + var lastItemContent = (children[children.Count - 1] as LoopingSelectorItem).Content; + var index = Items.IndexOf(lastItemContent); + _panel.Children.MoveRange(0, recycleCount, children.Count); + } + else if (numContsBelow > 0) + { + var recycleCount = numContsBelow; + var firstItemContent = (children[0] as LoopingSelectorItem).Content; + var index = Items.IndexOf(firstItemContent); + var paneItemCount = _panel.Children.Count; + + _panel.Children.MoveRange(paneItemCount - recycleCount, recycleCount, 0); + } + + //Probably not ideal to re-set every item's content, but trying to set + //the content of just the recycled items was doing weird things + //We have a small number of containers loaded, so this shouldn't have + //too big of impact + SetItemContent(); + } + private object GetElementAt(int index) { if (index < 0 || index >= ItemCount) @@ -540,6 +593,7 @@ namespace Avalonia.Controls.Primitives _preventUpdateSelection = true; + var oldOffY = _panel.Offset.Y; if (ShouldLoop) { //We measure for 10x as many items, so when we set the SelectedIndex @@ -549,7 +603,6 @@ namespace Avalonia.Controls.Primitives selIndex = selIndex == -1 ? 0 : selIndex; var extent = ItemCount * ItemHeight; _panel.Offset = new Vector(0, (selIndex * ItemHeight) + (extent * 5)); - } else { @@ -560,9 +613,11 @@ namespace Avalonia.Controls.Primitives _panel.Offset = new Vector(0, 0); else _panel.Offset = new Vector(0, selIndex * ItemHeight); + + EnsureContainers(false); } - EnsureContainers(); + RecycleContainersIfNecessaryOnScroll(_panel.Offset.Y, oldOffY); _preventUpdateSelection = false; } @@ -571,7 +626,7 @@ namespace Avalonia.Controls.Primitives /// Updates the SelectedIndex when scrolling occurs /// /// - internal void SetSelectedIndexFromOffset(double offsetY) + internal void SetSelectedIndexFromOffset(double oldOffsetY, double offsetY) { if (_preventUpdateSelection) return; @@ -586,14 +641,15 @@ namespace Avalonia.Controls.Primitives var pixelOffset = offsetY - extent * numExtents; SelectedIndex = (int)(pixelOffset / ItemHeight); + RecycleContainersIfNecessaryOnScroll(offsetY, oldOffsetY); } else { SelectedIndex = (int)(offsetY / ItemHeight); + EnsureContainers(false); + RecycleContainersIfNecessaryOnScroll(offsetY, oldOffsetY); } - EnsureContainers(); - _preventMovingScrollWhenSelecting = false; } @@ -612,7 +668,8 @@ namespace Avalonia.Controls.Primitives { //Ideally we always want this to be odd, since the selected item is placed in the middle, //so we have the same number of items above and below at all times - _totalItemsInViewport = (int)Math.Ceiling(x.Height / ItemHeight); + var itmHgt = ItemHeight; + _totalItemsInViewport = (int)Math.Ceiling(x.Height / (itmHgt == 0 ? 1 : itmHgt)); if (_totalItemsInViewport % 2 == 0) _totalItemsInViewport += 1;