Reflex Test (Single Touch)

0
303

Reflex Test is a simple game in which you see how quickly you can tap the screen when a target appears. (You can tap anywhere on the screen; the target is just a visual gimmick.) The app keeps track of your fastest time as well as your average time.

The tap detection done by this app could easily be done with a simple MouseLeftButtonDown event handler, but instead this app serves as an introduction to multi-touch functionality. You can easily use the multi-touch infrastructure for single touches, as this app does.

The User Interface

Reflex Test has a main page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 37.1 contains the XAML for the main page.

Figure 37.1 illustrates main page’s user interface during the four stages of the reflex-testing process.

The main page goes through four stages as the app is used.
FIGURE 37.1 The main page goes through four stages as the app is used.

LISTING 37.1 MainPage.xaml—The User Interface for Reflex Test’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”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<!– Add three animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– Slide the best time out then back in –>
<Storyboard x:Name=”SlideBestTimeStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”BestTimeTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”BestTimeTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the time is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Slide the average time out then back in –>
<Storyboard x:Name=”SlideAvgTimeStoryboard”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName=”AvgTimeTransform”
Storyboard.TargetProperty=”TranslateX”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”0”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.4” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:.4” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.8” Value=”0”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName=”AvgTimeTextBlock”
Storyboard.TargetProperty=”Visibility”>
<!– Ensure the time is visible on the way in,
even if collapsed on the way out –>
<DiscreteObjectKeyFrame KeyTime=”0:0:.4” Value=”Visible”/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<!– Animate in (then out) a message, which will either say
“CONGRATULATIONS!” or “TOO EARLY!” –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”50”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”50”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with two buttons and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”Transparent”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– A target vector graphic and ellipse to mark where the screen
was touched, collapsed until appropriate to show –>
<Grid Grid.RowSpan=”2”>
<Grid x:Name=”TargetGrid” Visibility=”Collapsed”>
<Ellipse Width=”440” Height=”440” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse Width=”300” Height=”300” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse Width=”160” Height=”160” StrokeThickness=”35”
Stroke=”{StaticResource PhoneAccentBrush}”/>
<Ellipse x:Name=”TouchEllipse” Visibility=”Collapsed” Opacity=”.9”
Fill=”{StaticResource PhoneForegroundBrush}” Width=”100”
Height=”100” HorizontalAlignment=”Left” VerticalAlignment=”Top”/>
</Grid>
</Grid>
<!– Show indeterminate progress (dancing dots) while waiting for the target
to appear (IsIndeterminate is set from code-behind for perf reasons) –>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”
VerticalAlignment=”Top”/>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”REFLEX TEST”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock x:Name=”PageTitle” Text=”tap to begin”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<Grid Grid.Row=”1” Margin=”{StaticResource PhoneHorizontalMargin}”>
<!– A display for the best time, average time, and # of tries –>
<StackPanel x:Name=”TimesPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right”>
<TextBlock Text=”BEST TIME” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestTimeTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,30”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestTimeTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgTimeHeaderTextBlock” Text=”AVG TIME”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgTimeTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-15,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgTimeTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– A “CONGRATULATIONS!” or “TOO EARLY!” message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The first two animations are used to slide in the best time and average time text blocks the first time they are displayed, and to slide them out and then in when they are updated. The last animation slides a message onto and then off the screen: “CONGRATULATIONS!” when the user gets a new best time, or “TOO EARLY!” when the user taps the screen before the target appears.

The Code-Behind

Listing 37.2 contains the code-behind for the main page.

