Blograby

Jigsaw Puzzle (Drag Gesture & WriteableBitmap)

Jigsaw Puzzle enables you to turn any picture into a challenging 30-piece jigsaw puzzle. You can use one of the included pictures, or choose a photo from your camera or pictures library. You can even zoom and crop the photo to get it just right. Drag pieces up from a scrollable tray at the bottom of the screen and place them where you think they belong. As you drag a piece, it snaps to each of the 30 possible correct positions to reduce your frustration when arranging pieces. Jigsaw Puzzle also can solve the puzzle for you, or reshuffle the pieces, both with fun animations.

Other than the instructions page, Jigsaw Puzzle contains a main page and a page for cropping an imported picture. This app leverages gesture listener’s drag events for a few different reasons. On the main page, dragging is used for moving puzzle pieces and for scrolling the tray of unused pieces at the bottom of the screen. On the page for cropping imported pictures, dragging is used to pan the picture.

Are you thinking of ways to increase the difficulty of the puzzles in this app? Although the puzzle pieces would become too difficult to drag if you make them much smaller (without also enabling zooming), you could enable pieces to be rotated.The next chapter demonstrates how to implement a rotation gesture.

The Main Page

Jigsaw Puzzle’s main page contains the 30 pieces arranged in 6 rows of 5. Each piece is a 96×96 canvas that contains a vector drawing represented as a Path element. 14 distinct shapes are used (4 if you consider rotated/flipped versions as equivalent), shown in Figure 42.1.

FIGURE 42.1 The 14 shapes consist of 4 corner pieces, 8 edge pieces, and 2 middle pieces.

Each piece is actually larger than 96 pixels in at least one dimension, which is fine because each Path can render outside the bounds of its parent 96×96 canvas. Each Path is given an appropriate offset inside its parent canvas to produce the appropriate interlocking pattern, as illustrated in Figure 42.2. Every puzzle presented by this app uses these exact 30 pieces in the exact same spots; only the image on the pieces changes.

The choice of a vector-based path to represent each piece is important because it enables the nonrectangular shapes to interlock and retain precise hit-testing. If puzzle-pieceshaped images were instead used as an opacity mask on rectangular elements, the bounding box of each piece would respond to gestures on the entire area that overlaps the bounding box of any pieces underneath. This would cause the wrong piece to move in many areas of the puzzle. The use of paths also enables us to apply a custom stroke to each piece to highlight its edges.

The User Interface

Listing 42.1 contains the XAML for the main page.

FIGURE 42.2 The 30 vector-based shapes, each shown with its parent canvas represented as a yellow square outline.

