Thursday 15 December 2011

Dynamically generating WP7 Live Tiles

I’ve been struggling to add a generic mechanism for generating live Tiles to my applications.

My requirements are:

  • Save to png format (so I can have a transparent background to always match the users theme)
  • Generate the tile based on a XAML layout.

Saving to PNG

I managed to waste a day creating my own mashup of various source code on line to write PNG’s from a WriteableBitmap.  I started out with some code from Joe Stegman's blog, but hadn’t noticed that I had to fill the EditableImage.  After a day’s rewriting, I managed to build a version that could save an uncompressed png from a WriteableBitmap.  Unfortunately, that produced a LiveTile image of 115KB, which I thought was using too many resources on the phone, just to flip a tile.  And that’s the cost for each side.

I then looked into the .NET ImageTools on CodePlex.  After downloading the source, I couldn’t find some of the source for the referenced DLLs, particularly the gzip libraries.  To make matters worse, it isn’t clear what the license actually is for that component, so after wasting several hours I decided to abandon it, and roll my own compressor from the DotNetZip project on Codeplex.

Finally success after inadvertently using the DeflateStream instead of the GZipStream to write, and now my tiles are down to around 8KB each.

Generating the Bitmap in the first place

As I said above, my second requirement was to generate the bitmap from XAML.

This work in progress class does all of it:

public static class TileExtensions
{
public static readonly string tiledirectory = @"Shared/ShellContent/tiles";
public static IsolatedStorageFile _appstore;

public static IsolatedStorageFile ISO
{
get
{
// lazy open
if (_appstore == null)
{
_appstore = IsolatedStorageFile.GetUserStoreForApplication();
}
return _appstore;
}

}

public static WriteableBitmap GetWriteableBitmapTile(this UIElement item)
{
item.Arrange(new Rect(0, 0, 173, 173));
WriteableBitmap wbmp = new WriteableBitmap(item, null);

return wbmp;
}

public static string GetImageTilePath(string name)
{
return tiledirectory + @"/" + name + ".png";
}

public static Uri GetImageTileUri(string fullpath)
{
return new Uri("isostore:"+fullpath);
}


public static string SaveImageTile(this UIElement tile, string name)
{
WriteableBitmap bmp = tile.GetWriteableBitmapTile();

string fullpath = GetImageTilePath(name);
if (!ISO.DirectoryExists(tiledirectory))
{
ISO.CreateDirectory(tiledirectory); // guarantee our directory exists
}

using (var stream = ISO.OpenFile(fullpath, System.IO.FileMode.OpenOrCreate))
{
bmp.SavePng(stream);
}

return fullpath;
}
}

 

 

 

 


 

The idea then is to load up some XAML as a UserControl, set it’s DataContext, and then render it like this:
var toRender = new MyUserControl;
toRender.DataContext = MyDataContext;
string tilepath = g.SaveImageTile("tilename");
ShellTile primary = ShellTile.ActiveTiles.First();
StandardTileData tile = new StandardTileData()
{
BackBackgroundImage = TileExtensions.GetImageTileUri(tilepath)
};
primary.Update(tile);


This would be a joy to update a tile.  And it all appears to work great.  The databinding works, the control renders.


BUT none of the images from the XAML render.


At the moment, I’ve had to force the loading of the images from my UserControl derived items’ constructor like this:


BitmapImage bmi = new BitmapImage();
bmi.CreateOptions = BitmapCreateOptions.None;
StreamResourceInfo streamInfo =
Framework.App.GetResourceStream(
new Uri(@"images\img1.png", UriKind.Relative));
bmi.SetSource(streamInfo.Stream);
img1.Source = bmi;


I’ve posted a question on the WP7 Forums, to see what suggestions people might come up with.


A solution:


After much experimentation, and rewriting, the magic came down to subscribing to the LayoutUpdated event of a FrameworkElement.
When the LayoutUpdated event is fired, it appears that the images are guaranteed to be loaded. From that point on you can begin generating the bitmap.
However, there is no way to wait for the event to be fired, so you need to implement some kind of callback.
In the end, my call now looks something like this:

 

TileRenderer tr = new TileRenderer(
new MyUserControl() { DataContext = new MyTileInfo() });
tr.SaveImageTile("tilefilename",
(x) => {
ShellTile primary = ShellTile.ActiveTiles.First();
StandardTileData tile = new StandardTileData()
{
BackBackgroundImage = x.GetImageTileUri()
};
primary.Update(tile);
});

2 comments:

  1. Look at this lib I made : http://wp7tiletoolkit.codeplex.com/

    I don't have problems rendering images, and it answers your problems : you can create custom tile layouts with XAML.

    Also available on nuget. Not yet supporting compressed PNG but it's a good idea that I could reuse, even though I didn't have any culprit for generating 100kb images ;).

    ReplyDelete
  2. I'm not sure your XAML will support an image with its source specified any better than I've managed.

    ReplyDelete