Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

Semantic Zoom in Windows Phone 8

From Wiki
Jump to: navigation, search
Featured Article
02 Mar
2014

Note.pngNote: This is an entry in the Nokia Imaging and Big UI Wiki Competition 2013Q4.

This article proposes a new SemanticZoom control for Windows Phone 8.

WP Metro Icon UI.png
SignpostIcon XAML 40.png
WP Metro Icon WP8.png
Article Metadata
Code ExampleTested with
SDK: Windows Phone 8.0 SDK
Compatibility
Platform(s):
Windows Phone 8
Article
Created: dtimotei (08 Dec 2013)
Last edited: croozeus (02 Mar 2014)

Contents

Introduction

Big UI. Big screen. We all waited for it, for full HD resolution on our Windows Phone 8 devices. And now we have it! But what can we do with it? Well, show more information than on a lower resolution screen. But sometimes there is too much information. What about using a concept that is already established on the Windows 8 platform, which is called Semantic Zoom. With this, we can show much information while allowing the user to quickly navigate through it. This will work also on lower res screens but the new full HD resolutions will benefit so much more from this.

In this article we'll present a new control which can be used to show structured information on high resolution Windows Phone 8 devices.

The Custom Control

Design

The SemanticZoom control should behave like the one in Windows 8. Thus we'll name it SemanticZoom. It will require three properties:

  • ZoomedInView - stores the view which is shown by default. It represent the "plain" state.
  • ZoomedOutView - stores the zoomed out view, which should contain the overview information (a summary of the plain state which allows quick navigation).
  • IsZoomedOut - boolean flag whether the zoomed out view is currently shown or not.

The functionality is simple, yet very powerful; we have two views: a zoomed in view and a zoomed out one which are shown mutually exclusive. We will usually place a long list in the default view. When we pinch the view to scale it in, the zoomed out view, with the overview will be shown. We also aim to have a fluid animation when we switch between the two views.

Note.pngNote: Why not LongListSelector? One may wonder why we shouldn't use instead the LongListSelector class in WP8. Well, that is still a list that can be customized only up to a point. You can't remove the inherent list behaviour. On the other hand, using the SemanticZoom control we can show separate types of information for the two views, bringing us 100% flexibility on how we design the UI, especially on high resolution devices. Maybe we have some hardcoded list items that will never change, or we can have just a couple of buttons that do some specific action.

Implementation

We first start with a basic control, a UserControl. Since we want to leverage the data binding feature of XAML, we'll create the two dependency properties that will hold the views:

        public static readonly DependencyProperty ZoomedInViewProperty =
DependencyProperty.Register("ZoomedInView", typeof(UIElement), typeof(SemanticZoom), new PropertyMetadata(default(UIElement)));
 
public UIElement ZoomedInView
{
get { return (UIElement) GetValue(ZoomedInViewProperty); }
set { SetValue(ZoomedInViewProperty, value); }
}
 
public static readonly DependencyProperty ZoomedOutViewProperty =
DependencyProperty.Register("ZoomedOutView", typeof(UIElement), typeof(SemanticZoom), new PropertyMetadata(default(UIElement)));
 
public UIElement ZoomedOutView
{
get { return (UIElement) GetValue(ZoomedOutViewProperty); }
set { SetValue(ZoomedOutViewProperty, value); }
}

For the boolean flag IsZoomedOut we'll do the same:

        public static readonly DependencyProperty IsZoomedOutProperty =
DependencyProperty.Register("IsZoomedOut", typeof(bool), typeof(SemanticZoom), new PropertyMetadata(default(bool), IsZoomedOutPropertyChangedCallback));
 
public bool IsZoomedOut
{
get { return (bool) GetValue(IsZoomedOutProperty); }
set { SetValue(IsZoomedOutProperty, value); }
}
 
private static void IsZoomedOutPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
// Toggle the views.
}

One can note that we used a callback to know when the zoomed flag is changed, so we can act on it and switch the views. But before switching them, we need a place where to show them.

In the .xaml file we will have the following default code:

<UserControl x:Class="SemanticZoomLibrary.SemanticZoom"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
d:DesignHeight="480" d:DesignWidth="480">
 