LISTING 37.2 MainPage.xaml.cs—The Code-Behind for Reflex Test’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings
Setting<TimeSpan> bestTime = new Setting<TimeSpan>(“BestTime”,
TimeSpan.MaxValue);
Setting<TimeSpan> avgTime = new Setting<TimeSpan>(“AvgTime”,
TimeSpan.MaxValue);
Setting<int> numTries = new Setting<int>(“NumTries”, 0);
DispatcherTimer timer = new DispatcherTimer();
Random random = new Random();
DateTime beginTime;
DateTime targetShownTime;
bool tapToBegin;
public MainPage()
{
InitializeComponent();
this.timer.Tick += Timer_Tick;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
// Respect the persisted values
UpdateLabels(true);
// Reset
this.tapToBegin = true;
this.PageTitle.Text = “tap to begin”;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
TouchPoint point = e.GetPrimaryTouchPoint(this);
if (point != null && point.Action == TouchAction.Down)
{
if (this.tapToBegin)
{
// Get started
this.tapToBegin = false;
this.PageTitle.Text = “”;
this.TargetGrid.Visibility = Visibility.Collapsed;
this.TouchEllipse.Visibility = Visibility.Collapsed;
// Show the indeterminate progress bar
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Visible;
this.beginTime = DateTime.Now;
// Make the target appear between .5 sec and 7 sec from now
timer.Interval = TimeSpan.FromMilliseconds(random.Next(500, 7000));
timer.Start();
}
else if (this.TargetGrid.Visibility == Visibility.Visible)
{
// The target has been tapped
DateTime endTime = DateTime.Now;
// Position and show the ellipse where the screen was touched
this.TouchEllipse.Margin = new Thickness(
point.Position.X – this.TouchEllipse.Height / 2,
point.Position.Y – this.TouchEllipse.Height / 2, 0, 0);
this.TouchEllipse.Visibility = Visibility.Visible;
// Show the elapsed time
TimeSpan newTime = endTime – this.targetShownTime;
this.PageTitle.Text = newTime.TotalSeconds + “ sec”;
this.tapToBegin = true;
// Record this attempt and update the UI
double oldTotal = this.avgTime.Value.TotalSeconds * this.numTries.Value;
// New average
this.avgTime.Value = TimeSpan.FromSeconds(
(oldTotal + newTime.TotalSeconds) / (this.numTries.Value + 1));
// New total number of tries
this.numTries.Value++;
if (newTime < this.bestTime.Value)
{
// New best time
this.bestTime.Value = newTime;
UpdateLabels(true);
// Animate in a congratulations message
this.MessageTextBlock.Text = “CONGRATULATIONS!”;
this.ShowMessageStoryboard.Begin();
}
else
{
UpdateLabels(false);
}
}
else
{
// The screen has been tapped too early
// Cancel the timer that would show the target
this.timer.Stop();
// Hide the progress bar and turn off the indeterminate
// animation to avoid poor performance
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false;
// Show exactly how early the tap was
DateTime endTime = this.beginTime + this.timer.Interval;
this.PageTitle.Text = (DateTime.Now – endTime).TotalSeconds + “ sec”;
this.tapToBegin = true;
// Animate in an explanatory message
this.MessageTextBlock.Text = “TOO EARLY!”;
this.ShowMessageStoryboard.Begin();
}
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Show the target
this.TargetGrid.Visibility = Visibility.Visible;
// Hide the progress bar and turn off the indeterminate
// animation to avoid poor performance
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false;
this.targetShownTime = DateTime.Now;
// We only want the Tick once
this.timer.Stop();
}
void UpdateLabels(bool animateBestTime)
{
if (this.numTries.Value > 0)
{
// Ensure the panel is visible and update the text blocks
this.TimesPanel.Visibility = Visibility.Visible;
this.BestTimeTextBlock.Text = this.bestTime.Value.TotalSeconds + “ sec”;
this.AvgTimeTextBlock.Text = this.avgTime.Value.TotalSeconds + “ sec”;
if (this.numTries.Value == 1)
this.AvgTimeHeaderTextBlock.Text = “AVG TIME (1 TRY)”;
else
this.AvgTimeHeaderTextBlock.Text = “AVG TIME (“ + this.numTries.Value
+ “ TRIES)”;
// Animate the textblocks out then in. The animations take care of
// showing the textblocks if they are collapsed.
this.SlideAvgTimeStoryboard.Begin();
if (animateBestTime)
this.SlideBestTimeStoryboard.Begin();
else
this.BestTimeTextBlock.Visibility = Visibility.Visible;
}
else
{
// Hide everything
this.TimesPanel.Visibility = Visibility.Collapsed;
this.BestTimeTextBlock.Visibility = Visibility.Collapsed;
this.AvgTimeTextBlock.Visibility = Visibility.Collapsed;
}
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Reflex Text”, UriKind.Relative));
}
}
}

[/code]

  • The event for capturing touch and multi-touch activity has the odd name FrameReported and is exposed on a static class called Touch (from the System.Windows.Input namespace). This event gets raised for touch activity across the entire application, so Listing 37.1 attaches a handler to this event in OnNavigatedTo but removes the handler in OnNavigatedFrom.
  • Inside the FrameReported event handler (Touch_FrameReported), GetPrimaryTouchPoint is called to get single-touch data, which is all this app cares about. If multiple fingers are pressed on the screen, GetPrimaryTouchPoint returns data about the first finger that makes contact with the screen.
  • FrameReported gets raised for three types of actions: a finger making contact with the screen, a finger moving on the screen, and a finger being released from the screen. These actions are analogous to mouse down, mouse move, and mouse up events, although here they apply per finger. This app only cares about taps, so the logic inside Touch_FrameReported only runs when the primary touch point is a touching-down action.

Be sure to detach any FrameReported handlers as soon as possible!

In addition to the performance implication of FrameReported handlers being invoked when they don’t need to be, forgetting to detach from the event can cause other problems if the page containing the handler is no longer active.

  • In addition to the Action property, the TouchPoint class returned by GetPrimaryTouchPoint exposes Position and Size properties. This app only makes use of the position of the touch point in order to place TouchEllipse in the spot that was tapped.
  • Notice that the progress bar’s IsIndeterminate property is only set to true while it is visible.

You should ignore TouchPoint’s Size property!

The current version of Windows Phone doesn’t actually support the discovery of a touch point’s size, so the reported Size.Width and Size.Height are always 1. This is unfortunate, as it would be a nice touch (no pun intended) for Reflex Test to make TouchEllipse the size of the fingertip that touched the screen.

The Finished Product

Reflex Test (Single Touch)