Moo Can (Turn Over)

0
237

Do you remember those cans that moo when you turn them upside down? The Moo Can app brings this classic children’s toy back to life in digital form! Moo Can makes a moo sound when you turn your phone upside down. Gravity also affects the cow on the screen, which rotates and falls toward whatever edge of the screen is currently on the bottom.

Just like the real cans, you can shake the phone to make a harsh sound that is different from the desired “moo” caused by gently turning the phone upside down. You can also change the cow to a sheep or cat, each of which makes its own unique sounds.

The Main Page

Moo Can has a main page, a calibration page, and an instructions page (not shown in this chapter). Listing 47.1 contains the XAML for the main page, and Listing 47.2 contains its code-behind.

LISTING 47.1 MainPage.xaml—The User Interface for Moo Can’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”
SupportedOrientations=”Portrait”>
<!– The application bar, with five menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#9CB366” ForegroundColor=”White”>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cow” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”sheep” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”cat” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”calibrate”
Click=”CalibrateMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Canvas>
<Canvas.Background>
<ImageBrush ImageSource=”Images/background.png”/>
</Canvas.Background>
<!– The cow, sheep, or cat –>
<Image x:Name=”AnimalImage” RenderTransformOrigin=”.5,.5”
Canvas.Left=”32” Canvas.Top=”50” Width=”434” Height=”507”>
<Image.RenderTransform>
<CompositeTransform x:Name=”AnimalTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

The user interface contains an animal image (whose source is set in code-behind) placed over the background image. It also uses an application bar menu, shown in Figure 47.1, for switching the animal or navigating to either of the other two pages. The application bar is given hard-coded colors, so it blends in with the grass in the background image when the menu is closed.

The application bar menu on the main page.
FIGURE 47.1 The application bar menu on the main page.

LISTING 47.2 MainPage.xaml.cs—The Code-Behind for Moo Can’s Main Page

[code]

using System;
using System.Windows.Media.Imaging;
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
{
Setting<string> chosenAnimal = new Setting<string>(“ChosenAnimal”, “cow”);
bool upsideDown = false;
// The start, middle, end, and length of the
// vertical path that the animal moves along
const double MAX_Y = 170;
const double MIN_Y = -75;
const double MID_Y = 47.5;
const double LENGTH_Y = 245;
// The start, middle and end of the clockwise rotation of the animal
const double MAX_ANGLE_CW = 180;
const double MIN_ANGLE_CW = 0;
const double MID_ANGLE_CW = 90;
// The start, middle and end of the counter-clockwise rotation of the animal
const double MIN_ANGLE_CCW = -180;
const double MAX_ANGLE_CCW = 0;
const double MID_ANGLE_CCW = -90;
// The length of the rotation, regardless of which direction
const double LENGTH_ANGLE = 180;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
// Allow the app to run (producing sounds) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the chosen animal
ChangeAnimal();
// 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;
}
void ChangeAnimal()
{
switch (this.chosenAnimal.Value)
{
case “cow”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cow.png”, UriKind.Relative));
break;
case “sheep”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/sheep.png”, UriKind.Relative));
break;
case “cat”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cat.png”, UriKind.Relative));
break;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// Transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
// Move the animal vertically based on the vertical force
this.AnimalTransform.TranslateY =
Clamp(MID_Y – LENGTH_Y * e.AverageAcceleration.Y, MIN_Y, MAX_Y);
// Clear the upside-down flag, only when completely upright
if (this.AnimalTransform.TranslateY == MAX_Y)
this.upsideDown = false;
// Rotate the animal to always be upright
if (e.AverageAcceleration.X <= 0)
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CW +
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CW, MAX_ANGLE_CW);
else
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CCW –
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CCW, MAX_ANGLE_CCW);
// Play the appropriate shake sound when shaken
if (ShakeDetection.JustShook(e.OriginalEventArgs))
{
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.MooShake.Play(); break;
case “sheep”: SoundEffects.BaaShake.Play(); break;
case “cat”: SoundEffects.MeowShake.Play(); break;
}
}
// Play the normal sound when first turned upside-down
if (!this.upsideDown && this.AnimalTransform.TranslateY == MIN_Y)
{
this.upsideDown = true;
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.Moo.Play(); break;
case “sheep”: SoundEffects.Baa.Play(); break;
case “cat”: SoundEffects.Meow.Play(); break;
}
}
});
}
// “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));
}
// Application bar handlers
void AnimalMenuItem_Click(object sender, EventArgs e)
{
this.chosenAnimal.Value = (sender as IApplicationBarMenuItem).Text;
ChangeAnimal();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void CalibrateMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/Calibrate/CalibratePage.xaml?appName=Moo Can”, UriKind.Relative));
}
}
}

