[WP7]Correct pinch zoom in Silverlight

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 theSilverlight 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.

1. xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
01. <Imagex:Name="ImgZoom"
02.         Source="sample.jpg"
03.         Stretch="UniformToFill"
04.         RenderTransformOrigin="0.5,0.5">
05.     <toolkit:GestureService.GestureListener>
06.         <toolkit:GestureListener
07.                 PinchStarted="OnPinchStarted"
08.                 PinchDelta="OnPinchDelta"/>
09.     </toolkit:GestureService.GestureListener>
10.     <Image.RenderTransform>
11.         <CompositeTransform
12.                 ScaleX="1"ScaleY="1"
13.                 TranslateX="0"TranslateY="0"/>
14.     </Image.RenderTransform>
15. </Image>

The wrong way

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

01. doubleinitialScale = 1d;
02.   
03. privatevoid OnPinchStarted(objects, PinchStartedGestureEventArgs e)
04. {
05.     initialScale = ((CompositeTransform)ImgZoom.RenderTransform).ScaleX;
06. }
07.   
08. privatevoid OnPinchDelta(objects, PinchGestureEventArgs e)
09. {
10.     var transform = (CompositeTransform)ImgZoom.RenderTransform;
11.     transform.ScaleX = initialScale * e.DistanceRatio;
12.     transform.ScaleY = transform.ScaleX;
13. }

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:

01. doubleinitialScale = 1d;
02.   
03. privatevoid OnPinchStarted(objects, PinchStartedGestureEventArgs e)
04. {
05.     initialScale = ((CompositeTransform)ImgZoom.RenderTransform).ScaleX;
06. }
07.   
08. privatevoid OnPinchDelta(objects, PinchGestureEventArgs e)
09. {
10.     var finger1 = e.GetPosition(ImgZoom, 0);
11.     var finger2 = e.GetPosition(ImgZoom, 1);
12.   
13.     var center =new Point(
14.         (finger2.X + finger1.X) / 2 / ImgZoom.ActualWidth,
15.         (finger2.Y + finger1.Y) / 2 / ImgZoom.ActualHeight);
16.   
17.     ImgZoom.RenderTransformOrigin = center;
18.   
19.     var transform = (CompositeTransform)ImgZoom.RenderTransform;
20.     transform.ScaleX = initialScale * e.DistanceRatio;
21.     transform.ScaleY = transform.ScaleX;
22. }

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 isthe 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 thealready 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.

01. // these two fully define the zoom state:
02. privatedouble TotalImageScale = 1d;
03. privatePoint ImagePosition = newPoint(0, 0);
04.   
05. privatePoint _oldFinger1;
06. privatePoint _oldFinger2;
07. privatedouble _oldScaleFactor;
08.   
09. privatevoid OnPinchStarted(objects, PinchStartedGestureEventArgs e)
10. {
11.     _oldFinger1 = e.GetPosition(ImgZoom, 0);
12.     _oldFinger2 = e.GetPosition(ImgZoom, 1);
13.     _oldScaleFactor = 1;
14. }
15.   
16. privatevoid OnPinchDelta(objects, PinchGestureEventArgs e)
17. {
18.     var scaleFactor = e.DistanceRatio / _oldScaleFactor;
19.   
20.     var currentFinger1 = e.GetPosition(ImgZoom, 0);
21.     var currentFinger2 = e.GetPosition(ImgZoom, 1);
22.   
23.     var translationDelta = GetTranslationDelta(
24.         currentFinger1,
25.         currentFinger2,
26.         _oldFinger1,
27.         _oldFinger2,
28.         ImagePosition,
29.         scaleFactor);
30.   
31.     _oldFinger1 = currentFinger1;
32.     _oldFinger2 = currentFinger2;
33.     _oldScaleFactor = e.DistanceRatio;
34.   
35.     UpdateImage(scaleFactor, translationDelta);
36. }
37.   
38. privatevoid UpdateImage(doublescaleFactor, Point delta)
39. {
40.     TotalImageScale *= scaleFactor;
41.     ImagePosition =new Point(ImagePosition.X + delta.X, ImagePosition.Y + delta.Y);
42.   
43.     var transform = (CompositeTransform)ImgZoom.RenderTransform;
44.     transform.ScaleX = TotalImageScale;
45.     transform.ScaleY = TotalImageScale;
46.     transform.TranslateX = ImagePosition.X;
47.     transform.TranslateY = ImagePosition.Y;
48. }
49.   
50. privatePoint GetTranslationDelta(
51.     Point currentFinger1, Point currentFinger2,
52.     Point oldFinger1, Point oldFinger2,
53.     Point currentPosition,double scaleFactor)
54. {
55.     var newPos1 =new Point(
56.         currentFinger1.X + (currentPosition.X - oldFinger1.X) * scaleFactor,
57.         currentFinger1.Y + (currentPosition.Y - oldFinger1.Y) * scaleFactor);
58.   
59.     var newPos2 =new Point(
60.         currentFinger2.X + (currentPosition.X - oldFinger2.X) * scaleFactor,
61.         currentFinger2.Y + (currentPosition.Y - oldFinger2.Y) * scaleFactor);
62.   
63.     var newPos =new Point(
64.         (newPos1.X + newPos2.X) / 2,
65.         (newPos1.Y + newPos2.Y) / 2);
66.   
67.     returnnew Point(
68.         newPos.X - currentPosition.X,
69.         newPos.Y - currentPosition.Y);
70. }

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!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值