Vibration Composer

0
232

Vibration Composer is probably the strangest app in this part of the book. A cross between a musical instrument and a handheld massager, this app provides an interesting way to create your own custom vibrating patterns.

Users can do some fun things with this app:

  • Create a solid vibration to use it like a massager app.
  • Set the phone on a table while it vibrates and watch it move!
  • Play music from the Music + Videos hub and then use this app to accompany it with vibrations.

This app requires the running-while-the-phone-is-locked capability (automatically granted during the marketplace certification process).Therefore, this app’s marketplace listing will contain the awkward phrase,“This app makes use of your phone’s RunsUnderLock.” (The exact phrase depends on whether you look in Zune or the phone’s Marketplace app, and may change in the future.)

The Main Page

Vibration Composer’s main page represents time in square chunks. Each chunk lasts 1/10th of a second. You can tap any square to toggle it between an activated state and a deactivated state. When you press the app’s start button, it highlights each square in sequence and makes the phone vibrate when an activated square is highlighted. Once it reaches the last activated square, it repeats the sequence from the beginning. Therefore, although the page contains 90 squares (enabling the unique pattern to be up to 9 seconds long), the user is in control over the length of the segment to be repeated. Figure 8.1 demonstrates three simple vibration patterns.

FIGURE 8.1 The pattern to be repeated is automatically trimmed after the last activated square.
FIGURE 8.1 The pattern to be repeated is automatically trimmed after the last activated square.

The User Interface

Listing 8.1 contains the XAML for the main page.

LISTING 8.1 MainPage.xaml—The User Interface for Vibration Composer’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar, with three buttons and five menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”start”
IconUri=”/Shared/Images/appbar.play.png” Click=”StartStopButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”solid”/>
<shell:ApplicationBarMenuItem Text=”alternating fast”/>
<shell:ApplicationBarMenuItem Text=”alternating slow”/>
<shell:ApplicationBarMenuItem Text=”sos”/>
<shell:ApplicationBarMenuItem Text=”phone ring”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<ScrollViewer>
<Grid>
<!– A wrap panel for containing the 90 buttons –>
<toolkit:WrapPanel x:Name=”WrapPanel”/>
<!– A separate rectangle for tracking the current position –>
<Rectangle x:Name=”CurrentPositionRectangle” Visibility=”Collapsed”
Width=”56” Height=”56” Margin=”12” Opacity=”.8”
HorizontalAlignment=”Left” VerticalAlignment=”Top”
Fill=”{StaticResource PhoneForegroundBrush}”/>
</Grid>
</ScrollViewer>
</phone:PhoneApplicationPage>

[/code]

Notes:

  • The first application bar button starts and stops the vibration sequence playback, the second button clears the sequence, and the third button navigates to an instructions page. Each of the menu items, shown in Figure 8.2, fills the page with a predefined pattern to help familiarize users with the app and give them ideas for getting interesting results.
  • This page uses a panel from the Silverlight for Windows Phone Toolkit known as a wrap panel to contain each of the squares. (The squares are added in code-behind.) A wrap panel stacks its children from left to right, and then wraps them from top to bottom when it runs out of room. (Or you can change the wrap panel’s Orientation property to Vertical to make it stack its children from top to bottom and then wrap them from left to right.) Figure 8.3 demonstrates how the wrap panel changes the layout of the squares based on the current orientation. In the portrait orientation, 6 squares fit in each row before wrapping to the next row. In the landscape orientations, 9 squares fit before wrapping. The reason this app uses a total of 90 squares is that it happens to be divisible by 6 and 9, avoiding a partial row of squares at the end.
    FIGURE 8.2 The application bar menu provides access to five predefined patterns.
    FIGURE 8.2 The application bar menu provides access to five predefined patterns.

    FIGURE 8.3 The wrap panel reflows the squares based on the current orientation.
    FIGURE 8.3 The wrap panel reflows the squares based on the current orientation.
  • The wrap panel is placed in a grid so a rectangle tracking the current position can be moved by code-behind to overlap each square when appropriate. This grid is placed inside a scroll viewer, so the user can access all 90 squares.