LISTING 42.1 MainPage.xaml—The User Interface for Jigsaw Puzzle’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=”Portrait”>
<!– Listen for drag events anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DragStarted=”GestureListener_DragStarted”
DragDelta=”GestureListener_DragDelta”
DragCompleted=”GestureListener_DragCompleted”/>
</toolkit:GestureService.GestureListener>
<!– The application bar, with 4 buttons and 5 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5” ForegroundColor=”White”
BackgroundColor=”#443225”>
<shell:ApplicationBarIconButton Text=”picture”
IconUri=”/Shared/Images/appbar.picture.png” Click=”PictureButton_Click”/>
<shell:ApplicationBarIconButton Text=”start over”
IconUri=”/Shared/Images/appbar.delete.png” Click=”StartOverButton_Click”/>
<shell:ApplicationBarIconButton Text=”solve” IsEnabled=”False”
IconUri=”/Images/appbar.solve.png” Click=”SolveButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cat and fish”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”city”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”statue of liberty”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”traffic”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”under water”
Click=”ApplicationBarMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Prevent off-screen pieces from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<Canvas Background=”#655”>
<!– The tray at the bottom –>
<Rectangle x:Name=”Tray” Fill=”#443225” Width=”480” Height=”224”
Canvas.Top=”576”/>
<!– All 30 pieces placed where they belong –>
<Canvas x:Name=”PiecesCanvas”>
<!– Row 1 –>
<Canvas Width=”96” Height=”96”>
<Path Data=”F1M312.63,0L385.7,0C385.48,…” Height=”129” Stretch=”Fill”
Width=”96” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”/>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
<Canvas Canvas.Left=”96” Width=”96” Height=”96”>
<Path Data=”F1M25.12,909.28C49.47,909.27,…” Height=”96”
Canvas.Left=”-33” Stretch=”Fill” Width=”162” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”>
<ImageBrush.Transform>
<TranslateTransform X=”-63”/>
</ImageBrush.Transform>
</ImageBrush>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
… 27 pieces omitted …
<Canvas Canvas.Left=”384” Canvas.Top=”480” Width=”96” Height=”96”>
<Path Data=”F1M777.45,0L800.25,0C802.63,…” Canvas.Left=”-33”
Height=”96” Stretch=”Fill” Width=”129” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”>
<ImageBrush.Transform>
<TranslateTransform X=”-351” Y=”-480”/>
</ImageBrush.Transform>
</ImageBrush>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
</Canvas>
<!– The image without visible piece boundaries, shown when solved –>
<Image x:Name=”CompleteImage” Visibility=”Collapsed” IsHitTestVisible=”False”
Stretch=”None”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

