Wednesday, December 5, 2012

Ignore single-tap before double-tap on Windows 8 apps


The new development platform for Windows 8 has been released too early. The number of dirty tricks I had to resort to in just a few months working with it made it pretty clear to me.

One of the most infamous bugs I found affects all the versions of the platform (x86, x64 and ARM) and it's an erroneous sequence of events that is propagated after a double-tap. I found it propagating from a ScrollViewer control, but I'm pretty sure other controls may behave this way as well.

When a user double-tapped on the screen I was expecting a single call to my double-tap event handler to be made, but with a bit of sadness a single-tap event was called immediately before it... with no fucking reason, if you ask me.
My code needs to be certain about which one of the two is called, because I need to react in two different, alternative ways to them. So I was stuck, trying to find a clever way to figure out when a single tap erroneously fired by the framework. And obviously when it's not.

With the help of my collegues we found a really simplicistic way of dealing with it, but it's actually a trick that mitigates the issue, not a definitive cure.

Here it is:

// Here's the part I hate the most: heuristics...
private const int DOUBLETAP_DELAY_MILLIS = 190;

private DateTime singleTapTime;
private bool singleTapCancelled;

private async void scrollViewer_Tapped(object sender,
                           TappedRoutedEventArgs args)
{
    // Save the moment the single tap
    // has been detected...
    this.singleTapTime = DateTime.Now;

    // ... but wait some time to be sure
    // that no double-tap event follows
    await Task.Delay(Viewer.DOUBLETAP_DELAY_MILLIS + 10);

    // A double-tap event will cancel our single- one 
    // before we reach this point...
    if (!this.singleTapCancelled)
    {
        // ...
    }

    this.singleTapCancelled = false;
}

private void scrollViewer_DoubleTapped(object sender,
                     DoubleTappedRoutedEventArgs args)
{
    TimeSpan tapsDelay =
        DateTime.Now - this.singleTapTime;

    // If we are between the configured timespan,
    // we cancel the previously fired single-tap event
    this.singleTapCancelled = (tapsDelay <= TimeSpan.FromMilliseconds(Viewer.DOUBLETAP_DELAY_MILLIS));

    // ...
}


An improvement would be protecting the boolean variable with a lock.
Also would be better to be tolerant on the number of milliseconds we wait. E.g. we could wait Viewer.DOUBLETAP_DELAY_MILLIS +/- some time.

But the only improvement should be done by Microsoft I guess...