The Spin the Bottle! app enables you to play the classic kissing game even if you don’t have a bottle handy. In this game, people sit in a circle and take turns spinning the bottle. When someone spins, he or she must kiss whomever the bottle ends up pointing toward. Even if you have no plans to play the game, you could still use this app as a fun time-waster. You could even find work-related applications for this, such as using it to assign tasks to your team members!
This app introduces a new gesture—the rotation gesture— which is a two-finger twist. It also simulates inertia (and friction), so the bottle keeps spinning once the fingers have left the screen and then gradually slows to a halt. The faster the fingers twist before releasing, the longer the bottle will continue to spin. This simulation of what happens in the real world is essential for this kind of app, otherwise the user could control exactly where the bottle would point!
The User Interface
This app contains only one page. Its user interface, shown to the right, is just an image of a bottle surrounded by two text blocks. Listing 43.1 contains the XAML.
LISTING 43.1 MainPage.xaml—The User Interface for Spin the Bottle!’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”Portrait”>
<!– Add two storyboards to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The initial spin to smoothly match the angle when the two fingers first
make contact with the screen –>
<Storyboard x:Name=”SpinStoryboard” Storyboard.TargetName=”BottleTransform”
Storyboard.TargetProperty=”Angle”>
<DoubleAnimation x:Name=”SpinAnimation” Duration=”0:0:.2”/>
</Storyboard>
<!– The inertia-simulating storyboard, whose strength is based on the
velocity of the fingers –>
<Storyboard x:Name=”InertiaStoryboard” Storyboard.TargetName=”BottleTransform”
Storyboard.TargetProperty=”Angle”>
<DoubleAnimation x:Name=”InertiaAnimation”>
<DoubleAnimation.EasingFunction>
<!– Simulates drag –>
<PowerEase Power=”7”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– Allow the rotate gesture anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”
PinchCompleted=”GestureListener_PinchCompleted”/>
</toolkit:GestureService.GestureListener>
<!– The explicit background is important for detecting the gesture anywhere –>
<Canvas Background=”Transparent”>
<!– The title –>
<TextBlock Canvas.Left=”-3” Canvas.Top=”74” FontFamily=”Segoe WP Black”
FontSize=”80” Foreground=”#2EA538” LineHeight=”58”
LineStackingStrategy=”BlockLineHeight”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
SPIN THE<LineBreak/>BOTTLE!
</TextBlock>
<!– Instructions –>
<TextBlock Canvas.Left=”-5” Canvas.Top=”780” FontFamily=”Segoe WP Black”
FontSize=”34” Foreground=”#2EA538” Width=”480” TextWrapping=”Wrap”
TextAlignment=”Right” LineHeight=”26”
LineStackingStrategy=”BlockLineHeight”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-10”/>
</TextBlock.RenderTransform>
DO A TWO-FINGERED SPIN AS FORCEFULLY AS POSSIBLE!
</TextBlock>
<!– The bottle –>
<Image Canvas.Left=”166” Canvas.Top=”160” Source=”Images/bottle.png”
Width=”148” Height=”480” RenderTransformOrigin=”.5,.5”>
<Image.RenderTransform>
<!– The transform’s angle is the target of both storyboards
plus direct manipulation from code-behind –>
<RotateTransform x:Name=”BottleTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>
[/code]
- The two storyboards are customized from code-behind before they are started. The inertia storyboard uses a PowerEase easing function to simulate friction by gradually slowing down the animation toward the end.
- To detect the rotate gesture, this app uses a gesture listener to detect all three pinch/stretch events. After all, a two-finger pinch/stretch gesture is very similar to the desired rotate gesture. The code must simply pay attention to the angle formed by the two fingers rather than the distance between them.
- This is the perfect kind of app for the gesture listener, because there are no controls on the page that could be impacted by its use.
- The root canvas is given a transparent background, which is important for ensuring that the entire screen is hit-testable.
The Code-Behind
Listing 43.2 contains the code-behind for the main page.
LISTING 43.2 MainPage.xaml.cs—The Code-Behind for Spin the Bottle!’s Main Page
[code]
using System;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
double startingAngle;
double previousDelta;
DateTime previousTime;
public MainPage()
{
InitializeComponent();
}
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
// Normalize the current angle, which can get quite large
// or small after the inertia animation
this.BottleTransform.Angle %= 360;
// Reset the velocity-tracking variables
this.previousDelta = 0;
this.previousTime = DateTime.Now;
// Rather than instantly jump to the angle of the fingers, smoothly
// animate to that angle
this.SpinAnimation.To = startingAngle = e.Angle + 90;
this.SpinStoryboard.Begin();
}
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// Directly update the angle of the bottle, as this should be a small
// incremental change
this.BottleTransform.Angle = startingAngle + e.TotalAngleDelta;
// Every 1/10th of a second, record the current delta and time
if ((DateTime.Now – this.previousTime).TotalSeconds > .1)
{
this.previousDelta = e.TotalAngleDelta;
this.previousTime = DateTime.Now;
}
}
void GestureListener_PinchCompleted(object sender, PinchGestureEventArgs e)
{
// Now compare the values from ~.1 second ago to the current values to
// get the rotation velocity at the moment the fingers release the bottle
double distance = e.TotalAngleDelta – this.previousDelta;
double time = (DateTime.Now – this.previousTime).TotalSeconds;
if (distance == 0 || time == 0)
return;
double velocity = distance / time;
// Adjust the inertia animation so the length of the remaining spin
// animation is proportional to the velocity
this.InertiaAnimation.Duration =
TimeSpan.FromMilliseconds(Math.Abs(velocity));
// Choose a number of spins proportional to the length of the animation
this.InertiaAnimation.By = 360 *
Math.Pow(this.InertiaAnimation.Duration.TimeSpan.TotalSeconds, 5);
// Make sure the bottle spins in the appropriate direction
if (velocity < 0)
this.InertiaAnimation.By *= -1;
this.InertiaStoryboard.Begin();
}
}
}
[/code]
- Other than the constructor, the code-behind simply consists of the three pinch/stretch event handlers.
- In GestureListener_PinchStarted, the handy Angle property on PinchStartedGestureEventArgs is used instead of Distance. To make the angle of the bottle match the angle represented by the two fingers, the code could have simply set BottleTransform’s Angle to this value. However, to avoid a jarring experience, this method uses SpinAnimation to quickly animate from the bottle’s current angle to the new angle.
- GestureListener_PinchDelta ignores the familiar DistanceRatio property passed via PinchGestureEventArgs and instead uses its TotalAngleDelta property. Adding this to the angle reported in the PinchStarted event gives an angle that matches the current position of the fingers. (The bottle ends up pointing toward the second of the two fingers.) Note that this method does directly set BottleTransform’s Angle to this value rather than animate it, but this is perfectly acceptable because the angle only changes by a small amount between PinchStarted, PinchDelta, and subsequent PinchDelta events.
- This app’s inertia simulation is different from what was done in the preceding chapter because this app manually calculates the final velocity. Every tenth of a second, GestureListener_PinchDelta records the current time and the current angle delta. When the PinchCompleted event is raised, GestureListener_PinchCompleted again captures the current time and angle delta and then compares them to the previously recorded values to determine the velocity at the point of release. (GestureListener_PinchDelta doesn’t record the values every time because the last angle delta passed to PinchDelta matches the angle delta passed to PinchCompleted, so it would always give a velocity of zero!)
- The absolute velocity value is not very meaningful, but its relative size and direction are. The mapping of the velocity value to InertiaAnimation’s duration and number of spins was derived by trial and error, looking for a realistic final effect.
The Finished Product