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.

Revision as of 08:51, 22 October 2013 by kiran10182 (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Creating Interactive Custom User Controls with Silverlight

From Wiki
Jump to: navigation, search

Usually Windows Phone applications that have highly interactable objects, game-like applications, are created with DirectX (or XNA in Windows Phone 7.x). Sometimes, however, the other aspects in the application drive using Silverlight, especially since it offers a wide scale of great ready-made components such as maps. This was also the case with Compass application; it really has only one component that had to be customised: The compass user control that is manipulated in different ways e.g. dragging the item on the screen, rotating it etc.

WP Metro Icon UI.png
SignpostIcon XAML 40.png
WP Metro Icon WP8.png
Article Metadata
Tested with
SDK: Visual Studio 2012 Express for Windows Phone 8
Devices(s): Nokia Lumia 920
Compatibility
Platform(s): Windows Phone 8
Windows Phone 8
Article
Keywords: Silverlight, UserControl
Created: Tomi_ (16 Dec 2012)
Last edited: kiran10182 (22 Oct 2013)

Contents

Composition of the compass control

The base class of the compass control is UserControl. The implementation is shared by CompassControl.xaml and the corresponding C# file, CompassControl.xaml.cs.

The compass control consists of four images:

  • Compass plate (compass-plate.png)
  • Bearing scale (scale.png)
  • Shadow of the bearing scale (scale-shadow.png)
  • Compass needle (compass-needle.png)

The images are positioned, not by using any specific layout, but just defining the proper margins:

<Grid x:Name="LayoutRoot" Background="Transparent">
<Image x:Name="Plate" Source="/Assets/Graphics/compass-plate.png" Width="290" Height="600" Stretch="Uniform" />
<Image x:Name="ScaleShadow" Source="/Assets/Graphics/scale-shadow.png" Width="276" Height="276" Margin="0, 190, 0, 0" Stretch="Uniform" />
<Image x:Name="Scale" Source="/Assets/Graphics/scale.png" Width="276" Margin="0, 190, 0, 0" Stretch="Uniform" />
<Image x:Name="Needle" Source="/Assets/Graphics/compass-needle.png" Width="21" Margin="0, 190, 0, 0" Stretch="Uniform" />
</Grid>

The result is displayed in the following image:

Compass-control.png

Scaling the control

Defining dependencies, in respect to size, is not straightforward in XAML code because of the lack of arithmetic clauses i.e. defining that the size of an Image element A is half of an image B is not easy. Thus, the easiest approach, defining the relative sizes with C# code, was chosen. All the images that we need to reference in C# code have been named in the XAML file with x:Name property. We use those names in C#:

// Constants
private const int PlateNativeWidth = 290;
private const int ScaleNativeWidth = 276;
private const int NeedleNativeWidth = 21;
 
...
 
private void SetRelativeSize(float relativeSize)
{
...
Plate.Width = relativeSize * PlateNativeWidth;
ScaleShadow.Width = relativeSize * ScaleNativeWidth;
Scale.Width = relativeSize * ScaleNativeWidth;
Needle.Width = relativeSize * NeedleNativeWidth;
...

To keep the components in their proper places we also scale the margins:

// Constants
private const int PlateNativeHeight = 600;
private const float ScaleRelativeTopMargin = 0.32f;
 
...
 
Thickness topMargin = new Thickness();
topMargin.Top = relativeSize * PlateNativeHeight * ScaleRelativeTopMargin;
ScaleShadow.Margin = topMargin;
Scale.Margin = topMargin;
Needle.Margin = topMargin;


Defining touch areas

The following image represents the touch areas:

Compass-control-areas.png


The compass control can be manipulated several ways:

  • The whole object can be rotated by moving it by the top (blue area)
  • The whole object can be dragged on the screen from its center and bottom (green area)
  • The bearing scale can be separately rotated (red area)
  • (The compass needle is rotated based on the readings of the compass sensor)


An enumeration for the touch areas and a helper method for determining based on the touch point coordinates is implemented in C# code:

public enum CompassControlArea
{
None = 0,
PlateTop = 1,
PlateCenter = 2,
Scale = 3,
PlateBottom = 4
};
 
...
 
private CompassControlArea AreaAt(double x, double y, UIElement container)
{
if (container != null)
{
if (container == Plate)
{
if (y < _plateManipulationBottom)
{
return CompassControlArea.PlateTop;
}
 
if (y >= _plateManipulationBottom && y < _scaleManipulationTop)
{
return CompassControlArea.PlateCenter;
}
 
if (y > _scaleManipulationBottom)
{
return CompassControlArea.PlateBottom;
}
}
else if (container == Scale)
{
return CompassControlArea.Scale;
}
}
 
return CompassControlArea.None;
}

_plateManipulationBottom and _scaleManipulationTop are class members of type double. Their values are defined in SetRelativeSize() method since their values depend of the size of the compass control. The enumeration value returned by the AreaAt() method is also stored and it can be queried via the public property getter CompassControl.ManipulatedArea.

Rotating the control

Making objects rotatable in XAML is easy. First you should define the origin for the rotation with UIElement.RenderTransformOrigin. Then add UIElement.RenderTransform and RotateTransform to the element. Here's how the bearing scale component of the compass control is defined in the XAML file:

<Image x:Name="Scale" Source="/Assets/Graphics/scale.png" Width="276" Margin="0, 190, 0, 0" Stretch="Uniform" RenderTransformOrigin="0.5, 0.5">
<UIElement.RenderTransform>
<RotateTransform x:Name="ScaleRotation" CenterX="0.5" CenterY="0.5" Angle="0" />
</UIElement.RenderTransform>
</Image>

The coordinates [0.5, 0.5] set to RenderTransformOrigin define that the scale rotates in respect to its center point and stays in the same position when rotated.

The logic for allowing the user rotate the component with touch events is implemented in C#:

// Constants
private const double RadiansToDegreesCoefficient = 57.2957795; // 180 / PI
 
// Members
private CompassControlArea _manipulatedArea = CompassControlArea.None;
private double _previousX = 0;
private double _previousY = 0;
 
...
 
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
_previousX = e.ManipulationOrigin.X;
_previousY = e.ManipulationOrigin.Y;
_manipulatedArea = AreaAt(_previousX, _previousY, e.ManipulationContainer);
}
 
protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
if (_manipulatedArea == CompassControlArea.PlateTop)
{
...
}
else if (_manipulatedArea == CompassControlArea.Scale)
{
double x = e.ManipulationOrigin.X;
double y = e.ManipulationOrigin.Y;
 
double centerX = Scale.ActualWidth / 2;
double centerY = Scale.ActualHeight / 2;
double deltaX = x - centerX;
double deltaY = y - centerY;
double previousDeltaX = _previousX - centerX;
double previousDeltaY = _previousY - centerY;
 
int angleDelta = -(int)Math.Round((Math.Atan2(previousDeltaY, previousDeltaX) - Math.Atan2(deltaY, deltaX)) * RadiansToDegreesCoefficient);
 
if (angleDelta > 180)
{
angleDelta -= 360;
}
else if (angleDelta < -180)
{
angleDelta += 360;
}
 
ScaleRotation.Angle = (ScaleRotation.Angle + angleDelta) % 360;
}
}
 
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
{
_manipulatedArea = CompassControlArea.None;
}

