Where did all my gestures go?

A common problem we’ve seen from customers is how to handle pointer input and manipulations on elements inside of collection controls such as ListViews, GridViews, and FlipViews. The app can get the PointerPressed event on items inside the View, but after that the pointer events disappear and the app cannot convert them into gestures or manipulations.

So what is going on here?

The common element is that these controls all host a ScrollViewer, and the ScrollViewer takes over the pointer handling to run its scrolling. Once the ScrollViewer is in charge it handles all of the pointer and manipulation events without the app having its own chance at them.

This means that if an app needs to handle pointer events inside a scrolling control then it needs to disable the ScrollViewer (more on that below).

Why does the ScrollViewer take over the pointer handling?

Efficiency and responsiveness. Xaml applications compose and render their graphics on separate threads. The rendering thread is in charge of keeping the screen updating at a fluid pace. The composition thread is where the app’s UI code runs and which can update the layout of the controls. Whenever the layout isn’t updating the render thread can keep the visual display crisp and quick even while other work is being done. Non-layout updates such as independent animations and rendering transforms can be done on the rendering thread without slowing down to synchronize the two threads.

Because touch manipulations need to be very responsive, Xaml apps tap into a Windows feature known as Direct Manipulation to handle input at a low level on the render thread. Direct Manipulation detects touch input such as scrolling, panning, and scaling. The ScrollViewer uses it to scroll swiftly and cleanly, and the app can listen to Direct Manipulation via the Xaml Manipulation events to apply scaling, translation, and rotation render transforms responsively in sync with the render thread.

That is where the missing pointer events go. When the ScrollViewer handles the PointerPressed event the Direct Manipulation engine takes over the pointer handling and the app doesn’t receive any more pointer events until the current manipulation has ended.

So what can an app do if it needs the pointer messages?

The easiest way is to restrict the needed pointer messages to a sub-item which blocks the scrolling and set it to handle its own manipulations. For example, an Image in a FlipView’s ItemTemplate with ManipulationMode=”All” will not trigger the ScrollViewer for touches inside the image. The Image will receive both Xaml manipulation events and pointer events which can be passed to a GestureRecognizer.

If the app cannot scope the pointer messages to a sub-item then it is trickier. The next best is to restrict the messages to a sub-region or usage mode. In that case the PointerPressed handler can detect if it is in the sub-region or non-scrolling mode, and if so it can walk the visual tree to find the ScrollViewer and disable the ScrollViewer’s invocation of Direct Manipulation. To do this, change the ScrollViewer’s HorizontalScrollMode and VerticalScrollMode to Disabled. Likely you’ll only need to change one, depending on if you have a horizontally scrolling GridView or FlipView, or a vertically scrolling ListView:

 

ScrollViewer FindParentScrollViewer(DependencyObject start)
{
    DependencyObject item = start;
    DependencyObject parent = null;
    do
    {
        parent = (DependencyObject)VisualTreeHelper.GetParent(item);
        if (parent is ScrollViewer)
        {
            return parent as ScrollViewer;
        }
        item = parent;
    } while (item != null);

    return null;
}

 

bool InDisableScrollViewerRegion(PointerPoint pt)
{
    // For demonstration purposes, scroll at the top and block the rest
    return pt.Position.Y > 400;
}

private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
    DependencyObject depObj = (DependencyObject)e.OriginalSource;
  
    if (InDisableScrollViewerRegion(e.GetCurrentPoint(this)))
    {
        DisableScrolling((DependencyObject)e.OriginalSource);
    }
   
    UIElement target = sender as UIElement;

    PointerPoint point = e.GetCurrentPoint(itemFlipView);
    gestureRecognizer.ProcessDownEvent(point);
    target.CapturePointer(e.Pointer);
    e.Handled = true;
}

ScrollMode _originalScrollMode;
bool _scrollingDisabled = false;
private void DisableScrolling(DependencyObject depObj)
{
       ScrollViewer parentElem;
       parentElem = FindParentScrollViewer(depObj);
       if (parentElem != null)
      {
            _originalScrollMode = parentElem.HorizontalScrollMode;
            _scrollingDisabled = true;
            parentElem.HorizontalScrollMode = ScrollMode.Disabled;
     }
}

private void RestoreScrolling(DependencyObject depObj)
{
     if (_scrollingDisabled)
     {
            _scrollingDisabled = false;
            ScrollViewer parentElem = FindParentScrollViewer(depObj);
            if (parentElem != null)
            {
                    parentElem.HorizontalScrollMode = _originalScrollMode;
            }
     }
}

 

Unfortunately there is no good solution if the app needs both scrolling and gestures (for example, to detect CrossSlides against the scrolling). In this case the only option to get the Pointer messages everywhere is to disable Direct Manipulation everywhere, but that disables scrolling as well. To get that back the app will need to detect the scrolling gestures itself and then navigate the ScrollViewer to the new location with ScrollToHorizontalOffset or ScrollToVerticalOffset or by updating the SelectedIndex. This is tricky and will be noticeably slower than letting the ScrollViewer do its thing. It should be avoided if at all possible.

 

--Rob

Follow us on Twitter @wsdevsol!