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!

Comments (6)

  1. Kris says:

    Hi Rob,

    Very helpful post. My question is how does the Gridview handle this? We can both scroll the Gridview using swipe gesture and select items using Cross-slide gestures?

  2. Deani Hansen (Blendrocks) says:

    Hi Rob, thanks for the blogpost. Nice explanation on exactly why my PoC isn't working. Thanks a lot. /Deani

  3. Leo Lin says:

    This should be fixed soon. If we want to allow hamburger layouts, one of the must haves are to slide to open drawer gesture. If my main content is a listview, then sliding to open won't work.

  4. Ravi says:

    Indeed very helpful Rob. So I'm "stuck between a rock and a hard place". I currently need to detect the direction of the gesture done on a GridView, and first tried with MSManipulationStateChanged, however unlike what the documentation says regarding inertiaDestinationX/inertiaDestinationY being part of the event, we never get those values.

    social.msdn.microsoft.com/…/w81htmlinertiadestinationy-inertiadestinationx-never-sent-as-part-of-the-msmanipulationevent

    GR now has this issue, is there a way to latch on to some hook to get this information?

  5. Kay61 says:

    We're now in 2016! Responding to Leo Lin, I think this has been fixed partially. My universal Windows 10 app has a ScrollViewer in one page and a ListView in another page. This issue is fixed on Windows 10 PC but NOT Windows 10 Mobile. My main intention is to implement sliding on the hamburger menu on the mobile version but it works the other way. Really very frustrating to not get it to work on the mobile version. Sorry Rob, its really quite stupid that the hamburger menu doesn't provide built-in sliding from left support. No choice but I am willing to implement it manually but the stupid ScrollViewer and ListView on the mobile version really sucks! What exactly is the universal platform when it works on one but not the other. I suspect internally there is two implementation, one for PC and another for mobile, right? Many apologies Rob for the quite blunt language. It came out of frustration at not getting it to work!

  6. Mike G says:

    Hi Rob, thanks for the post. I'm also falling into the final "unfortunate" category where I have vertically scrolling content, but need to detect horizontal swipes to switch content. Are there any good examples of how to approach implementing the workaround?

Skip to main content