How to create a Live Tile in the style of the Contacts Hub

With Windows Phone 7 Mango you are able to create your own Live Tiles. To be more precisely, you can specify content, titles and images for the front side as well as for the back side. The images can be web images or some pictures from your isolated storage. Sometimes it may be necessary to build a Live Tile like the one of the Contacts Hub.
As you can see I’ve created a tile with 9 custom images inside of it. These photos are loaded by a background agent and dynamically merged to one image. I want to explain how I did this.

First, provide a list of images to load:
private const string FLICKR_LIVE_TILE = "/Shared/ShellContent/flickr_live_tile.jpg";
private object imageCountLock = new object();
private const int IMAGES_TO_LOAD = 9;
private const int IMAGE_DIMENSIONS = 57;
private static string PhotoLiveTileNavUri = "/MainPage.xaml?list=flickr";
private static string TwitterLiveTileNavUri = "/MainPage.xaml?list=twitter";
// Just using the DisplayImage class with an attribut called "ThumbnailImage".
//The size of the image has to be really small.
public void UpdateTileImages(IList<DisplayImage> images) {
WebClient webClient = new WebClient();
int imageLoadedCount = 0;
var maxItems = Math.Min(images.Count, IMAGES_TO_LOAD);
Stream[] streams = new Stream[maxItems];
webClient.OpenReadCompleted += ((s, e) =>
{
lock(imageCountLock)
{
streams[imageLoadedCount] = e.Result;
imageLoadedCount++;
if (imageLoadedCount == maxItems)
{
CreateBackgroundImage(streams);
}
else
webClient.OpenReadAsync(new Uri(images[imageLoadedCount].ThumbnailImage, UriKind.Absolute));
}
});
webClient.OpenReadAsync(new Uri(images[imageLoadedCount].ThumbnailImage, UriKind.Absolute));
}
As you can see I’m using the WebClient class to load these images sequentially. After one has completed, the next one is loaded. After all images are successfully loaded, the CreateBackgroundImage method is called:
private void CreateBackgroundImage(Stream[] images)
{
Canvas can = new Canvas();
can.Background = (SolidColorBrush)Application.Current.Resources["PhoneAccentBrush"];
can.Width = 173;
can.Height = 173;
WriteableBitmap writeableBitmap = new WriteableBitmap(173, 173);
for (int i = 0; i < images.Count(); i++ )
{
var bitmapImage = new BitmapImage();
bitmapImage.SetSource(images[i]);
Image image = new Image();
image.Source = bitmapImage;
image.Width = IMAGE_DIMENSIONS;
image.Height = IMAGE_DIMENSIONS;
image.Stretch = Stretch.UniformToFill;
switch (i)
{
case 0:
image.Width++;
break;
case 1:
Canvas.SetLeft(image, IMAGE_DIMENSIONS);
image.Width++;
break;
case 2:
Canvas.SetLeft(image, 2 * IMAGE_DIMENSIONS + 1);
image.Width++;
break;
case 3:
Canvas.SetTop(image, IMAGE_DIMENSIONS);
image.Height++;
break;
case 4:
Canvas.SetTop(image, IMAGE_DIMENSIONS);
Canvas.SetLeft(image, IMAGE_DIMENSIONS);
image.Height++;
image.Width++;
break;
case 5:
Canvas.SetTop(image, IMAGE_DIMENSIONS);
Canvas.SetLeft(image, 2 * IMAGE_DIMENSIONS + 1);
image.Height++;
image.Width++;
break;
case 6:
Canvas.SetTop(image, 2*IMAGE_DIMENSIONS + 1);
Canvas.SetLeft(image, 0);
image.Height++;
break;
case 7:
Canvas.SetTop(image, 2*IMAGE_DIMENSIONS + 1);
Canvas.SetLeft(image, IMAGE_DIMENSIONS);
image.Width++;
image.Height++;
break;
case 8:
Canvas.SetTop(image, 2*IMAGE_DIMENSIONS + 1);
Canvas.SetLeft(image, 2 * IMAGE_DIMENSIONS + 1);
image.Width++;
image.Height++;
break;
}
can.Children.Add(image);
}
writeableBitmap.Render(can, null);
writeableBitmap.Invalidate();
using (var isoFileStream = new IsolatedStorageFileStream(FLICKR_LIVE_TILE, FileMode.OpenOrCreate, IsolatedStorageFile.GetUserStoreForApplication()))
{
writeableBitmap.SaveJpeg(isoFileStream, 173, 173, 0, 100);
}
var activeTile = ShellTile.ActiveTiles.Where(t => t.NavigationUri == new Uri(PhotoLiveTileNavUri)).FirstOrDefault();
StandardTileData shellTileData = new StandardTileData
{
BackBackgroundImage = new Uri("isostore:" + FLICKR_LIVE_TILE, UriKind.Absolute),
BackContent = "Photos",
BackgroundImage = new Uri(@"Background.png", UriKind.Relative),
BackTitle = "Local Things",
Count = images.Count(),
Title = ""
};
if (activeTile != null)
{
activeTile.Update(shellTileData);
}
else
{
ShellTile.Create(new Uri(PhotoLiveTileNavUri, UriKind.Relative), shellTileData);
}
}
Let me explain what I did. I created a canvas object where I can add multiple images to. The size of the canvas object has to be 173×173 pixels because that’s exactly the size of a Live Tile pinned to the start.
After that, you have to create an instance of BitmapImage and an instance of the Image class for every little image you want to display. You need the Image class because you only can add elements to the Canvas which are subclasses of UIElement.
The following switch statement isn’t the most elegant way to calculate the position of each item. I did it as easy as possible just for a better understanding. After you have positioned all items you have to render the Canvas object with the Render method of the WriteableBitmap class.
The next issue is that it’s only allowed to save these graphics as a jpeg image. If you haven’t enough images to fill out the Live Tile you need a background color which fits to the accent color. Therefore, I loaded the phone’s accent brush into the background of my canvas object.
After these steps you are able to save this collaboration image to the isolated storage and can use it as a background image for your live tile.
Note: You have to ensure that your images you want to display are as small as possible if you’re using a background agent. The time restrictions are very strict, so it might happen that the time is over until you haven’t finished loading all you images.

