Windows Phone 7: how to reset the idle detection countdown

Windows Phone 7 like every other phone OS turns off the screen after a period of inactivity. This is not a problem most of the time because any user activity (namely finger interactions on the screen) resets the countdown, so if you are using an application the screen saver will not get in the way. However there are some particular cases where it is useful to disable the idle detection, for example in games or apps that require long reading (or watching). In that case you can completely disable idle detection:

PhoneApplicationService.Current.UserIdleDetectionMode =
    IdleDetectionMode.Disabled;

Keep in mind that this disables the “screen saver” at all, so be careful because you could drain the poor user’s battery if you do it without valid reason.

There is another more interesting case, though: suppose your app uses the accelerometer as its main user input. In this case there won’t be any user activity to trigger a countdown reset, but disabling it at all also doesn’t look like the best idea (what if the user puts the phone on the table to go grab a beer?).

The best in this case would be to reset the count-down when a movement is detected, i.e. treating accelerometer events like screen user input. How to do this?

The answer is extremely simple: you can just disable the IdleDetection and re-enable it again. This will reset the count-down. One little caveat: you cannot re-enable it immediately after having disabled it, the OS is smart enough not to be fooled and will ignore your two commands. You’ll have to wait a short while before re-enabling idle detection.

Here is an example: when I get a new reading from the accelerometer (I’m using the AccelerometerHelper) I check if there has been a large enough movement and in that case I disable the idle detection. Otherwise I enable it –this effectively resets the countdown every time the movement goes above a given threshold. Keep in mind that the accelerometer fires 50 times per second, that’s why I used a bool field to avoid unnecessary calls to the system setting. I’m not sure this prevents an actual performance loss, but it would be worth it to experiment and measure a little if you are using this technique in your apps.

double _currentValue;
bool _screenSaverEnabled = true;

private void OnAccelerometerHelperReadingChanged(object sender, AccelerometerHelperReadingEventArgs e)
{
    Dispatcher.BeginInvoke(() =>
        {
            // you'll have something more useful in your app
            computedValue = e.OptimallyFilteredAcceleration.X;

            var delta = Math.Abs(computedValue - _currentValue);
            if (_screenSaverEnabled)
            {
                if (delta > SOME_ARBITRARY_THRESOLD)
                {
                    _screenSaverEnabled = false;
                    PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled;
                    Debug.WriteLine("Screen saver disabled");
                }
            }
            else
            {
                _screenSaverEnabled = true;
                PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Enabled;
                Debug.WriteLine("Screen saver enabled");
            }
            _currentValue = computedValue;
        }
    );
}

Happy coding!

Windows Phone 7: correct pinch zoom in Silverlight

Pinch zooming is one of those things that look incredibly simple until you actually try to implement them. At that point you realize it hides quite a number of intricacies that make it hard to get it right. If you tried to implement pinch zooming in Silverlight for Windows Phone 7 you probably know what I’m talking about.

What does it means getting it right?

Adrian Tsai already gave an excellent explanation, so I won’t repeat his words. The test is extremely simple: pick two points in the image (for example two eyes) and zoom with your fingers on them. If at the end of the zoom the two points are still under your fingers you got it right –otherwise you got it wrong.

Multitouch Behavior

Laurent Bugnion, Davide Zordan and David Kelly are the men behind  Multitouch Behavior for SL and WPF. It’s an impressive open source project and you should check it out. In addition to pinch-zooming it gives you rotation, inertia, debug mode and much more. It’s extremely easy to work with as you just need a couple of lines of XAML. The only shortcoming is that at the time of writing it seems that there is no way to read the current zoom state, making it difficult to fully support tombstoning. If you don’t need this, go grab Multitouch Behavior and stop reading: it will probably work better and you’ll save some time.

The XAML

This is the XAML we are starting with. Notice that our DIY implementation relies on the Silverlight Toolkit’s InputGesture. If you are not yet using it, please install the toolkit and add a reference to Microsoft.Phone.Controls.Toolkit in your project.

xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
<Image x:Name="ImgZoom"
        Source="sample.jpg"
        Stretch="UniformToFill"
        RenderTransformOrigin="0.5,0.5"> 
    <toolkit:GestureService.GestureListener>
        <toolkit:GestureListener
                PinchStarted="OnPinchStarted"
                PinchDelta="OnPinchDelta"/>
    </toolkit:GestureService.GestureListener>
    <Image.RenderTransform>
        <CompositeTransform
                ScaleX="1" ScaleY="1"
                TranslateX="0" TranslateY="0"/>
    </Image.RenderTransform>
</Image>

The wrong way

I’ve seen this example several times around, I suppose you’ve seen it too somewhere on The Interwebs™:

double initialScale = 1d;

private void OnPinchStarted(object s, PinchStartedGestureEventArgs e)
{
    initialScale = ((CompositeTransform)ImgZoom.RenderTransform).ScaleX;
}

private void OnPinchDelta(object s, PinchGestureEventArgs e)
{
    var transform = (CompositeTransform)ImgZoom.RenderTransform;
    transform.ScaleX = initialScale * e.DistanceRatio;
    transform.ScaleY = transform.ScaleX;
}

Very simple and good looking. I love simple solutions and I bet you do too, but as someone once said “Things should be as simple as possible, but not simpler.” And unfortunately this is simpler than possible (is this even a sentence?). The problem is that the scaling is always centered in the middle of the image, so this solution won’t pass the poke-two-fingers-in-the-eyes test.

