Coin Toss helps you with any heads-or-tails type of decision. Make an upward tossing motion with the phone in your hand, and watch the coin of your choice flip to give you an answer. You can choose between a penny, nickel, dime, or quarter, and see a neat visual history of your coin tosses.
The lesson of this chapter is detecting a tossing/throwing motion with the phone. And surprise, surprise, it’s the same algorithm used in the preceding chapter for detecting punches! (I guess they call it “throwing punches” for a reason!) The only difference is that the Z axis is tracked rather than the X axis.
The Main User Interface
Coin Toss has a main page, a history page, a settings page, and an instructions page. The code for the latter two pages isn’t shown in this chapter. The settings page is actually identical to the settings page from the preceding chapter, except “Required punching strength” is renamed to “Required tossing strength,” and the tip textbox at the bottom is removed.
Listing 45.1 contains the XAML for the main page. It is much more complicated than the preceding chapter’s main page in order to support all the animations done by this app.
LISTING 45.1 MainPage.xaml—The User Interface for Coin Toss’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”>
<!– Add three storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The 3D flip, 90° at a time –>
<Storyboard x:Name=”FlipStoryboard” Storyboard.TargetName=”CoinProjection”
Storyboard.TargetProperty=”RotationX”
Completed=”FlipStoryboard_Completed”>
<DoubleAnimation By=”90” Duration=”0:0:.06”/>
</Storyboard>
<!– The movement up and off the screen –>
<Storyboard x:Name=”RiseAndFallStoryboard”
Storyboard.TargetName=”CoinTransform”>
<!– Moving up then back down –>
<DoubleAnimation x:Name=”RiseAndFallAnimation1” AutoReverse=”True”
Storyboard.TargetProperty=”TranslateY” By=”-300”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Growing then shrinking (X) –>
<DoubleAnimation x:Name=”RiseAndFallAnimation2” AutoReverse=”True”
Storyboard.TargetProperty=”ScaleX” By=”1.05”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Growing then shrinking (Y) –>
<DoubleAnimation x:Name=”RiseAndFallAnimation3” AutoReverse=”True”
Storyboard.TargetProperty=”ScaleY” By=”1.05”>
<DoubleAnimation.EasingFunction>
<QuadraticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Flip in the “HEADS!” or “TAILS!” text in 3D –>
<Storyboard x:Name=”ShowResultStoryboard”
Storyboard.TargetName=”ResultProjection”
Storyboard.TargetProperty=”RotationX”>
<DoubleAnimation To=”0” Duration=”0:0:.4”>
<DoubleAnimation.EasingFunction>
<BackEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with three buttons and four menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click” />
<shell:ApplicationBarIconButton Text=”history”
IconUri=”/Images/appbar.history.png”
Click=”HistoryButton_Click” />
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click” />
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”penny” Click=”CoinMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”nickel” Click=”CoinMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”dime” Click=”CoinMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”quarter” Click=”CoinMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The grid containing the coin –>
<Grid RenderTransformOrigin=”.5,.5”>
<Grid.RenderTransform>
<!– Used to move and scale the coin –>
<CompositeTransform x:Name=”CoinTransform”/>
</Grid.RenderTransform>
<Grid.Projection>
<!– Used to flip the coin in 3D –>
<PlaneProjection x:Name=”CoinProjection”/>
</Grid.Projection>
<!– The tails side of the coin –>
<Image x:Name=”TailsImage” RenderTransformOrigin=”.5,.5”
Stretch=”None”>
<!– Reverse, so it looks correct when flipped over –>
<Image.RenderTransform>
<ScaleTransform ScaleY=”-1”/>
</Image.RenderTransform>
</Image>
<!– The heads side of the coin –>
<Image x:Name=”HeadsImage” Stretch=”None”/>
</Grid>
<!– The “HEADS!” or “TAILS!” text block, which animates independently –>
<TextBlock x:Name=”ResultTextBlock” FontFamily=”Segoe WP Black” FontSize=”120”
Foreground=”{StaticResource PhoneAccentBrush}” HorizontalAlignment=”Center”>
<TextBlock.Projection>
<!– Used to flip the text in 3D –>
<PlaneProjection x:Name=”ResultProjection” RotationX=”90”/>
</TextBlock.Projection>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>
[/code]
- The first two storyboards create the 3D coin-flip effect, pictured in Figure 45.1. The first storyboard does the 3D part, and the second one makes it more realistic by animating the scale and position of the coin while it flips in 3D.
- The final storyboard performs a 3D flip-in animation on the “HEADS!” or “TAILS!” result text, as shown in Figure 45.2.
- The application bar contains buttons for going to each of the other pages, and the application bar menu contains an entry for each of the four available coins, as shown in Figure 45.3. Although the settings page isn’t really important enough to have a prominent spot on the application bar, a button is used rather than a menu item because mixing a settings menu item with the four coin menu items wouldn’t be the cleanest presentation.
The Main Code-Behind
Listing 45.2 contains the code-behind for the main page. It makes use of three persisted settings defined in a separate Settings.cs file as follows:
[code]
public static class Settings
{
public static readonly Setting<string> ChosenCoin =
new Setting<string>(“ChosenCoin”, “penny”);
public static readonly Setting<List<string>> HistoryList =
new Setting<List<string>>(“HistoryList”, new List<string>());
public static readonly Setting<double> Threshold =
new Setting<double>(“Threshold”, 1);
}
[/code]
The Threshold setting is like the one from the preceding chapter, but with a default value of 1 rather than 1.5, so the default strength required for performing a coin toss is less than the default strength required for punching in the Boxing Glove app.
LISTING 45.2 MainPage.xaml.cs—The Code-Behind for Coin Toss’s Main Page
[code]
using System;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Devices.Sensors;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Accelerometer accelerometer;
int animationStep;
int animationTotalSteps;
bool isTails = false;
bool isTossing = false;
DateTimeOffset acceleratingQuicklyForwardTime = DateTime.MinValue;
Random random = new Random();
public MainPage()
{
InitializeComponent();
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer
try
{
this.accelerometer.Start();
}
catch
{
MessageBox.Show(
“Unable to start your accelerometer. Please try running this app again.”,
“Accelerometer Error”, MessageBoxButton.OK);
}
// Restore the chosen coin
this.HeadsImage.Source = new BitmapImage(new Uri(“Images/” +
Settings.ChosenCoin.Value + “Heads.png”, UriKind.Relative));
this.TailsImage.Source = new BitmapImage(new Uri(“Images/” +
Settings.ChosenCoin.Value + “Tails.png”, UriKind.Relative));
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Stop the accelerometer
try
{
this.accelerometer.Stop();
}
catch { /* Nothing to do */ }
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerReadingEventArgs e)
{
// We want the threshold to be negative, so
// forward motion is up and out of the screen
double threshold = -Settings.Threshold.Value;
// Only pay attention to large-enough magnitudes in the Z dimension
if (Math.Abs(e.Z) < Math.Abs(threshold))
return;
// See if the force is in the same direction as the threshold
// (forward throwing motion)
if (e.Z * threshold > 0)
{
// Forward acceleration
this.acceleratingQuicklyForwardTime = e.Timestamp;
}
else if (e.Timestamp – this.acceleratingQuicklyForwardTime
< TimeSpan.FromSeconds(.2))
{
// This is large backward force shortly after the forward force.
// Time to flip the coin!
this.acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
// We’re on a different thread, so transition to the UI thread
this.Dispatcher.BeginInvoke(delegate() { BeginToss(); });
}
}
void BeginToss()
{
if (this.isTossing)
return;
this.isTossing = true;
this.ShowResultStoryboard.Stop();
// Choose heads or tails
bool willBeTails = this.random.Next(0, 2) == 0;
// First randomly choose number of complete 360° flips
// (multiples of 4 because the animation is done 90° at a time)
this.animationTotalSteps = this.random.Next(3, 6) * 4;
if (this.isTails != willBeTails)
{
// It needs to land on the opposite side,
// so add two more animation steps (180°)
this.animationTotalSteps += 2;
}
// Make the duration of the rise-and-fall animations match the length
// of time that the coin will be spinning (+ a .1 second buffer)
this.RiseAndFallAnimation1.Duration =
this.RiseAndFallAnimation2.Duration =
this.RiseAndFallAnimation3.Duration =
TimeSpan.FromSeconds(this.animationTotalSteps * .03 + .1);
this.isTails = willBeTails;
this.animationStep = 0;
// Perform the first 90° of the flip and start the rise-and-fall in sync
this.FlipStoryboard.Begin();
this.RiseAndFallStoryboard.Begin();
}
void FlipStoryboard_Completed(object sender, EventArgs e)
{
this.animationStep++;
if (this.animationStep == this.animationTotalSteps)
{
// We’re done
if (this.isTails)
{
this.ResultTextBlock.Text = “TAILS!”;
Settings.HistoryList.Value.Add(“Images/” +
Settings.ChosenCoin.Value + “Tails.png”);
}
else
{
this.ResultTextBlock.Text = “HEADS!”;
Settings.HistoryList.Value.Add(“Images/” +
Settings.ChosenCoin.Value + “Heads.png”);
}
this.isTossing = false;
this.ShowResultStoryboard.Begin();
return;
}
// Each complete rotation has 4 phases (0°, 90°, 180°, 270°)
int phase = this.animationStep % 4;
// Check for 90° or 270°, the two points where the coin is
// perpendicular to the screen (and therefore invisible)
if (phase == 1 || phase == 3)
{
// This is when we toggle the visible image between heads and tails
// by showing/hiding the heads image on top of the tails image
if (this.HeadsImage.Visibility == Visibility.Collapsed)
this.HeadsImage.Visibility = Visibility.Visible;
else
this.HeadsImage.Visibility = Visibility.Collapsed;
}
this.FlipStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void HistoryButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/HistoryPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void CoinMenuItem_Click(object sender, EventArgs e)
{
IApplicationBarMenuItem menuItem = sender as IApplicationBarMenuItem;
Settings.ChosenCoin.Value = menuItem.Text;
this.HeadsImage.Source = new BitmapImage(new Uri(“Images/” + menuItem.Text
+ “Heads.png”, UriKind.Relative));
this.TailsImage.Source = new BitmapImage(new Uri(“Images/” + menuItem.Text
+ “Tails.png”, UriKind.Relative));
}
}
}
[/code]
- The interaction with the accelerometer is identical to the preceding chapter, except that e.Z is used instead of e.X. Notice that the threshold is made negative, which seems to contradict the description of the Z axis in Figure 44.1 from the preceding chapter. That’s because when you hold a phone flat and accelerate it upward, it “feels heavier” in the opposite direction, causing the value of e.Z to decrease. You experience the same sensation when going up in a fast-moving elevator. The same behavior applies to either direction in any dimension. This is the difference between measuring g-forces (what accelerometers do) versus measuring acceleration.
- The project includes image files named pennyHeads.png, pennyTails.png, nickelHeads.png, nickelTails.png, and so on. This is why the code is able to take the text from the menu item (or the string from the ChosenCoin setting) and simply prepend it to Heads.png or Tails.png.
- The URI string from the chosen heads or tails coin image is added to the history list each time. Although this is an odd and inefficient way to store the data, it’s an easy way to get the history page’s experience, shown next.
The History Page
The history page, shown in Figure 45.4, uses a wrap panel from the Silverlight for Windows Phone Toolkit to display miniature versions of past coin results.
The XAML for the history page is shown in Listing 45.3 and its code-behind is in Listing 45.4. The wrap panel is placed inside a scroll viewer so the entire history can be accessed when the coins are wrapped to off-screen rows.
LISTING 45.3 HistoryPage.xaml—The User Interface for Coin Toss’s History Page
[code]
<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.HistoryPage”
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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
SupportedOrientations=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<!– The application bar with a delete button –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png”
Click=”DeleteButton_Click” />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”COIN TOSS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”history”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock Name=”NoItemsTextBlock” Text=”No history. Start tossing!”
Visibility=”Collapsed”
Margin=”22 17 0 0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”
Grid.Row=”1”/>
<ScrollViewer Grid.Row=”1”>
<toolkit:WrapPanel x:Name=”WrapPanel” Margin=”12,0,0,0”/>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>
[/code]
LISTING 45.4 HistoryPage.xaml.cs—The Code-Behind for Coin Toss’s History Page
[code]
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class HistoryPage : PhoneApplicationPage
{
public HistoryPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
foreach (string uri in Settings.HistoryList.Value)
{
this.WrapPanel.Children.Add(new Image {
Source = new BitmapImage(new Uri(uri, UriKind.Relative)),
Height = 90, Margin = new Thickness(12) });
}
if (Settings.HistoryList.Value.Count == 0)
ShowListAsEmpty();
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to clear your coin toss history?”,
“Delete history”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.HistoryList.Value.Clear();
this.WrapPanel.Children.Clear();
ShowListAsEmpty();
}
}
void ShowListAsEmpty()
{
this.NoItemsTextBlock.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
}
}
[/code]
The Finished Product