Blograby

Level (Determining Angle)

No book covering the use of an accelerometer would be complete without showing you how to create a level! This chapter’s Level app not only features four classic tubular bubble levels (one on each edge), but it also shows the current angle of the phone with little accent lines that line up with companion lines when one of the edges of the phone is parallel to the ground. This makes it even easier to visually align the phone exactly as you wish.

Getting smooth, stable results from the accelerometer is important for an app such as this that relies on slow, small movements. Therefore, as in the preceding chapter, Level makes use of Microsoft’s AccelerometerHelper class to perform data smoothing. The key to this app is to use a little bit of trigonometry in order to determine the angle of the phone based on the accelerometer data.

The BubbleWindow User Control

The classic bubble display, shown in Figure 48.1, is implemented as a user control. That’s because Level’s main page uses four instances of this display. Listing 48.1 contains its XAML and Listing 48.2 contains its code-behind.

FIGURE 48.1 The BubbleWindow user control displays a colored bubble inside a marked rectangle.

LISTING 48.1 BubbleWindow.xaml—The User Interface for the BubbleWindow User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.BubbleWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<!– Add one storyboard to the control’s resource dictionary –>
<UserControl.Resources>
<Storyboard x:Name=”BubbleStoryboard” Storyboard.TargetName=”BubbleTransform”>
<!– Stretch the bubble horizontally –>
<DoubleAnimation Storyboard.TargetProperty=”ScaleX” By=”.5”
Duration=”0:0:.8” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Shrink the bubble horizontally –>
<DoubleAnimation Storyboard.TargetProperty=”ScaleY” By=”-.2”
Duration=”0:0:.8” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</UserControl.Resources>
<Canvas Background=”#333”>
<Ellipse x:Name=”Bubble” Width=”115” Height=”115” Visibility=”Collapsed”
Fill=”{StaticResource PhoneAccentBrush}” RenderTransformOrigin=”.5,.5”>
<Ellipse.RenderTransform>
<!– Used in the animations –>
<CompositeTransform x:Name=”BubbleTransform”/>
</Ellipse.RenderTransform>
</Ellipse>
<Line x:Name=”Line1” Stroke=”White”/>
<Line x:Name=”Line2” Stroke=”White”/>
<Rectangle x:Name=”Rectangle” Stroke=”#A555” StrokeThickness=”6”/>
</Canvas>
</UserControl>

[/code]

LISTING 48.2 BubbleWindow.xaml.cs—The Code-Behind for the BubbleWindow User Control

[code]

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
namespace WindowsPhoneApp
{
public partial class BubbleWindow : UserControl
{
public BubbleWindow()
{
InitializeComponent();
this.Loaded += BubbleWindow_Loaded;
}
void BubbleWindow_Loaded(object sender, RoutedEventArgs e)
{
// Adjust the two centered lines and the rectangular border to fit
// the size given to this instance of the control
this.Line1.X1 = this.Line1.X2 =
(this.ActualWidth / 2) – (this.Bubble.ActualWidth / 2);
this.Line1.Y2 = this.ActualHeight;
this.Line2.X1 = this.Line2.X2 =
(this.ActualWidth / 2) + (this.Bubble.ActualWidth / 2);
this.Line2.Y2 = this.ActualHeight;
this.Rectangle.Width = this.ActualWidth;
this.Rectangle.Height = this.ActualHeight;
// Don’t allow the bubble to render past this control’s border
this.Clip = new RectangleGeometry {
Rect = new Rect(0, 0, this.ActualWidth, this.ActualHeight) };
this.Bubble.Visibility = Visibility.Visible;
}
// Set the horizontal position of the bubble on a scale from 0 to 100
public void SetXPercentage(double percentage)
{
percentage = percentage / 100;
double left = (-this.Bubble.ActualWidth/2) + this.ActualWidth*percentage;
Canvas.SetLeft(this.Bubble, left);
}
// Set the vertical position of the bubble on a scale from 0 to 100
public void SetYPercentage(double percentage)
{
percentage = percentage / 100;
// Allow the bubble to go more off-screen with a taller possible range
double range = this.ActualHeight + 20 * 2;
double top = -20 + range * percentage – range / 2;
Canvas.SetTop(this.Bubble, top);
}
public void Animate()
{
// Stretch out the bubble if the animation isn’t already running
if (this.BubbleStoryboard.GetCurrentState() != ClockState.Active)
{
this.BubbleStoryboard.Stop();
this.BubbleStoryboard.Begin();
}
}
}
}

