1
votes

I am trying to recreate the Windows Phone continuum transiton (the one where a selected item circles out of and into view, for instance when opening an email in the mail client) and I can't seem to get one animation to work.

When you go back to the list of item, the previously selected item has to slide back into the list from the upper left somewhere. I figured that I'd keep a reference to the selected item, and then in the Loaded event handler of the page, check if that reference is not null, and if so, create a storyboard, find the proper listbox item, setting it as the target of the animation, and running it.

However, this is not working. Here's the listbox itemtemplate:

<StackPanel x:Name="commuteStackPanel" local:TiltEffect.IsTiltEnabled="True" Margin="0,0,0,38" Tap="StackPanel_Tap">
    <TextBlock Style="{StaticResource PhoneTextTitle3Style}" TextTrimming="WordEllipsis" Text="{Binding Title}" />
    <TextBlock Style="{StaticResource PhoneTextSubtleStyle}" Text="{Binding SelectedDays}" Margin="12,0,12,0"/>                                
</StackPanel>

this is the animation, defined as a page resource:

<Storyboard xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:Key="continuumStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" >
    <EasingDoubleKeyFrame KeyTime="0" Value="-70"/>
    <EasingDoubleKeyFrame KeyTime="0:0:4.15" Value="0">
        <EasingDoubleKeyFrame.EasingFunction>
            <ExponentialEase EasingMode="EaseOut" Exponent="3"/>
        </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" >
    <EasingDoubleKeyFrame KeyTime="0" Value="-30"/>
    <EasingDoubleKeyFrame KeyTime="0:0:4.15" Value="0">
        <EasingDoubleKeyFrame.EasingFunction>
            <ExponentialEase EasingMode="EaseOut" Exponent="3"/>
        </EasingDoubleKeyFrame.EasingFunction>
    </EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>

And this is the page's codebehind:

    private FrameworkElement lastSelectedElement = null;

    private void StackPanel_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        this.lastSelectedElement = sender as FrameworkElement;
    }

    private void page_Loaded(object sender, RoutedEventArgs e)
    {            
        if (this.lastSelectedElement != null)
        {
            // Using the last selected element's datacontext, find the relevant ListBoxItem
            var listBoxItem = commutesListBox.ItemContainerGenerator.ContainerFromItem(this.lastSelectedElement.DataContext);

            // Get the actual stackpanel we want to animate
            var element = FindVisualChild<StackPanel>(listBoxItem);

            // Ensure it has the proper transforms
            element.RenderTransform = new CompositeTransform { TranslateX = 0, TranslateY = 0 };

            // Get the storyboard and make sure it is not playing.
            var storyboard = Resources["continuumStoryboard"] as Storyboard;
            storyboard.Stop();

            // Set the stackpanel as the target for each timeline in the storyboard
            foreach (var timeline in storyboard.Children)
            {
                Storyboard.SetTarget(timeline, element);
            }

            // Play the animation.
            storyboard.Begin();
        }
    }

The FindVisualChild method I copied from an MSDN article:

private childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(obj, i);
            if (child != null && child is childItem)
                return (childItem)child;
            else
            {
                childItem childOfChild = FindVisualChild<childItem>(child);
                if (childOfChild != null)
                    return childOfChild;
            }
        }
        return null;
    }

However, this does not work. If I set any other element, for instance the listbox itself, as the target of the animation, I can see it playing, but when I set any of the elements in the datatemplate, nothing happens. No errors, nothing.

I figured I'd experiment and just try to animate the first item of the listbox by replacing

this.lastSelectedElement.DataContext

with

commutesListBox.Items.First()

but still no dice. What am I missing here?

EDIT I have since managed to get the animation to run, but I still don't understand why. It turned out that on the Loaded event, the viewmodel would repopulate the ObservableCollection the listbox is bound to. After preventing that from happening, the animation would play.

What I don't get is that the problem persists even if you do nothing with the bound values or datacontext, and instead use Items.First(). Even if the viewmodel changes, it ought to still have a First() item (and stepping through shows that it indeed does).

1

1 Answers

1
votes

Hmm, I've build a simple App with your code (you can downlad it here) and everything woks as it should (as I think). In my App:

  • add some items with first button,
  • tap on one item,
  • click second button to animate .

Maybe the problem is with the time when your page_Loaded event is fired - you have to tap StackPanel first in your code, and when Page is Loaded, the user didn't have a chance to tap item.