Correcting Pinch Zoom in Silverlight for Windows Phone
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 mean 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!
Source: http://www.frenk.com/2011/03/windows-phone-7-correct-pinch-zoom-in-silverlight/
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)






Comments
Paul Shezier replied on Fri, 2012/04/13 - 3:20pm
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..