My favorite way to create vector artwork based on an illustration is with Vector Magic (http://vectormagic.com). It’s not free, but it does a fantastic job of converting image files to a variety of vector formats. If you download the result as a PDF file and then rename the file extension to .ai, you can import it into Expression Blend, which converts it to XAML.

Fortunately for apps such as Jigsaw Puzzle, using many image brushes that point to the same image is efficient. Silverlight shares the underlying image rather than creating a separate copy for each brush.

FIGURE 42.3 Once the puzzle is solved, the puzzle piece borders are no longer visible.

The Code-Behind

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

LISTING 42.2 MainPage.xaml.cs—The Code-Behind for Jigsaw Puzzle’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
bool isDraggingTray;
bool isDraggingPiece;
double cumulativeDeltaX;
double cumulativeDeltaY;
int topmostZIndex;
List<FrameworkElement> piecesOnTray = new List<FrameworkElement>();
Random random = new Random();
IApplicationBarIconButton solveButton;
public MainPage()
{
InitializeComponent();
this.solveButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Persist the offset currently being applied to each piece, so
// they can appear in the same locations next time
Settings.PieceOffsets.Value.Clear();
foreach (FrameworkElement piece in this.PiecesCanvas.Children)
{
Settings.PieceOffsets.Value.Add(new Point(
(piece.RenderTransform as CompositeTransform).TranslateX,
(piece.RenderTransform as CompositeTransform).TranslateY));
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
RefreshPuzzleImage();
bool arePiecesCorrect = false;
if (Settings.PieceOffsets.Value.Count == this.PiecesCanvas.Children.Count)
{
// Restore the persisted position of each piece
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
t.TranslateX = Settings.PieceOffsets.Value[i].X;
t.TranslateY = Settings.PieceOffsets.Value[i].Y;
}
arePiecesCorrect = AreAllPiecesCorrect();
}
else
{
// This is the first run. After a 1-second delay, animate the pieces
// from their solved positions to random positions on the tray.
DispatcherTimer timer = new DispatcherTimer {
Interval = TimeSpan.FromSeconds(1) };
timer.Tick += delegate(object sender, EventArgs args)
{
StartOver();
timer.Stop();
};
timer.Start();
}
if (arePiecesCorrect)
ShowAsSolved();
else
ShowAsUnsolved();
}
// The three drag event handlers
void GestureListener_DragStarted(object sender, DragStartedGestureEventArgs e)
{
// Determine if we’re dragging the tray, a piece, or neither
FrameworkElement source = e.OriginalSource as FrameworkElement;
if (source == this.Tray)
{
// An empty spot on the tray is being dragged
if (e.Direction == System.Windows.Controls.Orientation.Horizontal)
this.isDraggingTray = true;
return;
}
FrameworkElement piece = GetPieceFromDraggedSource(source);
if (piece == null)
return;
if (e.Direction == System.Windows.Controls.Orientation.Horizontal &&
GetPieceTop(piece) > Constants.ON_TRAY_Y)
{
// Although a piece is being dragged, the piece is on the tray and the
// drag is horizontal, so consider this to be a tray drag instead
this.isDraggingTray = true;
}
else
{
this.isDraggingPiece = true;
// A piece is being dragged, so record its pre-drag position
CompositeTransform t = piece.RenderTransform as CompositeTransform;
this.cumulativeDeltaX = t.TranslateX;
this.cumulativeDeltaY = t.TranslateY;
}
}
void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
if (this.isDraggingTray)
{
// Scroll the tray
ScrollTray(e.HorizontalChange);
}
else if (this.isDraggingPiece)
{
FrameworkElement piece = GetPieceFromDraggedSource(
e.OriginalSource as FrameworkElement);
if (piece == null)
return;
CompositeTransform t = piece.RenderTransform as CompositeTransform;
// Apply the position change caused by dragging.
// We’re keeping track of the total change from DragStarted so the piece
// remains in the right spot after repeated snapping and unsnapping.
this.cumulativeDeltaX += e.HorizontalChange;
this.cumulativeDeltaY += e.VerticalChange;
t.TranslateX = this.cumulativeDeltaX;
t.TranslateY = this.cumulativeDeltaY;
// Ensure that this piece is on top of all others
this.topmostZIndex++;
Canvas.SetZIndex(piece, this.topmostZIndex);
// Ensure that the puzzle is no longer in the solved state
ShowAsUnsolved();
// If the piece is not on the tray, snap it to a solved horizontal
// and/or vertical boundary if it’s close enough
double left = GetPieceLeft(piece);
double top = GetPieceTop(piece);
if (top > Constants.ON_TRAY_Y)
return; // The piece is on the tray, so never mind
// Snapping to a horizontal boundary
if (left % Constants.PIECE_WIDTH < Constants.SNAPPING_MARGIN)
t.TranslateX -= left % Constants.PIECE_WIDTH;
else if (left % Constants.PIECE_WIDTH >
Constants.PIECE_WIDTH – Constants.SNAPPING_MARGIN)
t.TranslateX += Constants.PIECE_WIDTH – left % Constants.PIECE_WIDTH;
// Snapping to a vertical boundary
if (top % Constants.PIECE_HEIGHT < Constants.SNAPPING_MARGIN)
t.TranslateY -= top % Constants.PIECE_HEIGHT;
else if (top % Constants.PIECE_HEIGHT >
Constants.PIECE_HEIGHT – Constants.SNAPPING_MARGIN)
t.TranslateY += Constants.PIECE_HEIGHT – top % Constants.PIECE_HEIGHT;
}
}
void GestureListener_DragCompleted(object sender,
DragCompletedGestureEventArgs e)
{
// Give the tray an extra push (simulating inertia) based on
// the final dragging horizontal velocity
if (this.isDraggingTray && e.HorizontalVelocity != 0)
ScrollTray(e.HorizontalVelocity / 10);
this.isDraggingTray = this.isDraggingPiece = false;
if (AreAllPiecesCorrect())
ShowAsSolved();
}
FrameworkElement GetPieceFromDraggedSource(FrameworkElement source)
{
// When a piece is dragged, the source is the path,
// but we want to return its parent canvas
if (source == null || source.Parent == null ||
(source.Parent as FrameworkElement).Parent == null ||
(source.Parent as FrameworkElement).Parent != this.PiecesCanvas)
return null;
else
return source.Parent as FrameworkElement;
}
double GetPieceTop(FrameworkElement piece)
{
return Canvas.GetTop(piece) +
(piece.RenderTransform as CompositeTransform).TranslateY;
}
double GetPieceLeft(FrameworkElement piece)
{
return Canvas.GetLeft(piece) +
(piece.RenderTransform as CompositeTransform).TranslateX;
}
void ScrollTray(double amount)
{
// Retrieve the minimum and maximum horizontal positions among all
// pieces in the tray, to provide bounds on how far it can scroll
double minX = double.MaxValue;
double maxX = double.MinValue;
this.piecesOnTray.Clear();
foreach (FrameworkElement piece in this.PiecesCanvas.Children)
{
if (GetPieceTop(piece) > Constants.ON_TRAY_Y)
{
this.piecesOnTray.Add(piece);
double left = GetPieceLeft(piece);
if (left < minX) minX = left;
if (left > maxX) maxX = left;
}
}
if (this.piecesOnTray.Count == 0)
return;
// Change the amount if it would make the tray scroll too far
if (amount < 0 && (maxX + amount < this.ActualWidth –
Constants.MAX_PIECE_WIDTH || minX < Constants.NEGATIVE_SCROLL_BOUNDARY))
amount = Math.Max(-maxX + this.ActualWidth – Constants.MAX_PIECE_WIDTH,
Constants.NEGATIVE_SCROLL_BOUNDARY – minX);
if (amount > 0 && minX + amount > Constants.TRAY_LEFT_MARGIN)
amount = Constants.TRAY_LEFT_MARGIN – minX;
// “Scroll” the tray by moving each piece on the tray the same amount
foreach (FrameworkElement piece in this.piecesOnTray)
(piece.RenderTransform as CompositeTransform).TranslateX += amount;
}
// Move each piece to the tray in a random order
void StartOver()
{
// Copy the children to an array so their order
// in the collection is preserved
UIElement[] pieces = this.PiecesCanvas.Children.ToArray();
// Shuffle the children in place
for (int i = pieces.Length – 1; i > 0; i–)
{
int r = this.random.Next(0, i);
// Swap the current child with the randomly-chosen one
UIElement temp = pieces[i]; pieces[i] = pieces[r]; pieces[r] = temp;
}
// Now move the pieces to the bottom in their random order
for (int i = 0; i < pieces.Length; i++)
{
UIElement piece = pieces[i];
// Alternate the pieces between two rows
CreatePieceMovingStoryboard(piece, TimeSpan.Zero, TimeSpan.FromSeconds(1),
(i % 2 * Constants.TRAY_2ND_ROW_HORIZONTAL_OFFSET) +
(i / 2) * Constants.TRAY_HORIZONTAL_SPACING – Canvas.GetLeft(piece),
(i % 2 * Constants.TRAY_VERTICAL_SPACING) + Constants.TRAY_TOP_MARGIN
– Canvas.GetTop(piece)).Begin();
// Reset the z-index of each piece
Canvas.SetZIndex(piece, 0);
}
this.topmostZIndex = 0;
ShowAsUnsolved();
The Main Page 925
}
// Create a storyboard that animates the piece to the specified position
Storyboard CreatePieceMovingStoryboard(UIElement piece, TimeSpan beginTime,
TimeSpan duration, double finalX, double finalY)
{
DoubleAnimation xAnimation = new DoubleAnimation { To = finalX,
Duration = duration, EasingFunction = new QuinticEase() };
DoubleAnimation yAnimation = new DoubleAnimation { To = finalY,
Duration = duration, EasingFunction = new QuinticEase() };
Storyboard.SetTargetProperty(xAnimation, new PropertyPath(“TranslateX”));
Storyboard.SetTargetProperty(yAnimation, new PropertyPath(“TranslateY”));
Storyboard storyboard = new Storyboard { BeginTime = beginTime };
Storyboard.SetTarget(storyboard, piece.RenderTransform);
storyboard.Children.Add(xAnimation);
storyboard.Children.Add(yAnimation);
return storyboard;
}
bool AreAllPiecesCorrect()
{
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
if (t.TranslateX != 0 || t.TranslateY != 0)
return false; // This piece is in the wrong place
}
// All pieces are in the right place
return true;
}
void ShowAsSolved()
{
this.solveButton.IsEnabled = false;
int piecesToMove = 0;
Storyboard storyboard = null;
// For any piece that’s out of place, animate it to the solved position
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
if (t.TranslateX == 0 && t.TranslateY == 0)
continue; // This piece is already in the right place
// Animate it to a (0,0) offset, which is its natural position
storyboard = CreatePieceMovingStoryboard(piece,
TimeSpan.FromSeconds(.3 * piecesToMove), // Spread out the animations
TimeSpan.FromSeconds(1), 0, 0);
storyboard.Begin();
// Ensure each piece moves on top of pieces already in the right place
this.topmostZIndex++;
Canvas.SetZIndex(piece, this.topmostZIndex);
piecesToMove++;
}
if (storyboard == null)
{
// Everything is in the right place
this.CompleteImage.Visibility = Visibility.Visible;
}
else
{
// Delay the showing of CompleteImage until the last storyboard
// has completed
storyboard.Completed += delegate(object sender, EventArgs e)
{
// Ensure that the user didn’t unsolve the puzzle during the animation
if (!this.solveButton.IsEnabled)
this.CompleteImage.Visibility = Visibility.Visible;
};
}
}
void ShowAsUnsolved()
{
this.solveButton.IsEnabled = true;
this.CompleteImage.Visibility = Visibility.Collapsed;
}
void RefreshPuzzleImage()
{
ImageSource imageSource = null;
// Choose the right image based on the setting
switch (Settings.PhotoIndex.Value)
{
// The first case is for a custom photo saved
// from CroppedPictureChooserPage
case -1:
try { imageSource = IsolatedStorageHelper.LoadFile(“custom.jpg”); }
catch { imageSource = new BitmapImage(new Uri(“Images/catAndFish.jpg”,
UriKind.Relative)); }
break;
// The remaining cases match the indices in the application bar menu
case 0:
imageSource = new BitmapImage(new Uri(“Images/catAndFish.jpg”,
UriKind.Relative));
break;
case 1:
imageSource = new BitmapImage(new Uri(“Images/city.jpg”,
UriKind.Relative));
break;
case 2:
imageSource = new BitmapImage(new Uri(“Images/statueOfLiberty.jpg”,
UriKind.Relative));
break;
case 3:
imageSource = new BitmapImage(new Uri(“Images/traffic.jpg”,
UriKind.Relative));
break;
case 4:
imageSource = new BitmapImage(new Uri(“Images/underWater.jpg”,
UriKind.Relative));
break;
}
if (imageSource != null)
{
this.CompleteImage.Source = imageSource;
// Each of the 30 pieces needs to be filled with the right image
foreach (Canvas piece in this.PiecesCanvas.Children)
((piece.Children[0] as Shape).Fill as ImageBrush).ImageSource =
imageSource;
}
}
// Application bar handlers
void PictureButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/CroppedPictureChooserPage.xaml”,
UriKind.Relative));
}
void StartOverButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to dismantle the puzzle and “ +
“start from scratch?”, “Start over”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
StartOver();
}
void SolveButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Do you give up? Are you sure you want the puzzle to “
+ “be solved for you?”, “Solve”, MessageBoxButton.OKCancel)
!= MessageBoxResult.OK)
return;
ShowAsSolved();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void ApplicationBarMenuItem_Click(object sender, EventArgs e)
{
for (int i = 0; i < this.ApplicationBar.MenuItems.Count; i++)
{
// Set the persisted photo index to match the menu item index
if (sender == this.ApplicationBar.MenuItems[i])
Settings.PhotoIndex.Value = i;
}
RefreshPuzzleImage();
}
}
}

