How do I allow a UWP ListView to scroll past the l

2019-04-12 18:54发布

I have a ListView with a bunch of irregularly sized items. As you scroll the ListView, the bottom of the last item will end up at the bottom of the control: you can't keep scrolling.

If the last item is smaller than the control, I want the top of the last item to be able to scroll to the top of the control (if the item is larger than the control, I am fine with the default behavior; in this case, the top of the item will have scrolled past the top of the ListView control which satisfies my design). (This is similar to the behavior in something like Visual Studio Code with editor.scrollBeyondLastLine setting set to true)

I've almost got this working but it's not quite there. In my attempt, I subclassed ListView so I could override PrepareContainerForItemOverride.

I then attempt to add enough extra space to the item's bottom margin so the item's top will rest at the ListView's top.

  protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
    {
        base.PrepareContainerForItemOverride(element, item);

        var lvi = element as ListViewItem;
        if (lvi == null) { return; }

        var currentIndex = this.Items.IndexOf(item);
        if (currentIndex == (this.Items.Count - 1))
        {
            var sv = this.ScrollViewer();
            lvi.Measure(new Size(sv.ViewportWidth, double.PositiveInfinity));

            var slackSpace = sv.ViewportHeight - lvi.DesiredSize.Height;
            if (slackSpace > 0)
            {
                lvi.Margin = new Thickness(lvi.Margin.Left, lvi.Margin.Top, lvi.Margin.Right, slackSpace + lvi.Margin.Bottom);
            }                
        }
    }
}

(In this code, this.ScrollViewer() is just a little extension method that fishes out the ScrollViewer from any ListView. I'm using it here to try to pass something sensible to lvi.Measure.)

This doesn't quite work because the DesiredSize.Height of the ListViewItem ends up being a good bit larger than the Height of the rendered ListViewItem (I don't know why).

I could actually live with this behavior as a compromise if I knew that the DesiredSize.Height would always be larger than needed...but I bet that's not actually true.

So I clearly need to plug into a different part of the render pipeline to manage this. But I can't figure out where.

Should I be subclassing the ListView's ScrollViewer somehow (since I think the ScrollViewer is what is actually laying out the child ListViewItem controls?)? Is there some other way to do this? Or is it just impossible?

1条回答
劫难
2楼-- · 2019-04-12 19:16

Ideally it would be ScrollViewer's job to add extra spacing after it had arranged its child (panel in this case). Still it will have to look into panel's last child size to know exactly how much more space it should add.

But since ScrollViewer is sealed, I don't know if that's actually possible.

Another Hack, would be to create a custom StackPanel where we can adjust the measured height so that the last item will be scrolled on top.

The actual padding on bottom depends on the ScrollViewer's ViewportHeight hence the multiple passes through measure layout step.

public class BottomPaddedStackPanel: StackPanel
{
    private ScrollViewer scroll = null;

    public BottomPaddedStackPanel()
    {
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        if (scroll == null)
            InitScrollViewerData();

        Size panelSize = base.MeasureOverride(availableSize);
        var lastChild = this.Children[this.Children.Count - 1];

        if (scroll == null || scroll.ViewportHeight == 0)
            return panelSize;

        // add more space at the bottom to be able to scroll the last item at the top
        if (lastChild.DesiredSize.Height < scroll.ViewportHeight)
            panelSize.Height += scroll.ViewportHeight - lastChild.DesiredSize.Height;

        return panelSize;
    }

    private void InitScrollViewerData()
    {
        var lastChild = this.Children[this.Children.Count - 1];
        var lv = ItemsControl.ItemsControlFromItemContainer(lastChild);
        Border rootBorder = VisualTreeHelper.GetChild(lv, 0) as Border;
        scroll = rootBorder.Child as ScrollViewer;
        scroll.SizeChanged += (o, ev) => InvalidateMeasure();
    }
}

Set as the list's layout panel:

<ListView ItemsSource="{x:Bind Items}">
    <ListView.ItemsPanel>
        <ItemsPanelTemplate>
            <local:BottomPaddedStackPanel />
        </ItemsPanelTemplate>
    </ListView.ItemsPanel>
</ListView>
查看更多
登录 后发表回答