Jigsaw Puzzle (Drag Gesture & WriteableBitmap)

0
277

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.

The 14 shapes consist of 4 corner pieces, 8 edge pieces, and 2 middle pieces.
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.

The 30 vector-based shapes, each shown with its parent canvas represented as a yellow square outline.
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]

  • A gesture listener is attached to the entire page to listen for the three drag events: DragStarted, DragDelta, and DragCompleted.

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.

  • Although each piece is placed in its final “solved” position, the code-behind adjusts each position by modifying TranslateX and TranslateY properties on the CompositeTransform assigned to each piece. This gives us the nice property that no matter where a piece is moved, it can be returned to its solved position by setting both of these properties to zero.
  • The magic behind making each puzzle piece contain a portion of a photo is enabled by the image brush that fills each path. (The actual image is set in code-behind.) To make each piece contain the correct portion of the photo, each image brush (except the one used on the piece in the top left corner) is given a TranslateTransform. This shifts its rendering by the distance that the piece is from the top-left corner. (To make this work, each image brush is marked with top-left alignment, rather than its default center alignment.)

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.

  • The scrolling tray at the bottom isn’t an actual scroll viewer; it’s just a simple rectangle. The code-behind manually scrolls puzzle pieces when they sufficiently overlap this rectangle. This is done for two reasons: It’s convenient to keep the pieces on the same canvas at all times, and the gesture listener currently interferes with Silverlight elements such as scroll viewers.
  • The CompleteImage element at the bottom of the listing is used to show the complete image once the puzzle is solved, without the puzzle piece borders and tiny gaps between pieces obscuring it. Because it is aligned with the puzzle, showing this image simply makes it seem like the puzzle edges have faded away. Figure 42.3 shows what this looks like for the cat-and-fish puzzle shown at the beginning of this chapter. Because CompleteImage is not hit-testable, the user can still drag a piece while it is showing. As soon as any piece moves out of its correct position, the codebehind hides the image once again.
Once the puzzle is solved, the puzzle piece borders are no longer visible.
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]

  • This app uses two settings defined in a separate Settings.cs file for remembering the user’s chosen photo and for remembering the position of every puzzle piece:

    [code]
    public static class Settings
    {
    public static Setting<int> PhotoIndex = new Setting<int>(“PhotoIndex”, 2);
    public static Setting<List<Point>> PieceOffsets =
    new Setting<List<Point>>(“PieceOffsets”, new List<Point>());
    }
    [/code]

  • This app also uses many constants, defined as follows in a Constants.cs file:

    [code]
    public static class Constants
    {
    public const int PUZZLE_WIDTH = 480;
    public const int PUZZLE_HEIGHT = 576;
    public const int PIECE_WIDTH = 96;
    public const int PIECE_HEIGHT = 96;
    public const int MAX_PIECE_WIDTH = 162;
    public const int SNAPPING_MARGIN = 15;
    public const int NEGATIVE_SCROLL_BOUNDARY = -1550;
    public const int TRAY_HORIZONTAL_SPACING = 110;
    public const int TRAY_VERTICAL_SPACING = 80;
    public const int TRAY_LEFT_MARGIN = 24;
    public const int TRAY_TOP_MARGIN = 590;
    public const int TRAY_2ND_ROW_HORIZONTAL_OFFSET = 50;
    public const int ON_TRAY_Y = 528;
    }
    [/code]

  • The first time Jigsaw Puzzle is run, the pieces animate from their solved positions to a random ordering on the tray. (This condition is detected in OnNavigatedTo because the PieceOffsets list does not initially contain the same number of elements as pieces in PiecesCanvas.) This ordering of pieces on the tray is shown in Figure 42.4. Every other time, the pieces are placed exactly where they were previously left by reapplying their persisted TranslateX and TranslateY values.
