Patrick
Patrick

Reputation: 167

ListView with Groove like quick return header

When scrolling down, Groove moves the header up, outside of the viewable area just like a regular ListView header. When scrolling back up it moves the header back down into the viewable area right away, regardless of the current vertical scroll offset. The header seems to be part of the ListView content because the scrollbar includes the header.

How can this be implemented in a Windows 10 UWP app?

Quick return header demo

Upvotes: 3

Views: 215

Answers (2)

Patrick
Patrick

Reputation: 167

After looking around a bit and experimentation I can now answer my own question.

One can use an expression based composition animation to adjust the Y offset of the the header in relation to scrolling. The idea is based on this answer. I prepared a complete working example on GitHub.

The animation is prepared in the SizeChanged event of the ListView:

ScrollViewer scrollViewer = null;
private double previousVerticalScrollOffset = 0.0;
private CompositionPropertySet scrollProperties;
private CompositionPropertySet animationProperties;

SizeChanged += (sender, args) =>
{
    if (scrollProperties == null)
        scrollProperties = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scrollViewer);

    var compositor = scrollProperties.Compositor;

    if (animationProperties == null)
    {
        animationProperties = compositor.CreatePropertySet();
        animationProperties.InsertScalar("OffsetY", 0.0f);
    }

    var expressionAnimation = compositor.CreateExpressionAnimation("animationProperties.OffsetY - ScrollingProperties.Translation.Y");

    expressionAnimation.SetReferenceParameter("ScrollingProperties", scrollProperties);
    expressionAnimation.SetReferenceParameter("animationProperties", animationProperties);

    var headerVisual = ElementCompositionPreview.GetElementVisual((UIElement)Header);
    headerVisual.StartAnimation("Offset.Y", expressionAnimation);
};

The OffsetY variable in the animationProperties will drive the animation of the OffsetY property of the header. The OffsetY variable is updated in the ViewChanged event of the ScrollViewer:

scrollViewer.ViewChanged += (sender, args) =>
{
    float oldOffsetY = 0.0f;
    animationProperties.TryGetScalar("OffsetY", out oldOffsetY);

    var delta = scrollViewer.VerticalOffset - previousVerticalScrollOffset;
    previousVerticalScrollOffset = scrollViewer.VerticalOffset;

    var newOffsetY = oldOffsetY - (float)delta;

    // Keep values within negativ header size and 0
    FrameworkElement header = (FrameworkElement)Header;
    newOffsetY = Math.Max((float)-header.ActualHeight, newOffsetY);
    newOffsetY = Math.Min(0, newOffsetY);

    if (oldOffsetY != newOffsetY)
        animationProperties.InsertScalar("OffsetY", newOffsetY);
};

While this does animate correctly, the header is not stacked on top of the ListView items. Therefore the final piece to the puzzle is to decrease the ZIndex of the ItemsPanelTemplate of the ListView:

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <ItemsStackPanel Canvas.ZIndex="-1" />
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

Which gives this as a result:

Quick return header demo

Upvotes: 0

Kai Brummund
Kai Brummund

Reputation: 3568

You can do this by utilizing the ListView's internal ScrollViewer's ViewChanged event.

First you got to obtain the internal ScrollViewer. This is the simplest version, but you might want to use one of the many VisualTreeHelper Extensions around to do it safer and easier:

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    var border = VisualTreeHelper.GetChild(MyListView, 0);
    var scrollviewer = VisualTreeHelper.GetChild(border, 0) as ScrollViewer;

    scrollviewer.ViewChanged += Scrollviewer_ViewChanged;
}

In the EventHandler, you can then change the visibility of your header depending on the scroll direction.

private void Scrollviewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    var sv = sender as ScrollViewer;

    if (sv.VerticalOffset > _lastVerticalOffset)
    {
        MyHeader.Visibility = Visibility.Collapsed;
    }
    else
    {
        MyHeader.Visibility = Visibility.Visible;
    }
}

This is the basic idea. You might wan't to add some smooth animations instead of just changing the visibility.

Upvotes: 1

Related Questions