The Code-Behind

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

LISTING 8.2 MainPage.xaml.cs—The Code-Behind for Vibration Composer’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Devices;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// The squares
Button[] buttons = new Button[90];
// Remember the current sequence
Setting<IList<bool>> savedSequence = new Setting<IList<bool>>(“Sequence”,
new List<bool>());
DispatcherTimer timer = new DispatcherTimer();
int sequencePosition;
int sequenceEndPosition = -1;
bool isRunning = false;
IApplicationBarIconButton startStopButton;
public MainPage()
{
InitializeComponent();
this.startStopButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
// Initialize the timer
this.timer.Interval = TimeSpan.FromSeconds(.1);
this.timer.Tick += Timer_Tick;
// Attach the same Click handler to all menu items in the application bar
foreach (IApplicationBarMenuItem menuItem in this.ApplicationBar.MenuItems)
menuItem.Click += MenuItem_Click;
// Fill the wrap panel with the 90 buttons
for (int i = 0; i < this.buttons.Length; i++)
{
this.buttons[i] = new Button {
// Each button contains a square, invisible (Fill=null) when off and
// accent-colored when on
Content = new Rectangle { Margin = new Thickness(0, 7, 0, 5),
Width = 30, Height = 30 }
};
this.buttons[i].Click += Button_Click;
this.WrapPanel.Children.Add(this.buttons[i]);
}
TrimSequence();
// Allow the app to run (and vibrate) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Persist the current sequence
this.savedSequence.Value.Clear();
for (int i = 0; i <= this.sequenceEndPosition; i++)
this.savedSequence.Value.Add(this.buttons[i].Tag != null);
// Prevent this from running while instructions are shown
Stop();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the saved sequence, if any
for (int i = 0; i < this.savedSequence.Value.Count; i++)
{
if (this.savedSequence.Value[i])
TurnOn(this.buttons[i]);
}
TrimSequence();
}
// Click handler for each of the 90 buttons
void Button_Click(object sender, RoutedEventArgs e)
{
// Toggle the state of this button
Button b = sender as Button;
if (b.Tag != null)
TurnOff(b);
else
TurnOn(b);
TrimSequence();
}
void TurnOn(Button b)
{
b.Tag = true; // This button is “on”
(b.Content as Rectangle).Fill =
Application.Current.Resources[“PhoneAccentBrush”] as Brush;
}
void TurnOff(Button b)
{
b.Tag = null; // This button is “off”
(b.Content as Rectangle).Fill = null;
}
void TrimSequence()
{
// Find the end of the sequence (the last “on” button)
// and make the remaining buttons dim (opacity of .2)
this.sequenceEndPosition = -1;
for (int i = this.buttons.Length – 1; i >= 0; i–)
{
this.buttons[i].Opacity = 1;
if (this.sequenceEndPosition == -1)
{
if (this.buttons[i].Tag == null)
this.buttons[i].Opacity = .2;
else
this.sequenceEndPosition = i;
}
}
if (this.isRunning && this.sequenceEndPosition == -1)
{
// Force the playback to stop, because a sequenceEndPosition of -1
// means that the sequence is empty (all buttons are off)
StartStopButton_Click(this, EventArgs.Empty);
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Find the wrap-panel-relative location of the current button
Point buttonLocation = this.buttons[this.sequencePosition]
.TransformToVisual(this.WrapPanel).Transform(new Point(0, 0));
// Move the current position rectangle to overlap the current button
this.CurrentPositionRectangle.Margin = new Thickness(
buttonLocation.X + 12, buttonLocation.Y + 12, 0, 0);
// Either start or stop vibrating, based on the state of the current button
if (this.buttons[this.sequencePosition].Tag != null)
VibrateController.Default.Start(TimeSpan.FromSeconds(.5));
else
VibrateController.Default.Stop();
// Advance the current position, and make it loop indefinitely
this.sequencePosition = (this.sequencePosition + 1)
% (this.sequenceEndPosition + 1);
}
void Stop()
{
this.timer.Stop();
VibrateController.Default.Stop();
this.startStopButton.IconUri = new Uri(“/Shared/Images/appbar.play.png”,
UriKind.Relative);
this.startStopButton.Text = “start”;
this.CurrentPositionRectangle.Visibility = Visibility.Collapsed;
this.isRunning = false;
}
void Clear()
{
for (int i = 0; i < this.buttons.Length; i++)
TurnOff(this.buttons[i]);
}
// Application bar handlers
void StartStopButton_Click(object sender, EventArgs e)
{
if (this.isRunning)
{
// Stop and restore all state
Stop();
return;
}
if (this.sequenceEndPosition == -1)
{
MessageBox.Show(“The vibration pattern is empty! You must turn at least “
+ “one square on.”, “Empty Sequence”, MessageBoxButton.OK);
return;
}
// Start
this.startStopButton.IconUri =
new Uri(“/Shared/Images/appbar.stop.png”, UriKind.Relative);
this.startStopButton.Text = “stop”;
this.CurrentPositionRectangle.Visibility = Visibility.Visible;
this.sequencePosition = 0;
this.isRunning = true;
this.timer.Start();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear the whole sequence?”,
“Delete Sequence”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Clear();
TrimSequence();
}
}
void MenuItem_Click(object sender, EventArgs e)
{
Clear();
// Grab the text from the menu item to determine the chosen sequence
switch ((sender as IApplicationBarMenuItem).Text)
{
case “solid”:
TurnOn(this.buttons[0]);
break;
case “alternating fast”:
TurnOn(this.buttons[1]);
break;
case “alternating slow”:
TurnOn(this.buttons[6]);
TurnOn(this.buttons[7]);
break;
case “sos”:
TurnOn(this.buttons[10]);
TurnOn(this.buttons[11]);

break;
case “phone ring”:
TurnOn(this.buttons[17]);
TurnOn(this.buttons[21]);

break;
}
TrimSequence();
}
}
}

