Building A Multi-Touch Photo Viewer Control

If you want to view an image on your Windows Phone app, or on your Windows 8 tablet, most people would probably expect to be able to use their fingers to pinch zoom and drag using the touch screen.

Since this is a common scenario, I want to create a simple reusable control that allows me to do this using very little xaml, along the lines of this:

<my:ImageViewer
    Thumbnail="http://url.com/to/my/thumbnail.jpg"
    Image="http://url.com/to/my/MyImage.jpg"  />

…where Thumbnail is a low resolution image that loads fast, while the full resolution Image is being downloaded.

If you just want to use this control and don’t want to learn how to create a custom control, skip to the bottom to download the source for both Windows Phone and Windows 8 Runtime.

First off, we’ll create a new Windows Phone Class Library project and name it “SharpGIS.Controls”. (or whatever you like)

image

Create a new folder “Themes”, add a new XAML resource file and name it “Generic.xaml”. Make sure the “build action” for this file is set to “ApplicationDefinition”.

We will want to define the default template in this file for our control (if you are used to making User Controls, this is essentially where the XAML for custom controls go instead).

In the xaml we will want two things: An image for displaying a fast-loading thumbnail at first, and a second image for displaying the high resolution image. Also we will use a Grid around them to group them together. Generic.xaml should look something like this:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SharpGIS.Controls">
    <Style TargetType="local:ImageViewer">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:ImageViewer">
                    <Grid x:Name="Root" Background="Transparent">
                        <Image x:Name="Thumbnail" Source="{TemplateBinding Thumbnail}" CacheMode="BitmapCache" />
                        <Image x:Name="Image" Source="{TemplateBinding Image}" CacheMode="BitmapCache" />
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Notice the “CacheMode” property. This is important to set, to get proper performance when zooming and panning the image. If you have a Windows Phone try the official Facebook app, open an image and you will see what I mean (if you worked on that app PLEASE fix this).

Next up is the actual code. Add a new class, and name it “ImageViewer”. Make it inherit from ‘Control’. Add a constructor and use the “DefaultStyleKey” to tell Silverlight that you have a template defined in Generic.xaml that it should use.

public class ImageViewer : Control
{
    public ImageViewer()
    {
        DefaultStyleKey = typeof(ImageViewer);
    }
}

Next we define the dependency properties for the two images that the template was binding to:

public ImageSource Image
{
    get { return (ImageSource)GetValue(ImageProperty); }
    set { SetValue(ImageProperty, value); }
}

public static readonly DependencyProperty ImageProperty =
    DependencyProperty.Register("Image", typeof(ImageSource), typeof(ImageViewer), null);

public ImageSource Thumbnail
{
    get { return (ImageSource)GetValue(ThumbnailProperty); }
    set { SetValue(ThumbnailProperty, value); }
}

public static readonly DependencyProperty ThumbnailProperty =
    DependencyProperty.Register("Thumbnail", typeof(ImageSource), typeof(ImageViewer), null);

We should now be able to use this control in a Windows Phone project. Add a new Windows Phone Appplication project to your solution, right-click the ‘references’ and select “add reference’. Pick the SharpGIS.Controls project.

You should now be able to use some XAML like this to display an image:

<my:ImageViewer
    xmlns:my="clr-namespace:SharpGIS.Controls;assembly=SharpGIS.Controls"
    Thumbnail="http://url.com/to/my/thumbnail.jpg"
    Image="http://url.com/to/my/image.jpg" />

That’s all fine, but you still can’t use any touch to zoom the image.

Go back to the code and override OnApplyTemplate(). This code executes when the template from Themes\Generic.xaml has been loaded, and it’s your chance to grab any reference to the elements in there and ‘do something’ with them. In user controls you would often set the event handlers directly in the xaml. With templates on custom controls, you will have to hook these up in code-behind during OnApplyTemplate().

Here we will hook up for the manipulation events, as well as assign a transform we will apply to the element when these events trigger.

private Grid Root;
        
public override void OnApplyTemplate()
{
    Root = GetTemplateChild("Root") as Grid;
    if (Root != null)
    {
        Root.ManipulationDelta += Root_ManipulationDelta;
        Root.ManipulationStarted += Root_ManipulationStarted;
        Root.RenderTransform = new CompositeTransform();
    }
    base.OnApplyTemplate();
}

