Browse Source

Redo looping selectors to properly scroll

pull/4108/head
amwx 6 years ago
parent
commit
c1315f0458
  1. 139
      src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs
  2. 93
      src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs

139
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;

93
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
/// </summary>
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<LoopingSelectorItem> panelItems = new List<LoopingSelectorItem>();
@ -462,7 +477,7 @@ namespace Avalonia.Controls.Primitives
}
}
if (ItemCount > 0)
if (setContent && ItemCount > 0)
SetItemContent();
}
@ -519,6 +534,44 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Handles recycling of containers
/// </summary>
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
/// </summary>
/// <param name="offsetY"></param>
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;

Loading…
Cancel
Save