Metro In Motion #8 - AutoCompleteBox Reveal Animation

When I started the Metro In Motion series, I thought I would probably post three or four articles and be done. However, every time I use my Windows Phone 7 I seem to spot a new 'native' fluid UI effect which I would like to use in my own code. Also, these posts have proven very popular, there appears to be a real developer 'need' for this sort of information.

In this instalment of Metro In Motion I will show how to implement the fluid auto-complete effect that can be seen in the Windows Phone 7 email client. You can see a 'storyboard' for this effect below; the items within the auto-complete popup slide gracefully into view when the popup initially renders:

This effect uses the AutoCompleteBox which is part of the Silverlight for Windows Phone Toolkit. I have implemented this effect as a Blend Behaviour, allowing it to be easily added to an AutoCompleteBox either via drag and drop within Expression Blend, or by simply adding the XAML below:

<tk:AutoCompleteBox VerticalAlignment="Top"
                    ItemsSource="{Binding}"
                    ValueMemberPath="Surname">
  <!-- add the metro in motion effect -->
  <i:Interaction.Behaviors>
    <behaviour:AutoCompleteSlideBehaviour/>
  </i:Interaction.Behaviors>
</tk:AutoCompleteBox>

Let's take a look at how this behaviour is implemented ...

In order for this effect to work, we need to handle the Opening event raised by the Popup control that is part of the AutoCompleteBox template. When this event fires, each of the elements within the ListBox that the Popup contains are animated. The first task is to locate the ListBox and the Popup, this is performed when the behaviour is attached:

protected override void OnAttached()
{
  base.OnAttached();

  // locate the listBox, if this fails, the AutoCOmpleteBox is not loaded,
  // so try again after the Loaded event.
  if (!TryFindListBox())
  {
    RoutedEventHandler onLoaded = null;
    onLoaded = (s, e) =>
        {
          TryFindListBox();
          AssociatedObject.Loaded -= onLoaded;
        };
    AssociatedObject.Loaded += onLoaded;
  }
}

/// <summary>
/// Tries to locate the Listbox within the auto-completes Popup.
/// </summary>
private bool TryFindListBox()
{
  // locate the auto-complete  popup
  _popup = AssociatedObject.Descendants<Popup>().SingleOrDefault() as Popup;
  if (_popup == null)
    return false;

  _popup.Opened += Popup_Opened;

  // locate the ListBox
  _popUpListbox = _popup.Child.Descendants<ListBox>().SingleOrDefault() as ListBox;
  return true;
}

The above code uses some simple Linq-to-VisualTree to locate these elements. Notice that the approach taken first tries to locate these elements, if this fails, the Loaded event is handled, which is fired when elements (and their template components) are added to the visual tree.

The animation is fired each time the Popup is opened. Creating the animation itself is quite simple, the items within the ListBox are iterated over, with a TranslateTransform created for each, with delayed Storyboard animations used to fire the animations sequentially. However, the container's (i.e. ListBoxItem instances) may not have been created when the Opened event fires. In order to handle this, the code below checks for the container of the item at the first index. If this container has not yet been generated, the code which creates the animations is deferred until after the next LayoutUpdated event. This should ensure that the containers are now present. The code is shown below:

/// <summary>
/// Handle the Opened even on the Popup in order to animate the contents
/// </summary>
private void Popup_Opened(object sender, System.EventArgs e)
{
  if (_popUpListbox.Items.Count == 0)
    return;

  // an action which fires the required animation for each element
  Action fireAnimations = () =>
  {
    // locate all the ListBoxItems
    var itemContainers = _popUpListbox.Items
        .Select(i => _popUpListbox.ItemContainerGenerator.ContainerFromItem(i))
        .Where(i => i != null)
        .Cast<ListBoxItem>();

    // animate each item
    double startTime = 0;
    foreach (var listBoxItem in itemContainers)
    {
      // add a render transform that offsets the element
      var trans = new TranslateTransform()
          {
            Y = -50
          };
      listBoxItem.RenderTransform = trans;
      listBoxItem.Opacity = 0.0;

      var sb = new Storyboard();
      sb.BeginTime = TimeSpan.FromMilliseconds(startTime);
      sb.Children.Add(TiltBehaviour.CreateAnimation(null, 0, 0.2, "Y", trans));
      sb.Children.Add(TiltBehaviour.CreateAnimation(null, 1, 0.5, "Opacity", listBoxItem));
      sb.Begin();

      startTime += 100;
    }
  };

  // check if the item container for the first item has been generated
  if (_popUpListbox.ItemContainerGenerator.ContainerFromIndex(0) == null)
  {
    // if not, wait for the next LayoutUpdated event
    EventHandler updateHandler = null;
    updateHandler = (s, e2) =>
    {
      fireAnimations();
      _popUpListbox.LayoutUpdated -= updateHandler;
    };
    _popUpListbox.LayoutUpdated += updateHandler;
  }
  else
  {
    // otherwise, fire the animations now
    fireAnimations();
  }
}

The full sourcecode for this behaviour can be found in the WP7Contrib project, within the WP7Contrib.View.Controls assembly.

You can download a simple working example here: MetroInMotionEight.zip

I have of course updated the Sandwich Flow, Metro In Motion demo application to include this effect. Again, this is available via the WP7Contrib project:

Regards,Colin E.

blog comments powered by Disqus