The better but still wrong way

The knee-jerk reaction is to move the scaling center between our fingers as we perform the scaling:

double initialScale = 1d;

private void OnPinchStarted(object s, PinchStartedGestureEventArgs e)
{
    initialScale = ((CompositeTransform)ImgZoom.RenderTransform).ScaleX;
}

private void OnPinchDelta(object s, PinchGestureEventArgs e)
{
    var finger1 = e.GetPosition(ImgZoom, 0);
    var finger2 = e.GetPosition(ImgZoom, 1);

    var center = new Point(
        (finger2.X + finger1.X) / 2 / ImgZoom.ActualWidth,
        (finger2.Y + finger1.Y) / 2 / ImgZoom.ActualHeight);

    ImgZoom.RenderTransformOrigin = center;

    var transform = (CompositeTransform)ImgZoom.RenderTransform;
    transform.ScaleX = initialScale * e.DistanceRatio;
    transform.ScaleY = transform.ScaleX;
}

This is better. The first time it actually works well too, but as soon as you pinch the image a second time you realize the image moves around. The reason: the zoom state is the sum of all the zoom operations (each one having its center) and by moving the center every time you are effectively removing information from the previous steps. To solve this problem we could replace the CompositeTransform with a TransformGroup and then add a new ScaleTransform (with a new center) at every PinchStart+PinchDelta event group. This will probably work: every scaling will keep its center and all is well. Except your phone will probably catch fire and explode because of the number of transforms you are stacking up. My team has a name for this kind of solutions, and it isn’t a nice one (fortunately there is no English translation for that).

The right way

It is clear by now that simply setting a scale factor and moving the center won’t take us far. As we are real DIYourselfers we will do it with a combination of scaling and translation. In the already mentioned article, Adrian Tsai uses this technique in XNA and we will apply the same concept in Silverlight. If an image is worth a million worth, a line of code is probably worth even more, so I’ll let the c# do the talking.

// these two fully define the zoom state:
private double TotalImageScale = 1d;
private Point ImagePosition = new Point(0, 0);

private Point _oldFinger1;
private Point _oldFinger2;
private double _oldScaleFactor;

private void OnPinchStarted(object s, PinchStartedGestureEventArgs e)
{
    _oldFinger1 = e.GetPosition(ImgZoom, 0);
    _oldFinger2 = e.GetPosition(ImgZoom, 1);
    _oldScaleFactor = 1;
}

private void OnPinchDelta(object s, PinchGestureEventArgs e)
{
    var scaleFactor = e.DistanceRatio / _oldScaleFactor;

    var currentFinger1 = e.GetPosition(ImgZoom, 0);
    var currentFinger2 = e.GetPosition(ImgZoom, 1);

    var translationDelta = GetTranslationDelta(
        currentFinger1,
        currentFinger2,
        _oldFinger1,
        _oldFinger2,
        ImagePosition,
        scaleFactor);

    _oldFinger1 = currentFinger1;
    _oldFinger2 = currentFinger2;
    _oldScaleFactor = e.DistanceRatio;

    UpdateImage(scaleFactor, translationDelta);
}

private void UpdateImage(double scaleFactor, Point delta)
{
    TotalImageScale *= scaleFactor;
    ImagePosition = new Point(ImagePosition.X + delta.X, ImagePosition.Y + delta.Y);

    var transform = (CompositeTransform)ImgZoom.RenderTransform;
    transform.ScaleX = TotalImageScale;
    transform.ScaleY = TotalImageScale;
    transform.TranslateX = ImagePosition.X;
    transform.TranslateY = ImagePosition.Y;
}

private Point GetTranslationDelta(
    Point currentFinger1, Point currentFinger2,
    Point oldFinger1, Point oldFinger2,
    Point currentPosition, double scaleFactor)
{
    var newPos1 = new Point(
        currentFinger1.X + (currentPosition.X - oldFinger1.X) * scaleFactor,
        currentFinger1.Y + (currentPosition.Y - oldFinger1.Y) * scaleFactor);

    var newPos2 = new Point(
        currentFinger2.X + (currentPosition.X - oldFinger2.X) * scaleFactor,
        currentFinger2.Y + (currentPosition.Y - oldFinger2.Y) * scaleFactor);

    var newPos = new Point(
        (newPos1.X + newPos2.X) / 2,
        (newPos1.Y + newPos2.Y) / 2);

    return new Point(
        newPos.X - currentPosition.X,
        newPos.Y - currentPosition.Y);
}

Also note that in the XAML we must set the RenderTransformOrigin to 0,0.
This finally passes the fingers-in-the-eyes test! Now we can add some bells and whistles like handling dragging, blocking the zoom-out when the image is at full screen, and avoiding that the image is dragged outside the visible area. For those extra details please see the sample solution at the end of the article.

What about MVVM?

You are using MVVM-light for your WP7 app, aren’t you? We all agree my code is ugly and not very MVVM friendly, I’ll make no excuses. However it’s all strictly UI code, so it doesn’t feel so bad to have it in the code behind. What you will probably do is wire TotalImageScale and ImagePosition to your ViewModel. Those two values fully define the state of the zoom, so if you save and reload them in your ViewModel you will be good to go.

Download

Here is the full sample project so that you can play with the code within the comfort of your Visual Studio (my daughter is in the picture, please treat her with respect :-) ).
Feel free to use the code in your project. As always, any kind of feedback is deeply appreciated!