The puzzle pieces are arranged on the tray once they animate away from their solved positions.
FIGURE 42.4 The puzzle pieces are arranged on the tray once they animate away from their solved positions.
  • The three drag event handlers act differently depending on whether a puzzle piece is being dragged or the tray is being dragged. When the tray is dragged horizontally, we want it to scroll and reveal off-screen pieces. When a piece is dragged, it should move wherever the user’s finger takes it.
  • The DragStarted event handler (GestureListener_DragStarted) determines which type of dragging is occurring and sets either isDraggingTray or isDraggingPiece. (This handler can be called in cases where neither is true, such as dragging on an empty upper part of the screen, because these handlers are attached to the whole page.)

    DragStarted isn’t raised as soon as a finger touches the screen and starts moving; the gesture listener waits for the finger to move more than 12 pixels away to ensure that the gesture is a drag rather than a tap, and to determine the primary direction of the drag. DragStartedGestureEventArgs exposes this primary direction as a Direction property that is either Horizontal or Vertical.

    GestureListener_DragStarted leverages the Direction property to determine which kind of drag is happening. If the element reporting the event is the tray and the direction is horizontal, then it considers the gesture to be a tray drag. If a piece is being dragged horizontally and the vertical position of the piece visually makes it look like it’s on the tray, it also considers the gesture to be a tray drag. This is important to avoid the requirement that the tray can only be dragged on an empty spot. If a piece is being dragged vertically, or if it’s dragged in any direction far enough from the tray, then it’s considered to be a piece drag.

    Although this scheme is easy to implement, users might find the requirement to drag pieces off the tray in a mostly vertical fashion to be confusing and/or inconvenient. A more flexible approach would be to perform your own math and use a wider angle range.

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 DragDelta event exposes two more properties than DragStarted: HorizontalChange and VerticalChange. For tray dragging, the HorizontalChange value is passed to a ScrollTray helper method. This method provides the illusion of scrolling by manually updating the horizontal position of every piece whose vertical position makes it appear to be on the tray. Keeping all the pieces in the same canvas at all times (instead of moving pieces on the tray to a separate panel inside an actual scroll viewer) makes the logic throughout this page easier.

    For piece dragging, both HorizontalChange and VerticalChange are applied to the current piece’s transform, and then the piece is snapped to one or two solved-piece boundary locations if it’s close enough to a horizontal and/or vertical boundary. Ordinarily, the values of HorizontalChange and VerticalChange would be directly added to TranslateX and TranslateY, respectively, but this doesn’t work well when snapping is done. Because each snap moves the piece by as much as 14 pixels away from its natural position, continued snapping would cause the piece to drift further away from the user’s finger if we continued to add the HorizontalChange and VerticalChange values. Instead, by manually tracking the total cumulative distance from the beginning of the drag, the piece is returned to its natural position after it breaks free of a snapping boundary.

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.

  • The DragCompleted event exposes all the properties from DragDelta plus two more: HorizontalVelocity and VerticalVelocity. These values are the same ones exposed to the Flick event, and enable inertial flicking motion at the end of a drag. Just like in Chapter 40, “Darts,” the velocity is scaled down and then used to continue the dragging motion a bit. This is done for tray dragging only, to make it better mimic a real scroll viewer. Therefore, only the horizontal component of the velocity is used. At the end of every drag action, the location of each piece is checked to see whether the puzzle has been solved. We know that the pieces are all in the correct spots if their transforms all have TranslateX and TranslateY values of zero.
  • The “picture” and “instructions” button click handlers navigate to other pages, and the “start over” and “solve” button click handlers trigger animations that either move the pieces to random spots on the tray or to their solved positions. The solve animation is performed by the ShowAsSolved method, which animates each outof- place piece to its correct position over the course of one second, spaced .3 seconds apart. The resulting effect smoothly fills in the pieces in row major order, as pictured in Figure 42.5.
The automatic solving animation makes the pieces float into place according to their order in the canvas.
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.

 The sequence of events when navigating to the cropped photo chooser 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]

  • Drag gestures (and pinch/stretch gestures) are detected with a page-level gesture listener, just like on the main page.
  • The white rectangle not only reveals the puzzle’s dimensions if the picture doesn’t completely cover the area, but it also ends up giving the puzzle a white background when this happens. This is demonstrated in Figure 42.7.
  • Two different transforms are used to make the user’s gestures feel natural. Whereas drag gestures adjust ImageTransform much like the dragging of puzzle pieces on the main page, pinch and stretch gestures are applied to CanvasTransform. Because the canvas represents the puzzle area and it’s marked with a centered render transform origin of (.5,.5), the user’s gesture always zooms the image centered around the middle of the puzzle. If this were applied to the image instead, zooming would occur around the image’s middle, which might be far off-screen as the image gets zoomed and panned.
  • Two subtle things on this page prevent its performance from being disastrous. The bitmap caching on the image makes the panning and zooming much smoother than it would be otherwise, as does the clip applied to the page. (The page’s clip also prevents the entire screen from going blank if the photo is zoomed in to an extreme amount, caused by Silverlight failing to render a surface that is too big.)
A white rectangle serves as the puzzle’s background if the picture doesn’t completely cover it.
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]

  • Inside DragDelta, the HorizontalChange and VerticalChange values are directly added to the image transform each time; although they must be divided by any scale applied to the parent canvas so the distance that the image travels remains consistent. This direct application works well because the TranslateX and TranslateY are not changed through any other means, such as the snapping logic used on the main page.

    The PinchStarted handler records the scale when two fingers touch the screen, arbitrarily choosing ScaleX because both ScaleX and ScaleY are always set to the same value. The PinchDelta handler multiplies the initial scale by the finger distance ratio and then applies it to the canvas transform.

  • The handler for the done button’s click event, DoneButton_Click, leverages WriteableBitmap’s killer feature—the ability to capture the contents of any element and write it to a JPEG file. A new WriteableBitmap is created with the puzzle’s dimensions, and then this (the entire page) is rendered into it with a transform that shifts it 112 pixels upward. This is necessary to avoid rendering the page’s header into the captured image. Figure 42.8 demonstrates what happens if null is passed for the second parameter of Render.

    After refreshing the bitmap with a call to Invalidate, SaveJpeg writes the contents in JPEG format to a memory stream which can then be written to isolated storage.

If the page is rendered to the puzzle image from its top-left corner, the page header becomes part of the puzzle!
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:

  • It doesn’t capture parts of the user interface outside of the Silverlight visual tree—the application bar, status bar, and message boxes.
  • It doesn’t capture any popups, even ones that are attached to an element on the page.
  • It doesn’t capture any WebBrowser instances.
  • It doesn’t capture any MediaElement instances.

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

Windows Phone Jigsaw Puzzle (Drag Gesture & WriteableBitmap)
Jigsaw Puzzle (Drag Gesture & WriteableBitmap)