[/code]

  • This app moves and rotates the animal based on the accelerometer’s data, as shown in Figure 47.2. However, the raw data has a lot of noise, so using it directly would produce a jerky result. (This noise didn’t matter for the previous three apps because the punch, throw, and shake detection are coarse.) Therefore, this listing makes use of an AccelerometerHelper class published by Microsoft that performs data smoothing for you. It also simplifies the starting/stopping interaction with the accelerometer.
The chosen animal rotates and falls as you turn the phone upside down.
FIGURE 47.2 The chosen animal rotates and falls as you turn the phone upside down.
  • AccelerometerHelper exposes its functionality via a static Instance property, so the constructor uses this to attach a handler to its ReadingChanged event. This event is just like the ReadingChanged event from the preceding chapters, but with richer data passed to handlers.
  • This project uses a SoundEffects class just like the one from previous apps but with six properties for six possible sounds: Moo, MooShake, Baa, BaaShake, Meow, and MeowShake.
  • This app runs while locked and this page prevents the screen from auto-locking. This way, you can make the noises without keeping the phone screen on. At the same time, you can continue to watch the cow fall up and down without having to periodically tap the screen to keep it on.
  • To start the accelerometer, OnNavigatedTo simply sets AccelerometerHelper’s Instance’s Active property to true. Internally, this calls Start (if not already started) with the same sort of exception handling done in preceding chapters. This app doesn’t bother stopping the accelerometer, but this could be done by setting Active to false inside OnNavigatedFrom.
  • Inside Accelerometer_ReadingChanged, the AccelerometerHelper-specific AccelerometerHelperReadingEventArgs instance is leveraged to get the average acceleration in the X and Y directions. This class is different from the AccelerometerReadingEventArgs used by the preceding apps. It exposes several properties for getting the data with various types of smoothing applied.

AccelerometerHelperReadingEventArgs exposes four properties that enable you to choose how raw or smooth you want your data to be:

  • RawAcceleration—The same noisy data you would get from the Accelerometer class’s ReadingChanged event (but with calibration potentially applied, as described later in this chapter).
  • LowPassFilteredAcceleration—Applies a first-order low-pass filter over the raw data. The result is smoother data with a little bit of latency.
  • OptimalyFilteredAcceleration [sic]—This uses the same low-pass filter, but only when the current value is close enough to a rolling average value. If the value is sufficiently greater, the raw value is reported instead. (This algorithm is done independently for all three axes.) This gives a nice balance between having smooth results and low latency. Small changes are handled smoothly and large changes are reported quickly.
  • AverageAcceleration—Reports the mean of the most recent 25 data points collected from the “optimally filtered” algorithm.This gives the smoothest result of any of the choices, but it also has the highest latency.

Each property exposes X, Y, Z, and Magnitude properties (but unlike AccelerometerReadingEventArgs, no Timestamp property). Magnitude reports the length of the 3D vector formed by the other three values, which is the square root of X2 + Y2 + Z2. For more details about the algorithms behind these properties, see http://bit.ly/accelerometerhelper.

  • This app uses the same ShakeDetection class shown in the preceding chapter. Because the JustShook method is defined to accept an instance of AccelerometerReadingEventArgs—not AccelerometerHelperReadingEventArgs—this listing uses an OriginalEventArgs property to retrieve it. This property actually isn’t exposed by the AccelerometerHelperReadingEventArgs class; I added it directly to my copy of the source code.
  • A turn upside down is detected by noticing the first moment that the Y acceleration value is large enough after a point in time when it has been small enough. In other words, after the upside-down orientation has been detected, the user must turn the phone right side up to clear the value of upsideDown before another upside-down turn will be detected. Because the animal transform’s TranslateY value is proportional to the Y-axis acceleration (and restricted to a range of MIN_Y to MAX_Y), the phone is completely upside down when TranslateY is MIN_Y and the phone is completely upright when TranslateY is MAX_Y.
  • This app purposely does not make any noise when turned right side up, because a real can does not either. (I didn’t realize this until I bought a few of the cans and tried for myself.)

The Calibration Page

Moo Can enables users to calibrate the accelerometer in case its notion of right side up or upside down are slightly askew from reality. The process of calibration simply involves asking the user when they believe the phone is level, remembering the accelerometer’s reading at that moment, and then using those values as an offset to the accelerometer data from that point onward.

One benefit of using AccelerometerHelper rather than the raw accelerometer APIs is that calibration functionality is built in. It collects the data, stores it as isolated storage application settings (named “AccelerometerCalibrationX” and “AccelerometerCalibrationY”), and automatically offsets the data it returns—even the supposedly “raw” data.

