2
votes

Universal Windows 8.1 Store project here.

I want to know, when a ListView stops scrolling after user interaction. I found plenty of information on the net, but not one example reliably working on WP 8.1 (WPF/WP8 examples do not help much, and there are loads of them).

Here's what I do now.

1. The ListView

<ListView  
    x:Name="MessageList"
    ItemsSource="{Binding Messages}"  
    VerticalAlignment="Bottom"                
    ItemContainerStyle="{StaticResource ChatListViewItemStyle}" 
    PointerEntered="MessageList_OnPointerEntered"
    >
    <ListView.ItemTemplate>
        <DataTemplate>
            <messages:MessageContainer />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

2. The ScrollViewer

I get a ScrollViewer reference from the ListView in code behind.

// GetChildElement<T>(this DependencyObject root) is a simple extension method of mine
Scroll = MessageList.GetChildElement<ScrollViewer>();

3. ListViewer.PointerEntered and ScrollViewer.ViewChanged

PointerEntered handler is used to detect the start of user interaction. When an interaction is detected, I subscribe to Scroll.ViewChanged and use IsIntermediate flag of the event to detect when the list stops scrolling (including inertia).

void MessageList_OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
    Debug.WriteLine("START MONITORING INTERACTION");
    Scroll.ViewChanged += OnViewChangedByUser;
}

void OnViewChangedByUser(object sender, ScrollViewerViewChangedEventArgs e)
{
  Debug.WriteLine("WAITING FOR INTERACTION TO END");
  if (!e.IsIntermediate) {
    Debug.WriteLine("INTERACTION ENDED");
    Scroll.ViewChanged -= OnViewChangedByUser;
  }
}

This does work to some extent.

The problem

The problem is, ViewChanged is not fired when the list is scrolled to the end/start and the user pulls it out of bounds and releases it, causing it to return back with inertia. So, the interaction start is detected, but the end is not. ViewChanged is not fired at all -- neither with IsIntermediate=True, nor with False.

ViewChanged event is not fired at all

What is a better way of doing what I want?

2

2 Answers

2
votes

Sadly there's no good way to do this on Windows 8.1 aside from repeated polling and checking the ScrollOffset.

I'd just get an array of 10 doubles, and like 10 times a second I'd shift-in the current scroll offset. Than in that same handler check if the last 5 equals to the end of your list than raise an event.

1
votes

As Tamás Deme puts it, there's no nice way of doing what is required. However, I've found a workaround that works in my case (nothing nice about it though).

In fact, I'm detecting, whether the list is scrolled to the bottom, when the scrolling stops. It's detecting the end of scrolling is what is causing so much trouble.

There are two parts of the problem: 1 - detecting the end of user interaction, 2 - detecting the end of inertia. Suprisingly, there's no good way of solving either of them. Thankfully, what I actually need is just knowing the value of VerticalOffset when scrolling (user-driven or inertia-animated) ceases. I don't actually have to know whether the user is still holding the list or not.

void MessageList_OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
    IsScrolledToLastLine = false; // this is to signal, that the user is 
                                  // holding the list, and there must be no 
                                  // automatic scrolling, when content is 
                                  // added to it.
    Debug.WriteLine("[*]START MONITORING INTERACTION");
    Scroll.ViewChanged += OnViewChangedByUser;
    Scroll.LayoutUpdated += OnScrollLayoutUpdated;
}

void OnScrollLayoutUpdated(object sender, object e)
{
  // will trigger multiple times during scrolling 
  // AND  
  // will trigger when inertia finally stops 
  // (regardless of the changes of VerticalOffset)
  IsScrolledToLastLine = Scroll.ScrollableHeight == Scroll.VerticalOffset;
  Debug.WriteLine("Interaction progress: {0}", IsScrolledToLastLine);
}

void OnViewChangedByUser(object sender, ScrollViewerViewChangedEventArgs e)
{
  if (!e.IsIntermediate) {
    IsScrolledToLastLine = Scroll.ScrollableHeight == Scroll.VerticalOffset;
    Debug.WriteLine("Interaction end: {0}", IsScrolledToLastLine);
    Scroll.LayoutUpdated -= OnScrollLayoutUpdated;
    Scroll.ViewChanged -= OnViewChangedByUser;
  }
}

Scroll.LayoutUpdated

LayoutUpdated is fired multiple times during scrolling. Unlike ViewChanged this event is also fired when inertia stops in the situation shown in the picture of the post. Unfortunatelly, there is no way to determine in LayoutUpdated, whether the list stopped scrolling completely or not.

ViewChanged works fine when you actually change VerticalOffset by scrolling; LayoutUpdated covers the over-scrolling situation.

There is another problem though: OnScrollLayoutUpdated may remain subscribed when scrolling over the edges of the list, as ViewChanged will not trigger. Fortunately, I can just ignore that, this doesn't break anything.