The same method is used for rotating the whole compass control. For the complete source code of the control, see CompassControl.xaml and CompassControl.xaml.cs.

Making the control draggable

The compass control should not care about its position in respect to its parent view. Therefore, the dragging is implemented outside the control itself, in this case in a PhoneApplicationPage. The compass control has to be declared inside a component that allows it to be moved. In the Compass application the control is inside a Grid which is the layout root. In the MainPage.xaml of the application:

<Grid x:Name="LayoutRoot" Background="Transparent">
...
<!-- Compass control is the visible compass item -->
<src:CompassControl x:Name="CompassControl"
Width="{Binding ElementName=CompassControl, Path=PlateWidth}" Height="{Binding ElementName=CompassControl, Path=PlateHeight}" />
...
</Grid>

The size of the control is bound to the public properties PlateWidth and PlateHeight.

The position of the compass control on the screen is modified using the Margin property. One trick here is that we want to get the touch coordinates in respect to the MainPage instead of the compass control. This is because the control returns the coordinates relative to its top-left corner. In normal situations we could use them but since the control could be rotated, and when rotated the coordinates are still in respect to top-left corner, we would have to map them according to the angle of the control. The mapping is complex and adds unnecessary operations which could affect the performance of the application. Luckily we can tell the framework who we want to handle the touch events. This is done in MainPage.xaml.cs:

// Members
private double _previousX = 0;
private double _previousY = 0;
private double _startCompassX = 0;
private double _startCompassY = 0;
private bool _compassBeingMoved = false;
 
...
 
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
Compass.Ui.CompassControl.CompassControlArea area = CompassControl.ManipulatedArea;
 
if (area == Compass.Ui.CompassControl.CompassControlArea.PlateCenter
|| area == Compass.Ui.CompassControl.CompassControlArea.PlateBottom)
{
// The touched area is on the compass
 
// This page has to manage the manipulation of the compass position because even when
// the compass has been rotated, the manipulation coordinates are not.
e.ManipulationContainer = this;
 
Thickness margin = CompassControl.Margin;
_startCompassX = margin.Left;
_startCompassY = margin.Top;
 
_previousX = -1;
_previousY = -1;
 
_compassBeingMoved = true;
e.Handled = true;
}
...

The OnManipulationDelta() method then receives the touch points in respect to the MainPage and moves the compass control on the screen accordingly:

protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
{
if (_compassBeingMoved)
{
double x = e.ManipulationOrigin.X;
double y = e.ManipulationOrigin.Y;
 
double diffX = 0;
double diffY = 0;
 
if (_previousX == -1 && _previousY == -1)
{
ManipulationDelta delta = e.DeltaManipulation;
_previousX = x - delta.Scale.X;
_previousY = y - delta.Scale.Y;
}
 
diffX = _previousX - x;
diffY = _previousY - y;
_previousX = x;
_previousY = y;
 
Thickness margin = CompassControl.Margin;
margin.Top -= diffY;
margin.Left -= diffX;
CompassControl.Margin = margin;
 
e.Handled = true;
}
...

Full source code available in Projects

Compass-example-screenshot.png

Get the source code of the full application Compass project.

This page was last modified on 22 October 2013, at 08:51.
81 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.

×