The calibration page, designed to be shared among multiple apps, shows you how to take advantage of it. Listing 47.3 contains the XAML and Listing 47.4 contains the codebehind. It produces the page shown in Figure 47.3.

The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.
FIGURE 47.3 The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.

LISTING 47.3 CalibratePage.xaml—The User Interface for The Accelerometer Calibration Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CalibratePage”
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:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationName”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”calibrate” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel>
<TextBlock Margin=”24” TextWrapping=”Wrap” Text=”Tap the button …”/>
<Button x:Name=”CalibrateButton” Content=”calibrate” IsEnabled=”False”
Height=”150” Click=”CalibrateButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock x:Name=”WarningText” Visibility=”Collapsed” Margin=”24,0”
TextWrapping=”Wrap” FontWeight=”Bold”
Text=”Your phone is not still or level enough!”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 47.4 CalibratePage.xaml.cs—The Code-Behind for the Accelerometer Calibration Page

[code]

using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class CalibratePage : PhoneApplicationPage
{
bool calibrateX = true, calibrateY = true;
public CalibratePage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
// Ensure it is active
AccelerometerHelper.Instance.Active = true;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Set the application name in the header
if (this.NavigationContext.QueryString.ContainsKey(“appName”))
{
this.ApplicationName.Text =
this.NavigationContext.QueryString[“appName”].ToUpperInvariant();
}
// Check for calibration parameters
if (this.NavigationContext.QueryString.ContainsKey(“calibrateX”))
{
this.calibrateX =
bool.Parse(this.NavigationContext.QueryString[“calibrateX”]);
}
if (this.NavigationContext.QueryString.ContainsKey(“calibrateY”))
{
this.calibrateY =
bool.Parse(this.NavigationContext.QueryString[“calibrateY”]);
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
this.Dispatcher.BeginInvoke(delegate()
{
bool canCalibrateX = this.calibrateX &&
AccelerometerHelper.Instance.CanCalibrate(this.calibrateX, false);
bool canCalibrateY = this.calibrateY &&
AccelerometerHelper.Instance.CanCalibrate(false, this.calibrateY);
// Update the enabled state and text of the calibration button
this.CalibrateButton.IsEnabled = canCalibrateX || canCalibrateY;
if (canCalibrateX && canCalibrateY)
this.CalibrateButton.Content = “calibrate (flat)”;
else if (canCalibrateX)
this.CalibrateButton.Content = “calibrate (portrait)”;
else if (canCalibrateY)
this.CalibrateButton.Content = “calibrate (landscape)”;
else
this.CalibrateButton.Content = “calibrate”;
this.WarningText.Visibility = this.CalibrateButton.IsEnabled ?
Visibility.Collapsed : Visibility.Visible;
});
}
void CalibrateButton_Click(object sender, RoutedEventArgs e)
{
if (AccelerometerHelper.Instance.Calibrate(this.calibrateX,
this.calibrateY) ||
AccelerometerHelper.Instance.Calibrate(this.calibrateX, false) ||
AccelerometerHelper.Instance.Calibrate(false, this.calibrateY))
{
// Consider it a success if we were able to
// calibrate in either direction (or both)
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
else
{
MessageBox.Show(“Unable to calibrate. Make sure you’re holding your “ +
“phone still, even when tapping the button!”, “Calibration Error”,
MessageBoxButton.OK);
}
}
}
}

[/code]

  • This page enables calibration in just one dimension—or both—based on the query parameters passed when navigating to it. Listing 47.2 simply passes “appName=Moo Can” as the query string, so this app will enable any kind of calibration. Most likely, the user will be holding the phone in the portrait orientation when attempting to calibrate.
  • The calibration method— AccelerometerHelper.Instance.Calibrate—only succeeds if the phone is sufficiently still and at least somewhat-level in the relevant dimension(s). The AccelerometerHelper.Instance.CanCalibrate method tells you whether calibration will succeed, although the answer can certainly change between the time you call CanCalibrate and the time you call Calibrate, so you should always be prepared for Calibrate to fail.
  • The ReadingChanged event handler continually checks CanCalibrate so it can enable/disable the calibration button appropriately. CanCalibrate has two Boolean parameters that enable you to specify whether you care about just the X dimension, just the Y dimension, or both. (Calibrating the Z axis is not meaningful.) Listing 47.4 checks each dimension individually (if the app cares about the dimension) so it can display a helpful message to the user. The only way you can calibrate both X and Y dimensions simultaneously is by placing the phone flat on a surface parallel to the ground.
  • CalibrateButton_Click tries to calibrate whatever will succeed. Calibrate has the same two parameters as CanCalibrate, so this listing first attempts to calibrate both dimensions, but if that fails (by returning false), it tries to calibrate each dimension individually.

Moo Can (Turn Over)