[/code]

FIGURE 42.4 The puzzle pieces are arranged on the tray once they animate away from their solved positions.

The Direction property passed to drag events never changes until a new drag is initiated!

Although the Direction property exposed to DragStarted handlers is also exposed to DragDelta and DragCompleted handlers, its value never changes until the drag has completed and a new drag has started.This is true even if the actual direction of the finger motion changes to be completely vertical instead of horizontal, or vice versa.This makes it easy to implement panning or other motion that is locked to one axis, although it also means that detecting more flexible motion requires you to interpret the finger motion manually.

In Jigsaw Puzzle, this fact can cause frustration if a user tries to drag a piece from the tray directly to its final position, yet the straight-line path is more horizontal than it is vertical.To help combat this, the instructions page explains that pieces must be dragged upward to leave the tray.

The HorizontalChange and VerticalChange properties exposed by DragDelta are relative to the previous raising of DragDelta!

Unlike PinchGestureEventArgs, whose DistanceRatio and TotalAngleDelta properties are relative to the values when pinching or stretching started, the HorizontalChange and VerticalChange properties exposed by DragDeltaGestureEventArgs and DragCompletedGestureEventArgs do not accumulate as dragging proceeds.

FIGURE 42.5 The automatic solving animation makes the pieces float into place according to their order in the canvas.