<Grid x:Name="LayoutRoot" Background="{StaticResource PhoneChromeBrush}">
<!-- Control code -->
</Grid>
</UserControl>

So, we need to add inside the grid, two content controls which will hold each view, each's content will be bound to the respective view dependency property:

<ContentControl Name="ZoomedOutContent" Content="{Binding ZoomedOutView}" Visibility="Collapsed" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ManipulationDelta="ZoomedOutContent_OnManipulationDelta">
<ContentControl.RenderTransform>
<CompositeTransform ScaleX="1" ScaleY="1"/>
</ContentControl.RenderTransform>
 
<ContentControl.RenderTransformOrigin>
<Point X="0.5" Y="0.5"/>
</ContentControl.RenderTransformOrigin>
</ContentControl>
 
<ContentControl Name="ZoomedInContent" Content="{Binding ZoomedInView}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" ManipulationDelta="ZoomedInContent_OnManipulationDelta">
<ContentControl.RenderTransform>
<CompositeTransform ScaleX="1" ScaleY="1"/>
</ContentControl.RenderTransform>
<ContentControl.RenderTransformOrigin>
<Point X="0.5" Y="0.5"/>
</ContentControl.RenderTransformOrigin>
</ContentControl>

Note.pngNote: We'll need to set the DataContext for the root grid to the class instance of this control in order for the data binding to work. We'll do that later in the code.

The two content controls will have a default render transform with scale 1 (to be modified later during transition phase) and the origin for the render transform will be {0.5, 0.5} (the center of the control). The zoomed out view will be hidden at the beginning. In order to be able to toggle with touch between the two views, we'll hook in the manipulation events, more specifically the ManipulationDelta event.

Now, on to the animation for the toggling. This one is composed of two parts, for each we'll create methods in the code behind:

  • Opacity increase/decrease
  • Scaling increase/decrease

The scaling animation is executed by a DoubleAnimation on the control render transform's scale component. We take the target control, the limits for the scaling and the axis on which we should apply the scale:

private DoubleAnimation CreateScaleAnimation(UIElement target, double from, double to, char axis)
{
DoubleAnimation doubleAnimation = new DoubleAnimation
{
Duration = TimeSpan.FromMilliseconds(ScaleAnimationDuration),
From = from,
To = to
};
 
Storyboard.SetTargetProperty(doubleAnimation, new PropertyPath(
string.Format("(UIElement.RenderTransform).(ScaleTransform.Scale{0})", axis)));
Storyboard.SetTarget(doubleAnimation, target);
 
return doubleAnimation;
}

The opacity animation is executed also by a DoubleAnimation on the render transform. This has just two parameters: the target control and a flag whether we want to show or hide it. Based on this, we'll either from from a 0 (hidden, transparent) to 1 (visible, opaque) opacity. At the end of the animation we'll need to modify the visibility, because otherwise we'd have the controls conflict when taps or other touch actions are executed.

private DoubleAnimation CreateOpacityAnimation(UIElement target, bool show)
{
DoubleAnimation opacityAnimation = new DoubleAnimation
{
Duration = TimeSpan.FromMilliseconds(OpacityAnimationDuration),
From = show ? 0 : 1,
To = show ? 1 : 0
};
 
Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath(OpacityProperty));
Storyboard.SetTarget(opacityAnimation, target);
 
opacityAnimation.Completed += (sender, args) =>
{
target.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
};
 
return opacityAnimation;
}

The efective, complete animation creation methods use some fields that are defined (and set) in the following way:

public SemanticZoom()
{
InitializeComponent();
 
LayoutRoot.DataContext = this;
IsZoomedOut = false;
ScaleAnimationDuration = 350;
OpacityAnimationDuration = 350;
}
 
public int ScaleAnimationDuration { get; set; }
public int OpacityAnimationDuration { get; set; }

Now that we have the bits for the animation(s) we can create the main ZoomIn() and ZoomOut() methods:

private void ZoomIn()
{
Storyboard storyboard = new Storyboard();
 
storyboard.Children.Add(CreateScaleAnimation(ZoomedInContent, 0, 1, 'X'));
storyboard.Children.Add(CreateScaleAnimation(ZoomedInContent, 0, 1, 'Y'));
storyboard.Children.Add(CreateScaleAnimation(ZoomedOutContent, 1, 2, 'X'));
storyboard.Children.Add(CreateScaleAnimation(ZoomedOutContent, 1, 2, 'Y'));
storyboard.Children.Add(CreateOpacityAnimation(ZoomedInContent, true));
storyboard.Children.Add(CreateOpacityAnimation(ZoomedOutContent, false));
 
ZoomedInContent.Visibility = Visibility.Visible;
ZoomOutButton.Visibility = Visibility.Visible;
 
storyboard.Begin();
}
 
private void ZoomOut()
{
Storyboard storyboard = new Storyboard();
 
storyboard.Children.Add(CreateScaleAnimation(ZoomedInContent, 1, 0, 'X'));
storyboard.Children.Add(CreateScaleAnimation(ZoomedInContent, 1, 0, 'Y'));
storyboard.Children.Add(CreateScaleAnimation(ZoomedOutContent, 2, 1, 'X'));
storyboard.Children.Add(CreateScaleAnimation(ZoomedOutContent, 2, 1, 'Y'));
storyboard.Children.Add(CreateOpacityAnimation(ZoomedInContent, false));
storyboard.Children.Add(CreateOpacityAnimation(ZoomedOutContent, true));
 
ZoomOutButton.Visibility = Visibility.Collapsed;
ZoomedOutContent.Visibility = Visibility.Visible;
 
storyboard.Begin();
}

The actual animation is done using a Storyboard in which we add the individual animations: scale from 0 (hidden) to 1 (visible) for one control, scale from 2 to 1 for the other one and the opacity animations on each. We will also toggle the visibility of a zoom button, which we'll add later in the article.

The final bits consist of creating the manipulation event handlers:

private void ZoomedOutContent_OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (e.PinchManipulation != null &&
IsZoomInPinch(e.PinchManipulation))
{
IsZoomedOut = false;
e.Handled = true;
e.Complete();
}
}
 
private void ZoomedInContent_OnManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (e.PinchManipulation != null &&
!IsZoomInPinch(e.PinchManipulation))
{
IsZoomedOut = true;
e.Handled = true;
e.Complete();
}
}

In order to know which was we're pinching, and thus what kind of zoom we wish to perform, we'll make a helper function. This will check if the distance between the original pinch contacts is smaller or bigger than the current one. If the current distance is bigger than the original one, it means we did a zoom in operation. The code bits are presented below, and they use the distance formula, to calculate the distance between the two points:

private bool IsZoomInPinch(PinchManipulation pinchManipulation)
{
double originalDistance = Math.Sqrt(
Math.Pow(pinchManipulation.Original.PrimaryContact.X - pinchManipulation.Original.SecondaryContact.X, 2) +
Math.Pow(pinchManipulation.Original.PrimaryContact.Y - pinchManipulation.Original.SecondaryContact.Y, 2));
double currentDistance = Math.Sqrt(
Math.Pow(pinchManipulation.Current.PrimaryContact.X - pinchManipulation.Current.SecondaryContact.X, 2) +
Math.Pow(pinchManipulation.Current.PrimaryContact.Y - pinchManipulation.Current.SecondaryContact.Y, 2));
return currentDistance > originalDistance;
}

Right at the beginning, remember we created the callback for the IsZoomedOut's property change, but didn't implement it. The implementation is the following, now that we have the zooming methods:

private static void IsZoomedOutPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
if ((bool) dependencyPropertyChangedEventArgs.NewValue)
((SemanticZoom) dependencyObject).ZoomOut();
else
((SemanticZoom) dependencyObject).ZoomIn();
}

For completeness, we'll add a button in the LayoutRoot grid, at the end. This button will provide a quick access to zooming out and an indicator that a SemanticZoom control has been used:

<Button Name="ZoomOutButton" Content="-" BorderThickness="0" Padding="0" HorizontalAlignment="Right" VerticalAlignment="Bottom" Click="ZoomOutButtonClick"/>

The click handler implementation is a breeze. All we have to do is to set the IsZoomedOut flag to true, and the rest is done by the data binding callbacks which in turn call the zoom methods:

private void ZoomOutButtonClick(object sender, RoutedEventArgs e)
{
IsZoomedOut = true;
}

