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!

45 Responses to Windows Phone 7: correct pinch zoom in Silverlight

  1. Prabhu says:

    Thanks for this post. I was looking just for this.

    I have a doubt though; in “The right way” implementation, when you apply the scale transform what is the RenderTransformOrigin point for it? I saw that it is set to 0,0 in the xaml..

    • frenk says:

      Nice to hear it helped!
      That’s correct, the origin must be set at 0,0 (the translation assumes that zoom is around that point).

  2. Prabhu says:

    Thanks for this post. I was looking just for this.

    I have a doubt though; in “The right way” implementation, when you apply the scale transform what is the RenderTransformOrigin point for it? I saw that it is set to 0,0 in the xaml..

    Prabhu
    http://www.techtwaddle.net

  3. Prabhu says:

    Ok, I get it now. Along with scaling the image is also translated, so appears centered around pinch location.

    Prabhu
    http://www.techtwaddle.net

  4. Thanks !!

    P.S.
    Your daughter is cute !!

  5. [...] Windows Phone 7: Correct pinch zoom in silverlight by Francesco De Vittori. I was using this before I stumbled on Charles’ article :) Also, Charles’ implementation seemed more intuitive to me. [...]

  6. William says:

    Thanks for this post, this really makes the Pan/Zoom with GestureListener usable!

    Do you know how I could modify this code to support the image zooming staying inside a grid?

    I am trying to use this code on a page where I have a few buttons (like next, previous, etc.) that get covered up if you zoom too far.

  7. Andy says:

    Hi,
    thanks for this – its extremely helpful. I am curious about a problem i am seeing though when i wrap the Image control in a ScrollViewer so that i can pan around the zoomed in image. I get some very odd distortion going on and i have been racking my brains to try and solve this to no avail.

    I have simply placed it around your XAML (inside the Grid)

    <Image x:Name="ImgZoom"…
    ….

    If you give this a try on the “Bells and Whistles” page, zoom in a bit then try some up/down swipes to scroll you will see what i mean. If you have any idea on this that would put me out of my misery i would be very thankfull!
    Cheers

  8. Sebastian says:

    Perfect example. Thanks for publishing! The last thing missing for me is the correct implementation of the flick effect like in the build in wp7 image viewer so that you can push an image around on the surface.

    Is it possible that you add this to your sample project? Flicking seems to be the most complicated thing here…

  9. Michael says:

    Great job!! I am wondering if it can be made to be smoother, the zooming seems kind of choppy compared to the xna example which is very fluid and oPhoneish. Thanks!

  10. anui says:

    Thank you very much!!
    This helps me save a lot of time. And maybe in number of projects.

  11. Paul says:

    Hi, Thanks for sharing this. One thing: if you add CacheMode=”BitmapCache” to the Image in the xaml, it makes a big difference in the speed of the zooming.
    Thanks.

  12. Edwin says:

    Hi Fran,

    I Just wanted to say THANK YOU. This code has improved my app a lot – other solutions asked me to use XNA and all sorts of rubbish, this does the trick much better. I still need to work on it but I finally have a professional implementation of WP7 pinch zoom.

    Thanks mate, I hope you know how A) Smart you are and B) how much your work means to guys like me.

  13. maxim says:

    if you set low bound for scale let’s say 0.5 instead of 1 in:
    (TotalImageScale * scaleDelta >= 1) && (Tot caleDelta <= MAX_IMAGE_ZOOM);
    then when you zoomout image to scale less than 1 it starts to jump around. How to solve this?

  14. Chris says:

    You are the man. I’ve been screwing around with one of the not so good solutions I found on the web for days now trying to get it right. http://alvaropeon.wordpress.com/2011/03/10/implementing-pinch-to-zoom-images-in-wp7/

    Thankfully your solution works perfectly.

  15. Inquisitor says:

    For my immediate needs, this worked great! Thanks for posting.

    BTW: As a follow-on article, I might suggest you add rotation… moving (and rotating) the image object within a larger object.

    • frenk says:

      I would really like to improve and extend this sample, however with the second children that arrived a few weeks ago it will take a good while to find the time :-)

  16. Jonny says:

    Thanks for this. This is the best solution I’ve found so far (like other also noted).

    However it’s still not perfect. The performance on a real device is really sluggish/slow. Just compare it to the zoom and panning of the default pictures app in WP7. That one is really smooth and nice. There’s gotta be a way to do this perfectly nice and clean without reinventing wheels, fires and what not. It’s strange that MS doesn’t seem to be able to get these things right.

    I’ll still be looking for a way to improve the above code. Like finding where all the slowness comes from.

    • frenk says:

      I haven’t looked at the code for a while but I remember it wasn’t really as good as the built-in control. It was ok but not perfect.

      I completely agree with you, this should be readily available in the SDK, maybe in the Toolkit first. Maybe if enough people tell the Toolkit team this has a chance to happen!

  17. msuk says:

    Thank you very much!
    This article helped me a lot.

    I need to set default scale and position after load image.
    The scale i set:
    ImgZoom.Source = bitmap;
    UpdateImageScale(2.9);
    and it’s OK but how to set image positon to middle of the image?

    b.r.
    msuk

  18. Amir says:

    Thanks for this. This has helped me a lot. Have you found a way to reduce the sluggishness of the zooming? I’ve tried caching the image but the quality of the image is unbearable. Any thoughts?

  19. abk says:

    Thanks a lot frenk for posting the code. It did really help as other users… although I am facing an issue similar to posted by Amir:

    Please update if someone has already found a solution to this

    When i wrap the Image control in a ScrollViewer so that i can pan around the zoomed in image. I get some very odd distortion going on and i have been racking my brains to try and solve this to no avail.

    I have simply placed it around your XAML (inside the Grid)

    <Image x:Name="ImgZoom"

  20. Ryan says:

    Thanks so much for this sample! You are a god! With the CacheMode = “BitmapCache” it works perfectly.

  21. TheKid says:

    Just a thought…

    Has anyone ever tried using the WebBrowser control that is just displaying the image?

    • Thanks frenk for this, works a treat for the app I’m working on.

      TheKid – Yes my app used to just show the image in a web browser, the experience was sub par. Yes you got pinch zoom but the image would never start in full screen and always felt a little dijointed – doing it this way gives a much more connected feel to it all I find – but there is nothing stopping you from using the web browser from a technical stand point

  22. frenk says:

    Hi everybody, sorry for my lack of responses. Unfortunately I’m going through a super busy period and have a hard time putting further work into this sample. However I appreciated a lot all the contributions!

  23. Andy says:

    Hi Frenk,
    sounds like you are preoccupied with children in ‘free’ time which is similar to me. The above code appears to work perfectly on first glance but i have found an oddity that i wonder if you had any insight to. I have tried the following on your demo app as well as the piece i wrote myself and the behaviour is the same. Basically if you pan one way or another, the image will not scale from the midpoint from that point on. As you scale up the image travels ever so slightly in the direction of the pan. The problem is worse at the extremeties of an image. As an example, pan the image to the left and then try a pinch/zoom on the right hand side – you will see the image travel left during the pinch. Pan up and try a pinch zoom at the bottom of the image and the image will travel upwards during the pinch.

    I cannot for the life of me figure out why because the maths looks correct and if you do a Pan (i.e. update the translate X and Y) the composite trannsform seems to always be in posession of the right data. Is it a mathematical rounding issue or is the formula not quite right or is there a subtle trick to fix this. I’ve been tweaking the formula and xaml without any joy so would be pleased if you had any ideas. I know it can work as i expect because i can to scale and pan on the standard windows phone image viewer.

    Many thanks for the original article as well – was a great head start for me
    Rgeards
    Andy

  24. [...] to what is experience in the Windows Phone web browser and other native apps, I stumbled upon the IndexOutRange blog, where the another dev has taken what I consider a successful stab at the problem. How I [...]

  25. tingting says:

    Good job!
    Thanks for this post, it’s useful.
    ps:the child is so lovely.

  26. gano says:

    Cheers for the code dude, really helpful for a noob app developer like me.

    Many thanks

    B >o<

  27. Philip Colmer says:

    Hi

    Thank you for sharing this code – it has saved lots of headaches!

    I’m using the Image control pointing at a BitmapImage that has been created from a downloaded JPEG.

    I’ve addded CacheMode=”BitmapCache” to the XAML definition for Image but panning around the zoomed-in image is not smooth. Any thoughts or suggestions on what I might have done wrong?

    Also, does wrapping the image inside a scrollviewer cause problems? I wanted to allow the user to scroll the page up to see text that goes with the image but it seems like wrapping a scrollviewer around the image stops the gestures from working. The events fire but the image never changes.

    Thanks.

    Philip

  28. HamGuy says:

    Hi, frenk
    Thanks for your tips. But I got a question:
    I added a image control in my page, and set the value of streach property with “uniformtofill” in landscape oritation layout, “uniform” in portrait oritation layout. Both the width and height were “Auto”. But when the page oritation changed, the pinch and zoom din’t work any more. I debuged and traced step by step, but found nothing. Where was wrong? Could you give me a hand?
    Looking forward your reply! Thanks in advance!

  29. Dachi says:

    Hi frenk!

    Thank you for the great tutorial! I have two doubts/questions if you can solve it.

    1) When the image is in a ScrollViewer he doesn’t work
    2) How can I implement when I Pinch to zoom and after I will drag the image.

    Thank you for all and greetings

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>