The Cropped Picture Chooser Page

Because the photo chooser can sometimes be slow to launch, and because decoding the chosen picture can be slow, this page shows a “loading” message during these actions. Figure 42.6 demonstrates the user flow through this page.

FIGURE 42.6 The sequence of events when navigating to the cropped photo chooser page.

The User Interface

Listing 42.3 contains the XAML for this page.

LISTING 42.3 CroppedPictureChooserPage.xaml—The User Interface for Jigsaw Puzzle’s Cropped Picture Chooser Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CroppedPictureChooserPage”
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}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– The 2-button application bar, shown on return from the PhotoChooserTask –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”0” IsVisible=”False” ForegroundColor=”White”>
<shell:ApplicationBarIconButton Text=”done”
IconUri=”/Shared/Images/appbar.done.png” Click=”DoneButton_Click”/>
<shell:ApplicationBarIconButton Text=”cancel”
IconUri=”/Shared/Images/appbar.cancel.png” Click=”CancelButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Listen for drag events anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DragDelta=”GestureListener_DragDelta”
PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”/>
</toolkit:GestureService.GestureListener>
<!– Prevent a zoomed-in photo from making the screen go blank –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<Canvas Background=”#443225”>
<!– Shown on return from the PhotoChooserTask –>
<Canvas x:Name=”CropPanel” Visibility=”Collapsed”>
<!– Designate the puzzle boundary –>
<Rectangle Fill=”White” Canvas.Top=”112” Width=”480” Height=”576”/>
<!– The canvas provides screen-centered zooming –>
<Canvas Canvas.Top=”112” Width=”480” Height=”576”
RenderTransformOrigin=”.5,.5”>
<Canvas.RenderTransform>
<!– For zooming –>
<CompositeTransform x:Name=”CanvasTransform”/>
</Canvas.RenderTransform>
<Image x:Name=”Image” Stretch=”None” CacheMode=”BitmapCache”>
<Image.RenderTransform>
<!– For panning –>
<CompositeTransform x:Name=”ImageTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– Top and bottom borders that let the image show through slightly –>
<Rectangle Opacity=”.8” Fill=”#443225” Width=”480” Height=”112”/>
<Rectangle Canvas.Top=”688” Opacity=”.8” Fill=”#443225” Width=”480”
Height=”112”/>
<!– The title and instructions –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”CROP PICTURE” Foreground=”White”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Foreground=”White” TextWrapping=”Wrap” Width=”432”>
Pinch &amp; stretch your fingers to zoom.<LineBreak/>
Drag to move the picture.
</TextBlock>
</StackPanel>
</Canvas>
<!– Shown while launching the PhotoChooserTask –>
<Canvas x:Name=”LoadingPanel”>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”LoadingTextBlock” Text=”LOADING…” Foreground=”White”
Style=”{StaticResource PhoneTextTitle0Style}” Width=”432”
TextWrapping=”Wrap”/>
</StackPanel>
</Canvas>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

