Animating item selection in a WP7 application

Another day another occasion to share an implementation detail behind my 2Day todo-list application. In this post I share the code used to animate the selection in a ListBox. Here is what is looks like in the create/edit folder dialog of 2Day:

You can grab the source code directly here if you want.

Background

The basic idea is to create another element behind the ListBox which will be animated when the selection changes. The tricky part is to find out the appropriate coordinates of this element to make sure it’s perfectly on top of the element we need to higlight.

XAML

The XAML is not very complicated. Notice I use a particular style on the ListBox to remove its scrolling capability. I have to do that because otherwise if the user perform a drag gesture on the ListBox, it moves its element without moving the overlay I need to add. I use a simple Grid to be able to add the overlay “behind” the ListBox:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<phone:PhoneApplicationPage.Resources>
    <Style x:Key="ListBoxNoScrollStyle" TargetType="ListBox">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
        <Setter Property="BorderThickness" Value="0"/>
        <Setter Property="BorderBrush" Value="Transparent"/>
        <Setter Property="Padding" Value="0"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ListBox">
                    <ItemsPresenter/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</phone:PhoneApplicationPage.Resources>
 
<Grid>
    <Border x:Name="overlayHost">
        <Border 
            x:Name="iconOverlay" 
            Margin="4,4,0,0"
            VerticalAlignment="Top"
            HorizontalAlignment="Left"
            Background="Red" 
            CacheMode="BitmapCache"
            Width="48" 
            Height="48"/>
    </Border>
 
    <ListBox 
        x:Name="listbox"
        HorizontalAlignment="Center"
        CacheMode="BitmapCache"
        Style="{StaticResource ListBoxNoScrollStyle}">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <toolkit:WrapPanel/>
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Border 
                    Width="48" 
                    Height="48"
                    BorderBrush="Red"
                    BorderThickness="1"
                    Margin="5">
                    <TextBlock 
                        Text="{Binding}" 
                        Foreground="White"/>
                </Border>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Code

The code is for now in the code-behind of the view (see next paragraph for improvements ideas). I use the ItemContainerGenerator property of the ListBox to retrieve the UI element from the Data element (in this example: an integer).

Then I use the TransformToVisual method to get the location of the visual relative to the parent ListBox. Next step is to animate the position of the item using DoubleAnimations:

?View Code CSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public partial class MainPage : PhoneApplicationPage
{
    private readonly Point emptyPoint = new Point();
    private DoubleAnimation animationX;
    private DoubleAnimation animationY;
    private Storyboard storyboard;
 
    public MainPage()
    {
        InitializeComponent();
 
        this.listbox.ItemsSource = Enumerable.Range(1, 32);
 
        this.Loaded += this.OnLoaded;
    }
 
    private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        this.Loaded -= this.OnLoaded;
 
        this.listbox.SelectionChanged += this.OnSelectionChanged;
 
        this.overlayHost.Height = this.listbox.ActualHeight;
        this.overlayHost.Width = this.listbox.ActualWidth;
        this.overlayHost.Margin = this.listbox.Margin;
 
        this.UpdateHighlightedItem();
    }
 
    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        this.UpdateHighlightedItem();
    }
 
    private void UpdateHighlightedItem()
    {
        var element = this.listbox.ItemContainerGenerator.ContainerFromIndex(this.listbox.SelectedIndex) as FrameworkElement;
        if (element != null)
        {
            // compute the location of the selected element
            Point destination = element.TransformToVisual(this.listbox).Transform(this.emptyPoint);
            Point origin = this.iconOverlay.TransformToVisual(this.listbox).Transform(this.emptyPoint);
 
            var translateTransform = this.iconOverlay.RenderTransform as TranslateTransform;
            if (translateTransform == null)
            {
                translateTransform = new TranslateTransform();
                this.iconOverlay.RenderTransform = translateTransform;
 
                TimeSpan duration = TimeSpan.FromMilliseconds(150);
                IEasingFunction ease = new QuarticEase { EasingMode = EasingMode.EaseOut };
 
                this.animationX = new DoubleAnimation { Duration = duration, EasingFunction = ease };
                this.animationY = new DoubleAnimation { Duration = duration, EasingFunction = ease };
 
                this.storyboard = new Storyboard();
                this.storyboard.Children.Add(this.animationX);
                this.storyboard.Children.Add(this.animationY);
            }
            else
            {
                this.storyboard.Stop();
            }
 
            Storyboard.SetTarget(this.animationX, translateTransform);
            Storyboard.SetTarget(this.animationY, translateTransform);
            Storyboard.SetTargetProperty(this.animationX, new PropertyPath(TranslateTransform.XProperty));
            Storyboard.SetTargetProperty(this.animationY, new PropertyPath(TranslateTransform.YProperty));
 
            this.animationX.From = origin.X;
            this.animationY.From = origin.Y;
            this.animationX.To = destination.X;
            this.animationY.To = destination.Y;
 
            this.storyboard.Begin();
        }
    }
}

Possible improvements

As you have seen, the current code is not easy to reuse. I think all the logic cold be embedded in a reusable component. Either using an attached property (and tweaking the template of the ListBox) or by subclassing the orginal ListBox class. I leave this exercise to the reader :-)

Conclusion

In this article I shared the technique used in 2Day to animate the item selection. Many improvements are possible on top of this code, but I hope it could help WP developers.

Download the source code here if you want.

2 thoughts on “Animating item selection in a WP7 application

  1. Very cool! Thanks for the post — I can possibly see this being used in a WPF app I’m playing around with.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>