[/code]

Notes:

  • The squares used by this app are actually normal buttons whose content is a rectangle element. These are created in the page’s constructor and added to the wrap panel.
  • The setting of ApplicationIdleDetectionMode to Disabled allows the app to continue running even if the user locks their screen. (This is the only kind of background running your app can do in version 7.0 of Windows Phone. Actual multitasking is coming by the end of 2011.) This enables the phone to be used as, say, a massager, without needing to keep the screen active and worrying about accidentally tapping things.

When you make your app run under the lock screen, consider making it optional by providing a setting that toggles the value of ApplicationIdleDetectionMode.Unfortunately, although setting it to Disabled takes effect immediately, setting it to Enabled once it’s in this state does not work.Therefore, you must explain to your users that toggling the run-underlock feature requires restarting your app, at least in one direction.

For a brief period of time, the Windows Phone Marketplace required that any app that sets ApplicationIdleDetectionMode to Disabled must provide a way for users to set it to Enabled. For now, this is no longer a requirement.

  • The state of each button is stored in its Tag property. In addition to updating the fill of the rectangle inside the relevant button, TurnOn sets the button’s Tag to true, whereas TurnOff sets it to null. Tag can be set to any object, and it’s up to you how you want to use the values. It’s just a handy spot to store your own data. Using Tag is considered to be a bit of a hack, but it’s a simple solution. Instead of using it in this case, many Silverlight developers might use the Visual State Manager, covered in Chapter 19, “Animation Lab,” to define two distinct states for each button.
  • A timer keeps track of where we are when playing the sequence of vibrations, much like the timers used in Chapter 2, “Flashlight.” The Tick event handler, Timer_Tick, either starts or stops the phone’s vibration with VibrateController.Default.Start and VibrateController.Default.Stop (from the Microsoft.Devices namespace). You can call Start with a time span of up to 5 seconds. Although longer time spans aren’t supported, using a timer enables you to combine shorter vibrations into an arbitrarily-long one.

    Given that the timer interval is 1/10th of a second, a more straightforward implementation of Timer_Tick would make the phone vibrate for 1/10th of a second if the current square is on, otherwise do nothing:

    [code]if (this.buttons[this.sequencePosition].Tag != null)
    VibrateController.Default.Start(TimeSpan.FromSeconds(.1));
    else { /* Nothing to do! */ }[/code]

    However, this can cause small gaps of silence when consecutive squares are on. Because I wanted to enable long stretches of vibration with consecutive “on” squares, Timer_Tick instead starts the vibration for longer than necessary (half a second) or stops the vibration each tick. (Calling Start while the phone is already vibrating is fine; the vibration will simply continue for the length of time specified.)

