1
2
3
4
5
6

Thinking Outside the Box - Using Programatic Animations in Silverlight

At the Atlanta Silverlight Firestarter a few months ago myself and Mason Brown did a talk 'Lighting Up the UI'. At the end of the talk I ranted a bit trying to encourage developers to think outside of the box when approaching UI's in Silverlight - to be open to creative ways in tackling new UI challenges. (You can find more on this talk in these posts on SilverlightAtlanta.net).

The example I went over in my example was a simple example of how to move an item from one ListBox to another. Most developers would look at this task the same way - they would remove the item being moved from the 'from' ListBox, and they would add the item to the 'to' ListBox. It would probably look something like this:

// get a handle on the item we are moving
ListBoxItem lbi = fromList.SelectedItem as ListBoxItem;
if (lbi == null)
    return;
 
fromList.Items.Remove(lbi); // remove it from the 'from' list
toList.Items.Add(lbi); // add it to the 'to' list

When you work with designers and usability engineers you'll often get challenged to do something which requires you to think outside the box. In this example the request is to get rid of the 'popping' between lists. The intent is to actually move the item, and show it moving from one list to the other - this is the sort of detail that can distinguish a standard app from a real user experience.

Here is a sample Silverlight application which shows off moving an item between lists both through the standard add/remove method, and our 'thinking outside the box' method we'll cover after the break:

Get Microsoft Silverlight

(use the radio buttons to see the two ways of moving an item between ListBoxes)

(continue reading ...)

To accomplish the item 'moving' from one list to another I took a few more steps than just the typical add/remove route - here is a summary of the steps taken:

  1. Create a layover grid where we will display the item as it moves between ListBoxes
  2. Get the X/Y position of the item in the 'from' list box relative to our layover grid
  3. Create a temporary WriteableBitmap image of the ListBoxItem we are moving
  4. Remove the item from the 'from' list and put it in the 'to' list
  5. Get the X/Y position of the item in the 'to' list box relative to our layover grid
  6. Remove the item from the 'to' list
  7. Create a Storyboard animation from the 'from' position to the 'to' position
  8. Add our temporary BitmapImage to the layover grid and use the Storyboard to animate it
  9. Once the animation is completed add the item to the 'to' list and get rid of our temporary WriteableBitmap

All of these steps together should make it appear that the item is actually moving from one list to the other. Underneath we are still really just adding and removing the item from one list to the other, but we are using programatic animations of a rendering of the ListBoxItem to make it seem like the item is actually moving from one spot to another.

It is worth mentioning that it is not always necessary to use a WriteableBitmap to do this - in many cases you can just animate the UIElement you are working with directly using a RenderTransform. The reason we are using a WriteableBitmap in this example is because a ListBoxItem will size to fit its container, so the WriteableBitmap is a quick and easy way to accomplish this task instead.

The key element to this type of simulated movement is a RenderTransform. If we were using a Canvas we would use X,Y positioning of the element, but in most cases you won't be using a Canvas - so understanding how a RenderTransform can be used to make something appear in a different location is key to this kind of positioning.

The solution to this problem is something I've come up with while working on similar challenges in recent projects at work - I'd love to see other examples of how this could be accomplished, so please feel free to share your thoughts in the comments.

A Quicker Route

The example I threw together for this post is a bit more complicated than it really needs to be, but I think the route taken in the example above is good a good exercise on how to think outside the box. Here is a brief summary for how you might accomplish the same task with fewer steps:

  1. Get the X/Y position of the item in the 'from' ListBox relative to the LayoutRoot element
  2. Remove the item from the 'from' list, and add it to the 'to' list
  3. Get the X/Y position of the item in the 'to' ListBox relative to the LayoutRoot element
  4. Calculate the difference between the 'from' position and the 'to' position
  5. Set the X/Y RenderTransform of the ListBoxItem to the offset so it still appears to be in the 'from' position
  6. Animate the X/Y RenderTransform to 0, 0 so it animates to where it really is in the 'to' list

Here is what the code looks like:

private void TransitionFromListToListWithAnimatics(ListBox fromList, ListBox toList)
{
    // get a handle on the item we are moving, if nothing is selected then return
    ListBoxItem lbi = fromList.SelectedItem as ListBoxItem;
    if (lbi == null)
        return;
 
    // figure out the offset of the listbox item relative to the LayoutRoot in the 'from' state
    MatrixTransform initialPositionRelativeToRoot = lbi.TransformToVisual(LayoutRoot) as MatrixTransform;
    Debug.Assert(initialPositionRelativeToRoot != null);
 
    // remove the item from the 'from' list, add it to the 'to' list 
    fromList.Items.Remove(lbi);
    toList.Items.Add(lbi);
    toList.SelectedItem = lbi; // select the item that is now in a new list
    lbi.UpdateLayout();
 
    // figure out the offset of the listbox item relative to the LayoutRoot in the 'to' state
    MatrixTransform newPositionRelativeToRoot = lbi.TransformToVisual(LayoutRoot) as MatrixTransform;
    Debug.Assert(newPositionRelativeToRoot != null);
 
    // calculate the difference between the 'to' and 'from' positions.. we will use this as a RenderTransform to 
    // make the item still appear to be in the old list even though it has been added to the new list
    double xOffset = initialPositionRelativeToRoot.Matrix.OffsetX - newPositionRelativeToRoot.Matrix.OffsetX;
    double yOffset = initialPositionRelativeToRoot.Matrix.OffsetY - newPositionRelativeToRoot.Matrix.OffsetY;
 
    // generate a transform based on the difference
    TransformGroup differenceTransform = TransitionStoryboardHelper.GetTransformGroupForXYOffset(xOffset, yOffset);
    lbi.RenderTransform = differenceTransform;
 
    // build an animation from the offset to 0,0 so that it comes to where the item actually is in the list
    ElasticEase easing = new ElasticEase() { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 4 };
    TimeSpan animationPeriod = new TimeSpan(0, 0, 0, 0, 350);
    Storyboard sb = TransitionStoryboardHelper.BuildTranslateTransformTransition(lbi, new Point(0, 0), 
        animationPeriod, easing);
    sb.Begin();
}

Comments