To be able to reuse the control we'll place the code we have so far into a library.

Using the control in a sample application

In order to showcase the control in a "real" application, I've built a simple app that lists some real and fictitious (unreleased) devices with Windows Phone, grouped by display size. The following screenshot will show the default view of the application:

The default app screen (zoomed in view)

If we apply a manipulation (zoom out) or click the bottom-right '-' button, we'll see the semantic zoom (zoomed out) view:

The display sizes list (zoomed out view)

The usage of the SemanticZoom control is the following:

<SemanticZoomLibrary:SemanticZoom Grid.Row="1" Margin="0,2,0,-2" Name="ZoomControl">
<SemanticZoomLibrary:SemanticZoom.ZoomedInView>
<ListBox Name="DevicesList" ItemsSource="{Binding Devices, Source={StaticResource DevicesRepository}}" >
<ListBox.ItemTemplate>
<DataTemplate>
<local:DeviceDataTemplateSelector Content="{Binding}">
<local:DeviceDataTemplateSelector.GroupTemplate>
<DataTemplate>
<Grid Background="RoyalBlue" HorizontalAlignment="Stretch">
<TextBlock Margin="10" HorizontalAlignment="Stretch">
<Run Text="Devices with display size: "/>
<Run Text="{Binding DisplaySize}"/>
</TextBlock>
</Grid>
</DataTemplate>
</local:DeviceDataTemplateSelector.GroupTemplate>
<local:DeviceDataTemplateSelector.DeviceTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" FontSize="45"/>
</DataTemplate>
</local:DeviceDataTemplateSelector.DeviceTemplate>
</local:DeviceDataTemplateSelector>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</SemanticZoomLibrary:SemanticZoom.ZoomedInView>
<SemanticZoomLibrary:SemanticZoom.ZoomedOutView>
<ListBox ItemsSource="{Binding DisplaySizes, Source={StaticResource DevicesRepository}}" Margin="50" SelectionChanged="Selector_OnSelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Source="/Assets/phone.png" Height="50"/>
<TextBlock Grid.Column="1" Text="{Binding}" FontSize="45"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</SemanticZoomLibrary:SemanticZoom.ZoomedOutView>
</SemanticZoomLibrary:SemanticZoom>

Basically, we have a list with all devices in the normal (zommed in) view, grouped by display size. Then, in the zoomed out view we have just the list of display sizes. When we click on one of it, we'll go to that group in the main list, with the following code:

private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var devicesList = ((ListBox) ZoomControl.ZoomedInView);
ZoomControl.IsZoomedOut = false;
 
var displaySize = e.AddedItems.Cast<string>().FirstOrDefault();
var device = ((DevicesRepository) Resources["DevicesRepository"]).Devices.FirstOrDefault(d => d.DisplaySize == displaySize);
 
// WP requires this hack to have the target device at the top
devicesList.ScrollIntoView(devicesList.Items.Last());
devicesList.Dispatcher.BeginInvoke(() =>
{
devicesList.ScrollIntoView(device);
});
}

If you are wondering what the DeviceDataTemplateSelector is, it's a simple class that selects a template based on what actual object (newContent) we have as list item:

public class DeviceDataTemplateSelector : ContentControl
{
public DataTemplate GroupTemplate { get; set; }
public DataTemplate DeviceTemplate { get; set; }
 
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
 
ContentTemplate = SelectTemplate(newContent, this);
}
private DataTemplate SelectTemplate(object newContent, DependencyObject dependencyObject)
{
WindowsPhoneDevice device = newContent as WindowsPhoneDevice;
if (device != null)
{
if (string.IsNullOrEmpty(device.Name))
{
return GroupTemplate;
}
 
return DeviceTemplate;
}
return null;
}
}

A video showing the usage of the control can be viewed below:

Conclusion

Having a big UI gives us more opportunities to show more and richer information. However this can come with the cost of having to grasp all that information. Separating, or giving an overview on the existing data can greately enhance the usability of our applications. With the usage of a SemanticZoom control we can do just that.

The full sample + the library can be found here: Media:SemanticZoomSample.zip

Happy coding! :)

References

This page was last modified on 2 March 2014, at 13:19.
422 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×