How does the phone’s vibration on/off setting impact vibrations triggered by my app?

It has no impact.The phone’s vibration setting (found under “ringtones & sounds” in the Settings app) only controls whether incoming phone calls can vibrate.When you use VibrateController, it always makes the phone vibrate. Furthermore, you have no way to discover the phone’s vibration setting, so you can’t manually respect this setting even if you want to.

  • Inside Timer_Tick, the rectangle that highlights the current position is moved to completely overlap the appropriate button. This is done with the handy TransformToVisual method (available on all UI elements), which provides a way to map any point relative to one element into coordinates relative to another element. Previously, we have done this by calling GetPosition on a MouseEventArgs instance, but we have no such instance at this point in the code. The awkward thing about TransformToVisual (in addition to its name) is that it returns an instance of a GeneralTransform object. From this object, you must then call Transform to map one point to another.
  • The timer is stopped when the user navigates away from this page. This is done to avoid a problem when navigating to the instructions page while the timer is running. The Timer_Tick event would continue to be called, but the call to TransformToVisual fails when main page is no longer the active page.

The Instructions Page

Listing 8.3 contains the XAML for the simple instructions page shown in Figure 8.4.

FIGURE 8.4 The instructions page used by Vibration Composer.
FIGURE 8.4 The instructions page used by Vibration Composer.

LISTING 8.3 InstructionsPage.xaml—The User Interface for the Instructions Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.InstructionsPage”
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” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header, with some tweaks –>
<StackPanel Margin=”24,16,0,12”>
<TextBlock Text=”VIBRATION COMPOSER” Margin=”-1,0,0,0”
FontFamily=”{StaticResource PhoneFontFamilySemiBold}”
FontSize=”{StaticResource PhoneFontSizeMedium}”/>
<TextBlock Text=”instructions” Margin=”-3,-10,0,0”
FontFamily=”{StaticResource PhoneFontFamilySemiLight}”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<TextBlock Margin=”24,12” TextWrapping=”Wrap”>
Tap the squares to toggle them on and off. …
<LineBreak/><LineBreak/>
When you’re ready to start the vibration, tap the “play” button. …
<LineBreak/><LineBreak/>
The end of the sequence is always the last “on” square. …
<LineBreak/><LineBreak/>
Tap “…” to access a menu of sample sequences.
</TextBlock>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • This page also supports all orientations, and uses a scroll viewer to ensure that all the text can be read regardless of orientation.
  • The code-behind file, InstructionsPage.xaml.cs, has nothing more than the call to InitializeComponent in the class’s constructor.

Listing 8.3 has something new inside its text block: LineBreak elements! You can use LineBreak to insert a carriage return in the middle of a single text block’s text.This only works when the text is specified as the inner content of the element. It cannot be used when assigning the Text attribute to a string.

The Finished Product

Vibration Composer