FIGURE 42.7 A white rectangle serves as the puzzle’s background if the picture doesn’t completely cover it.

If an image has transparent regions, those regions are not hit-testable when used to fill a shape!

This app ensures that the picture used to fill the puzzle pieces never contains any transparency. Those transparent regions would not respond to any touch events,making it difficult or impossible to drag the affected pieces.Note that this is different behavior compared to giving an otherwise- rectangular element transparent regions with an opacity mask. With an opacity mask, the transparent regions are still hit-testable (just like an element marked with an opacity of 0).

The Code-Behind

Listing 42.4 contains the code-behind for this page.

LISTING 42.4 CroppedPictureChooserPage.xaml.cs—The Code-Behind for Jigsaw Puzzle’s Cropped Picture Chooser Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Phone;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class CroppedPictureChooserPage : PhoneApplicationPage
{
bool loaded;
double scaleWhenPinchStarted;
public CroppedPictureChooserPage()
{
InitializeComponent();
this.Loaded += CroppedPictureChooserPage_Loaded;
}
void CroppedPictureChooserPage_Loaded(object sender, RoutedEventArgs e)
{
if (this.loaded)
return;
this.loaded = true;
// When navigating to this page in the forward direction only (from the
// main page or when reactiving the app), launch the photo chooser task
Microsoft.Phone.Tasks.PhotoChooserTask task = new PhotoChooserTask();
task.ShowCamera = true;
task.Completed += delegate(object s, PhotoResult args)
{
if (args.TaskResult == TaskResult.OK)
{
WriteableBitmap imageSource = PictureDecoder.DecodeJpeg(
args.ChosenPhoto);
// Perform manual “uniform to fill” scaling by choosing the larger
// of the two scales that make the image just fit in one dimension
double scale = Math.Max(
(double)Constants.PUZZLE_WIDTH / imageSource.PixelWidth,
(double)Constants.PUZZLE_HEIGHT / imageSource.PixelHeight);
this.CanvasTransform.ScaleX = this.CanvasTransform.ScaleY = scale;
// Center the image in the puzzle
this.ImageTransform.TranslateY =
-(imageSource.PixelHeight – Constants.PUZZLE_HEIGHT) / 2;
this.ImageTransform.TranslateX =
-(imageSource.PixelWidth – Constants.PUZZLE_WIDTH) / 2;
// Show the cropping user interface
this.Image.Source = imageSource;
this.LoadingPanel.Visibility = Visibility.Collapsed;
this.CropPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = true;
}
else
{
// The user cancelled from the photo chooser, but we can’t automatically
// navigate back right here, so update “LOADING…” with instructions
this.LoadingTextBlock.Text =
“Press the Back button again to return to the puzzle.”;
}
};
task.Show();
}
// Raised for single-finger dragging
void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
// Pan the image based on the drag
this.ImageTransform.TranslateX +=
e.HorizontalChange / this.CanvasTransform.ScaleX;
this.ImageTransform.TranslateY +=
e.VerticalChange / this.CanvasTransform.ScaleY;
}
// Raised when two fingers touch the screen (likely to begin a pinch/stretch)
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
this.scaleWhenPinchStarted = this.CanvasTransform.ScaleX;
}
// Raised continually as either or both fingers move
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// The distance ratio is always relative to when the pinch/stretch started,
// so be sure to apply it to the ORIGINAL zoom level, not the CURRENT
double scale = this.scaleWhenPinchStarted * e.DistanceRatio;
this.CanvasTransform.ScaleX = this.CanvasTransform.ScaleY = scale;
}
// Application bar handlers
void DoneButton_Click(object sender, EventArgs e)
{
// Create a new bitmap with the puzzle’s dimensions
WriteableBitmap wb = new WriteableBitmap(Constants.PUZZLE_WIDTH,
Constants.PUZZLE_HEIGHT);
// Render the page’s contents to the puzzle, but shift it upward
// so only the region intended to be for the puzzle is used
wb.Render(this, new TranslateTransform { Y = -112 });
// We must explicitly tell the bitmap to draw its new contents
wb.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
wb.SaveJpeg(stream, Constants.PUZZLE_WIDTH, Constants.PUZZLE_HEIGHT,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Save the file to isolated storage.
// This overwrites the file if it already exists.
IsolatedStorageHelper.SaveFile(“custom.jpg”, stream);
}
// Indicate that the user has chosen to use a custom image
Settings.PhotoIndex.Value = -1;
// Return to the puzzle
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
void CancelButton_Click(object sender, EventArgs e)
{
// Don’t do anything, just return to the puzzle
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}

[/code]

FIGURE 42.8 If the page is rendered to the puzzle image from its top-left corner, the page header becomes part of the puzzle!

If you want to take a screenshot of your app for your marketplace submission on a real phone rather than the emulator, you can temporarily put code in your page that captures the screen and saves it to your pictures library as follows:

[code]

void CaptureScreen()
{
// Create a new bitmap with the page’s dimensions
WriteableBitmap wb = new WriteableBitmap((int)this.ActualWidth,
(int)this.ActualHeight);
// Render the page’s contents with no transform applied
wb.Render(this, null);
// We must explicitly tell the bitmap to draw its new contents
wb.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
wb.SaveJpeg(stream, (int)this.ActualWidth, (int)this.ActualHeight,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Requires referencing Microsoft.Xna.Framework.dll
// and the ID_CAP_MEDIALIB capability, and only works
// when the phone is not connected to Zune
new Microsoft.Xna.Framework.Media.MediaLibrary().SavePicture(
“screenshot.jpg”, stream);
}
}

[/code]

Once there, you can sync it to your desktop with Zune to retrieve the photo in its full resolution. This has a few important limitations, however:

Also, you need to determine a way to invoke this code without impacting what you’re capturing.

Listing 42.4 doesn’t make any attempt to preserve the page’s state in the face of deactivation and reactivation; it simply relaunches the photo chooser.There are a few strategies for preserving the state of this page.The most natural would be to persist the image to a separate temporary file in isolated storage, along with values in page state that remember the current zoom and panning values.

What’s the difference between detecting dragging with gesture listener drag events versus using mouse down, mouse move, and mouse up events?

One major difference is that the drag events are only raised when one finger is in contact with the screen. With the mouse events (or with the multi-touch FrameReported event), you can base dragging on the primary finger and simply ignore additional touch points.This may or may not be a good thing, depending on your app. Because the preceding chapter uses the mouse events for panning, it gives the user the ability to do zooming and panning as a combined gesture, which mimics the behavior of the built-in Maps app. In Jigsaw Puzzle’s cropped photo chooser page, on the other hand, the user must lift their second finger if they wish to pan right after zooming the picture.

Another difference is that the drag events are not raised until the gesture listener is sure that a drag is occurring, e.g. one finger has made contact with the screen and has already moved a little bit. In contrast, the mouse move event is raised as soon as the finger moves at all. For Jigsaw Puzzle, the delayed behavior of the drag events is beneficial for helping to avoid accidental dragging.

A clear benefit of the drag events, if applicable to your app, is that the finger velocity at the end of the gesture is exposed to your code.However, you could still get this information when using the mouse approach if you also attach a handler to gesture listener’s Flick event.The Direction property exposed to the drag events, discussed earlier, also enables interesting behavior that is tedious to replicate on your own.

The Finished Product

Jigsaw Puzzle (Drag Gesture & WriteableBitmap)
Exit mobile version