[/code]

This user control has nothing to do with the accelerometer. It simply enables its consumer to do three things:

FIGURE 48.2 The stretched bubble, simulating the expected deformation from fast motion.

Note that the manual layout code in Listing 48.2 that adjusts the lines and rectangle could be eliminated by leveraging a grid’s automatic layout rather than using a canvas.

The Main Page

Besides the same calibration page used by Moo Can and shown in the preceding chapter, Level only has the single main page. Listing 48.3 contains the XAML for the main page, and Listing 48.4 contains its code-behind.

LISTING 48.3 MainPage.xaml—The User Interface for Level’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
SupportedOrientations=”Portrait”>
<Grid>
<Grid.Background>
<!– The metallic background –>
<ImageBrush ImageSource=”Images/background.png”/>
</Grid.Background>
<!– The four lines in the middle that don’t move –>
<Line Y1=”240” Y2=”290” X1=”240” X2=”240” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”510” Y2=”560” X1=”240” X2=”240” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”80” X2=”130” Stroke=”#555” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”350” X2=”400” Stroke=”#555” StrokeThickness=”6”/>
<!– The four lines that tilt based on the phone’s angle –>
<Canvas RenderTransformOrigin=”.5,.5” Width=”480” Height=”800”>
<Canvas.RenderTransform>
<RotateTransform x:Name=”CenterTransform” Angle=”45”/>
</Canvas.RenderTransform>
<Line Y1=”240” Y2=”290” X1=”240” X2=”240”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”510” Y2=”560” X1=”240” X2=”240”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”80” X2=”130”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
<Line Y1=”400” Y2=”400” X1=”350” X2=”400”
Stroke=”{StaticResource PhoneAccentBrush}” StrokeThickness=”6”/>
</Canvas>
<!– The display for the exact angle –>
<TextBlock x:Name=”AngleTextBlock” Foreground=”#555”
HorizontalAlignment=”Center” VerticalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform x:Name=”AngleTextBlockTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– The circle in the center –>
<Ellipse Width=”220” Height=”220” Stroke=”#555” StrokeThickness=”6”/>
<!– The four bubble level displays –>
<local:BubbleWindow x:Name=”TopWindow” Width=”275” Height=”75”
VerticalAlignment=”Top”/>
<local:BubbleWindow x:Name=”BottomWindow” Width=”275” Height=”75”
VerticalAlignment=”Bottom”/>
<local:BubbleWindow x:Name=”LeftWindow” Width=”622” Height=”75”
HorizontalAlignment=”Left” Margin=”-274,0,0,0” RenderTransformOrigin=”.5,.5”>
<local:BubbleWindow.RenderTransform>
<CompositeTransform Rotation=”-90”/>
</local:BubbleWindow.RenderTransform>
</local:BubbleWindow>
<local:BubbleWindow x:Name=”RightWindow” Width=”622” Height=”75”
HorizontalAlignment=”Right” Margin=”0,0,-274,0” RenderTransformOrigin=”.5,.5”>
<local:BubbleWindow.RenderTransform>
<CompositeTransform Rotation=”-90”/>
</local:BubbleWindow.RenderTransform>
</local:BubbleWindow>
<!– The calibrate pseudo-button that tilts based on the phone’s angle –>
<Border Margin=”0,0,0,120” HorizontalAlignment=”Center”
VerticalAlignment=”Bottom” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”Calibrate_MouseLeftButtonUp”>
<TextBlock Text=”calibrate” Padding=”36” Foreground=”#555”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
RenderTransformOrigin=”.5,.5”>
<TextBlock.RenderTransform>
<RotateTransform x:Name=”CalibrateTextBlockTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</Border>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 48.4 MainPage.xaml.cs—The Code-Behind for Level’s Main Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Used to track every 5th call to Accelerometer_ReadingChanged
double readingCount = 0;
// The angle 5 calls ago
double previousAngle = 0;
public const double RADIANS_TO_DEGREES = 180 / Math.PI;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// This is the key formula
double rawAngle =
Math.Atan2(e.AverageAcceleration.X, e.AverageAcceleration.Y)
* RADIANS_TO_DEGREES;
// Express the angle from 0-90°, used by some calculations
double angle0to90 = Math.Abs(rawAngle) % 90;
// Calculate the horizontal % of the left & right bubbles by
// using the angle as an offset from the midpoint (50%)
double landscapeXPercentage = Clamp(Math.Abs(rawAngle) > 90
? 50 + angle0to90
: 50 – (90 – angle0to90), 0, 100);
// The horizontal % of the top & bottom bubbles requires more cases
double portraitXPercentage;
// When the bottom bubble window is on top
if (Math.Abs(rawAngle) <= 25)
portraitXPercentage = Clamp(rawAngle > 0
? 50 – angle0to90 * 2
: 50 + angle0to90 * 2, 0, 100);
// When the top bubble window is on top
else if (Math.Abs(rawAngle) >= 155)
portraitXPercentage = Clamp(rawAngle > 0
? 50 – (90 – angle0to90) * 2
: 50 + (90 – angle0to90) * 2, 0, 100);
// When the right bubble window is on top
else if (rawAngle < 0)
portraitXPercentage = 100;
// When the left bubble window is on top
else
portraitXPercentage = 0;
// The Y% for the left/right bubbles is the same
// as the X% for the top/bottom bubbles
double landscapeYPercentage = portraitXPercentage;
// The Y% for the top/bottom bubbles is the same
// as the inverse of the X% for the left/right bubbles
double portraitYPercentage = 100 – landscapeXPercentage;
this.Dispatcher.BeginInvoke(delegate()
{
// Set the primary (horizontal) position of each bubble
this.TopWindow.SetXPercentage(portraitXPercentage);
this.BottomWindow.SetXPercentage(portraitXPercentage);
this.LeftWindow.SetXPercentage(landscapeXPercentage);
this.RightWindow.SetXPercentage(landscapeXPercentage);
// Set the vertical position of each bubble
this.TopWindow.SetYPercentage(portraitYPercentage);
this.BottomWindow.SetYPercentage(portraitYPercentage);
this.LeftWindow.SetYPercentage(landscapeYPercentage);
this.RightWindow.SetYPercentage(landscapeYPercentage);
// On every 5th call, check to see if significant motion has occurred.
// If the angle has changed by more than 8 degrees, trigger the
// animation on any bubbles that are vertically close to the top
// or bottom of their window (< 10% or > 90%).
readingCount = (readingCount + 1) % 5;
if (readingCount == 0)
{
if (Math.Abs(previousAngle – rawAngle) > 8)
{
if (portraitYPercentage < 10 || portraitYPercentage > 90)
{
this.TopWindow.Animate();
this.BottomWindow.Animate();
}
if (landscapeYPercentage < 10 || landscapeYPercentage > 90)
{
this.LeftWindow.Animate();
this.RightWindow.Animate();
}
}
this.previousAngle = rawAngle;
}
// Update the exact angle display, using values from 0 to 45°
double angle0to45 = angle0to90 <= 45 ? angle0to90 : 90 – angle0to90;
this.AngleTextBlock.Text = angle0to45.ToString(“##0.0°”);
// Tilt the non-bubble pieces of UI accordingly
this.AngleTextBlockTransform.Angle = rawAngle – 180;
this.CalibrateTextBlockTransform.Angle = rawAngle – 180;
this.CenterTransform.Angle = rawAngle – 180;
});
}
void Calibrate_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/Shared/Calibrate/CalibratePage.xaml?” +
“appName=Level&calibrateX=true&calibrateY=true”, UriKind.Relative));
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
}
}

[/code]

FIGURE 48.3 In a right triangle, the tangent of an angle equals the opposite length divided by the adjacent length.

The trigonometric functions in System.Math return angles specified in radians!

This is why Listing 48.4 multiplies the result of Math.Atan2 by the RADIANS_TO_DEGREES constant.This converts the value expressed in radians to a value expressed in degrees by multiplying the value by 180 / π.

FIGURE 48.4 The raw angle reported by Math.Atan2, where each arrow indicates the direction that must point toward the sky.

To get even more stability in its readings, the Level app tweaks the code for AccelerometerHelper to make AverageAcceleration examine the previous 50 samples rather than 25.This was done by simply changing the value of a SamplesCount static variable from 25 to 50. This has the side effect of doubling the latency in the reported data, but this is acceptable for a level app.The latency simply makes the liquid in each bubble window act a little “thicker.”

The Finished Product

Exit mobile version