The ManipulationDelta event triggers as you move. It will give you information about how much the user dragged, and how much he/she pinches, as well as the center of the pinch. Unfortunately the pinch scale amount is shown as separate X and Y directions, and no ‘Uniform Scale’ is shown. This makes it hard to get a good average of the scale, and you would have to pick just one of them (one could be pinching and the other stretching).

I’ve found that defining the amount you’re scaling is the change of the length of the diagonal of the boundingbox of all the touch points works well. Errrrrrr, that might have sounded confusing. Let’s use a picture instead. The orange circles below are touchpoints, and the rectangle is the box that encompasses all of them (2 or more points). The blue line is the diagonal length of this box. So the amount of scaling = length_Before / length_After.

image

We don’t get the actual touch points in the manipulation events. So instead I start with a simple box that I define as 1x1 and track the scaling of it. The diagonal length of that box at the beginning is the square root of 2, which we will define in the ManipulationStarted event. We also add a property for tracking the center of the box.

private Point? lastOrigin;
private double lastUniformScale;

private void Root_ManipulationStarted(object sender, ManipulationStartedEventArgs e)
{
    lastUniformScale = Math.Sqrt(2);
    lastOrigin = null;
}

So all that’s left is listen to the ManipulationDelta event, and update the transform on the grid. This consist of comparing previous origin to the new, as well as calculating the scale factor based on the box diagonal. Also note that when you apply scale, this is means you’re scaling out and away from the upper left corner. To offset this, you will need to add some additional translation to the image, as shown below:

private void Root_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
    var transform = Root.RenderTransform as CompositeTransform;
    if (transform != null)
    {
        var origin = e.ManipulationContainer.TransformToVisual(this).Transform(e.ManipulationOrigin);

        if (!lastOrigin.HasValue)
            lastOrigin = origin;

        //Calculate uniform scale factor
        double uniformScale = Math.Sqrt(Math.Pow(e.CumulativeManipulation.Scale.X, 2) + 
                                        Math.Pow(e.CumulativeManipulation.Scale.Y, 2));
        if (uniformScale == 0)
            uniformScale = lastUniformScale;

        //Current scale factor
        double scale = uniformScale / lastUniformScale;

        if (scale > 0 && scale != 1)
        {
            //Apply scaling
            transform.ScaleY = transform.ScaleX *= scale;
            //Update the offset caused by this scaling
            var ul = Root.TransformToVisual(this).Transform(new Point());
            transform.TranslateX = origin.X - (origin.X - ul.X) * scale;
            transform.TranslateY = origin.Y - (origin.Y - ul.Y) * scale;
        }
        //Apply translate caused by drag
        transform.TranslateX += (origin.X - lastOrigin.Value.X);
        transform.TranslateY += (origin.Y - lastOrigin.Value.Y);

        //Cache values for next time
        lastOrigin = origin;
        lastUniformScale = uniformScale;
    }
}

And that’s it!

Now what’s left is to turn off the thumbnail when the image has loaded, as well as raise loaded events for the high resolution image, so that you can display a progress bar while you wait for the image to load. I won’t go into details on this, but in OnApplyTemplate, simply grab the Image template child, and listen for the ImageLoaded event.

I’ve packaged it all up in one complete control, as well as a sample showing how this would be used in an application where you would navigate to a page with this control on it.

You can download the source and a sample app here.

Here’s a preview off what that app looks like:

Comments (5) -

  • Very cool!  

    I haven't tackled phone dev yet so forgive my asking but is the phone ManipulationDeltaEvent different between WPF and the Phone?

    In WPF, we have all the touch points in the manipulation event. Ex:

    foreach (IManipulator m in deltaArgs.Manipulators) {
       Point tp = m.GetPosition(this);
    }

    Also, we avoid the Sqrt and Pow by using the precomputed length of the scale vector which WPF gives to us:

    scale = deltaArgs.CumulativeManipulation.Scale.Length / 1.41;  // Normalize to [0..1]

    Anyway, thanks for posting this. It's pretty cool!
    John
  • John: No the are pretty much the same as WPF, however WPF does give you more information as you point out (including the correct scaling that I would have to jimmy rig to get to on WP7).
  • Hi, You said you have a windows 8 control (winrt). can you provide the link to that? thanks
  • Tareq: I said that? When? I don't have a Win8 version at this point but you could probably fairly easy port the above code to WinRT.
  • I read that "If you just want to use this control and don’t want to learn how to create a custom control, skip to the bottom to download the source for both Windows Phone and Windows 8 Runtime."

    I thought you have it lol
    btw thanks, it helped a lot in my windows phone app

Pingbacks and trackbacks (5)+

Add comment