Thursday, October 17, 2013

Turnstile Feather Transition with a Pivot Control on Windows Phone 8

I’ve been intending to make my first foray into Windows Phone development for a while now, but I had decided to put it off until I actually owned a Windows Phone device.  I recently completed the 24 month contract on my previous phone (a Nokia E7 running Symbian Belle, which I was actually pretty happy with) and upgraded to a Nokia Lumia 925.

I’m very happy with the phone and I really like the Windows Phone OS… and I’m now a Windows Phone developer.  I’m yet to actually publish my first app but I wanted to share with you an interesting issue I encountered and the solution I came up with.  I couldn’t find any other information specific to this issue so I thought that this might be useful.

I had several pages in place to list, display, add and edit several entities so I decided I would look at adding page transitions then rather than going back and retrofitting them to every page at the end.  I decided to use the TurnstileTransition on the display, add and edit pages and the TurnstileFeatherTransition on the list pages, both from the Windows Phone Toolkit.

Several of my list pages contained just a single LongListSelector.  They worked fine simply by setting the TurnstileFeatherEffect.FeatheringIndex attached property for the page elements as you would expect.  One of the pages contained several LongListSelectors inside a Pivot though, and that page didn’t quite work as it should.  Let’s build a test app so that you can see what I mean.

Start by creating a new Windows Phone Pivot App project.  We’re going to use NuGet to add the Windows Phone Toolkit package so if, like me, you have configured VS not to immediately save Windows projects, you’ll have to immediately save your project.  For some reason NuGet won’t add packages to projects in a temporary location.

Once the project is saved, select Manage NuGet Packages from the Project menu.  In the dialogue, select the ‘nuget.org’ item in the Online section and then search for windows phone toolkit.  Install the package and you now have page transitions at you disposal.

We next need to enable transitions for all pages in the app.  To do that, open the code file behind App.xaml, i.e. App.xaml.cs or App.xaml.vb.  In the InitializePhoneApplication method, find the line that reads like this:

C#

  1. RootFrame = new PhoneApplicationFrame();

VB

  1. RootFrame = New PhoneApplicationFrame()

and change it to this:

C#

  1. RootFrame = new TransitionFrame();

VB

  1. RootFrame = New TransitionFrame()

If we’re going to have page transitions then we need to have at least two pages to navigate between.  Our project gave us MainPage.xaml for free but we need to add a second page.  From the Project menu, select Add New Item and then add a new Windows Phone Portrait Page.

Now that we have a second page to navigate to, let’s add the Turnstile transition to it.  First, we’ll need to import the ‘toolkit’ namespace, so add the following attribute to the main <phone: PhoneApplicationPage> element:

XAML

  1. xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"

You can now add the following elements inside the main element:

XAML

  1. <toolkit:TransitionService.NavigationInTransition>
  2.     <toolkit:NavigationInTransition>
  3.         <toolkit:NavigationInTransition.Backward>
  4.             <toolkit:TurnstileTransition Mode="BackwardIn"/>
  5.         </toolkit:NavigationInTransition.Backward>
  6.         <toolkit:NavigationInTransition.Forward>
  7.             <toolkit:TurnstileTransition Mode="ForwardIn"/>
  8.         </toolkit:NavigationInTransition.Forward>
  9.     </toolkit:NavigationInTransition>
  10. </toolkit:TransitionService.NavigationInTransition>
  11. <toolkit:TransitionService.NavigationOutTransition>
  12.     <toolkit:NavigationOutTransition>
  13.         <toolkit:NavigationOutTransition.Backward>
  14.             <toolkit:TurnstileTransition Mode="BackwardOut"/>
  15.         </toolkit:NavigationOutTransition.Backward>
  16.         <toolkit:NavigationOutTransition.Forward>
  17.             <toolkit:TurnstileTransition Mode="ForwardOut"/>
  18.         </toolkit:NavigationOutTransition.Forward>
  19.     </toolkit:NavigationOutTransition>
  20. </toolkit:TransitionService.NavigationOutTransition>

Notice the use of the TurnstileTransition.  That’s now going to provide a nice effect whenever we navigate to or from that page.

We need to do something similar, although not exactly the same, in MainPage.xaml.  The ‘toolkit’ namespace is imported the same way and the transitions follow the same pattern but, this time, we use the TurnstileFeatherTransition.

XAML

  1. <toolkit:TransitionService.NavigationInTransition>
  2.     <toolkit:NavigationInTransition>
  3.         <toolkit:NavigationInTransition.Backward>
  4.             <toolkit:TurnstileFeatherTransition Mode="BackwardIn"/>
  5.         </toolkit:NavigationInTransition.Backward>
  6.         <toolkit:NavigationInTransition.Forward>
  7.             <toolkit:TurnstileFeatherTransition Mode="ForwardIn"/>
  8.         </toolkit:NavigationInTransition.Forward>
  9.     </toolkit:NavigationInTransition>
  10. </toolkit:TransitionService.NavigationInTransition>
  11. <toolkit:TransitionService.NavigationOutTransition>
  12.     <toolkit:NavigationOutTransition>
  13.         <toolkit:NavigationOutTransition.Backward>
  14.             <toolkit:TurnstileFeatherTransition Mode="BackwardOut"/>
  15.         </toolkit:NavigationOutTransition.Backward>
  16.         <toolkit:NavigationOutTransition.Forward>
  17.             <toolkit:TurnstileFeatherTransition Mode="ForwardOut"/>
  18.         </toolkit:NavigationOutTransition.Forward>
  19.     </toolkit:NavigationOutTransition>
  20. </toolkit:TransitionService.NavigationOutTransition>

When using TurnstileFeatherTransition, we have to specify the order in which elements are animated.  To do that, we add the TurnstileFeatherEffect.FeatheringIndex attached property to each of the main elements.  Our MainPage.xaml contains a Pivot with two PivotItems that each contain a LongListSelector.  We need to specify that the Pivot is animated first, followed by the LongListSelectors.

Obviously only the contents of one PivotItem will be displayed at a time, so only one LongListSelector will ever be animated.  My first thought was just to set the indexes in sequence, i.e. 0 for the Pivot, 1 for the first LongListSelector, 2 for the second LongListSelector and so on.

XAML

  1. <!--Pivot Control-->
  2. <phone:Pivot Title="MY APPLICATION" toolkit:TurnstileFeatherEffect.FeatheringIndex="0">
  3.     <!--Pivot item one-->
  4.     <phone:PivotItem Header="first">
  5.         <!--Double line list with text wrapping-->
  6.         <phone:LongListSelector x:Name="List1"
  7.                                 Margin="0,0,-12,0"
  8.                                 ItemsSource="{Binding Items}"
  9.                                 toolkit:TurnstileFeatherEffect.FeatheringIndex="1"
  10.                                 SelectionChanged="List1_SelectionChanged">
  11.             <phone:LongListSelector.ItemTemplate>
  12.                 <DataTemplate>
  13.                     <StackPanel Margin="0,0,0,17">
  14.                         <TextBlock Text="{Binding LineOne}"
  15.                                    TextWrapping="Wrap"
  16.                                    Style="{StaticResource PhoneTextExtraLargeStyle}"/>
  17.                         <TextBlock Text="{Binding LineTwo}"
  18.                                    TextWrapping="Wrap"
  19.                                    Margin="12,-6,12,0"
  20.                                    Style="{StaticResource PhoneTextSubtleStyle}"/>
  21.                     </StackPanel>
  22.                 </DataTemplate>
  23.             </phone:LongListSelector.ItemTemplate>
  24.         </phone:LongListSelector>
  25.     </phone:PivotItem>
  26.  
  27.     <!--Pivot item two-->
  28.     <phone:PivotItem Header="second">
  29.         <!--Double line list no text wrapping-->
  30.         <phone:LongListSelector x:Name="List2"
  31.                                 Margin="0,0,-12,0"
  32.                                 ItemsSource="{Binding Items}"
  33.                                 toolkit:TurnstileFeatherEffect.FeatheringIndex="2"
  34.                                 SelectionChanged="List2_SelectionChanged">
  35.             <phone:LongListSelector.ItemTemplate>
  36.                     <DataTemplate>
  37.                         <StackPanel Margin="0,0,0,17">
  38.                             <TextBlock Text="{Binding LineOne}"
  39.                                        TextWrapping="NoWrap"
  40.                                        Margin="12,0,0,0"
  41.                                        Style="{StaticResource PhoneTextExtraLargeStyle}"/>
  42.                             <TextBlock Text="{Binding LineThree}"
  43.                                        TextWrapping="NoWrap"
  44.                                        Margin="12,-6,0,0"
  45.                                        Style="{StaticResource PhoneTextSubtleStyle}"/>
  46.                         </StackPanel>
  47.                     </DataTemplate>
  48.             </phone:LongListSelector.ItemTemplate>
  49.         </phone:LongListSelector>
  50.     </phone:PivotItem>
  51. </phone:Pivot>

We can then handle the SelectionChanged event of each LongListSelector and navigate to the second page to see our transition in action.

C#

  1. private void List1_SelectionChanged(object sender, SelectionChangedEventArgs e)
  2. {
  3.     NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
  4. }
  5.  
  6. private void List2_SelectionChanged(object sender, SelectionChangedEventArgs e)
  7. {
  8.     NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
  9. }

VB

  1. Private Sub List1_SelectionChanged(ByVal sender As Object, ByVal e As SelectionChangedEventArgs)
  2.     NavigationService.Navigate(New Uri("/Page1.xaml", UriKind.Relative))
  3. End Sub
  4.  
  5. Private Sub List2_SelectionChanged(ByVal sender As Object, ByVal e As SelectionChangedEventArgs)
  6.     NavigationService.Navigate(New Uri("/Page1.xaml", UriKind.Relative))
  7. End Sub

If you now run the project and select an item from the first list, you’ll see the intended page transition in all its glory.  The items on the page flip out one after the other in a smooth series and then the next page flips in.  Hit the Back button and the second page flips back and the first page appears again, one item at a time.  All seems wonderful.

Now, select the second list on the pivot page and try the same thing.  Something is not quite right.  You’ll notice that the page title and list headers flip out first, as they should, but then there’s a little bit of a delay before the list items start to flip out.  They still feather as they should but that little delay detracts from the overall UX.  What to do?

Given that the first list animates correctly and the second doesn’t, I wondered whether setting the index of both lists to 1 would do the trick.  That didn’t help, nor did any other combination of static values.  It occurred to me that maybe changing the indexes as the selected PivotItem changes might fix the issue.  As only one list can be visible, and therefore animated, at a time, I figured that it would make sense that only the selected list have a valid index.  An element that doesn’t get animated has an index of –1, so I decided to not set the values in XAML but rather do it dynamically in code instead.

XAML

  1. <!--Pivot Control-->
  2. <phone:Pivot x:Name="MainPivot"
  3.              Title="MY APPLICATION"
  4.              toolkit:TurnstileFeatherEffect.FeatheringIndex="0"
  5.              SelectionChanged="MainPivot_OnSelectionChanged">
  6.     <!--Pivot item one-->
  7.     <phone:PivotItem x:Name="Item1" Header="first">
  8.         <!--Double line list with text wrapping-->
  9.         <phone:LongListSelector x:Name="List1"
  10.                                 Margin="0,0,-12,0"
  11.                                 ItemsSource="{Binding Items}"
  12.                                 SelectionChanged="List1_SelectionChanged">
  13.             <phone:LongListSelector.ItemTemplate>
  14.                 <DataTemplate>
  15.                     <StackPanel Margin="0,0,0,17">
  16.                         <TextBlock Text="{Binding LineOne}"
  17.                                    TextWrapping="Wrap"
  18.                                    Style="{StaticResource PhoneTextExtraLargeStyle}"/>
  19.                         <TextBlock Text="{Binding LineTwo}"
  20.                                    TextWrapping="Wrap"
  21.                                    Margin="12,-6,12,0"
  22.                                    Style="{StaticResource PhoneTextSubtleStyle}"/>
  23.                     </StackPanel>
  24.                 </DataTemplate>
  25.                                     </phone:LongListSelector.ItemTemplate>
  26.         </phone:LongListSelector>
  27.     </phone:PivotItem>
  28.  
  29.     <!--Pivot item two-->
  30.     <phone:PivotItem x:Name="Item2" Header="second">
  31.         <!--Double line list no text wrapping-->
  32.         <phone:LongListSelector x:Name="List2"
  33.                                 Margin="0,0,-12,0"
  34.                                 ItemsSource="{Binding Items}"
  35.                                 SelectionChanged="List2_SelectionChanged">
  36.             <phone:LongListSelector.ItemTemplate>
  37.                 <DataTemplate>
  38.                     <StackPanel Margin="0,0,0,17">
  39.                         <TextBlock Text="{Binding LineOne}"
  40.                                    TextWrapping="NoWrap"
  41.                                    Margin="12,0,0,0"
  42.                                    Style="{StaticResource PhoneTextExtraLargeStyle}"/>
  43.                         <TextBlock Text="{Binding LineThree}"
  44.                                    TextWrapping="NoWrap"
  45.                                    Margin="12,-6,0,0"
  46.                                    Style="{StaticResource PhoneTextSubtleStyle}"/>
  47.                     </StackPanel>
  48.                 </DataTemplate>
  49.             </phone:LongListSelector.ItemTemplate>
  50.         </phone:LongListSelector>
  51.     </phone:PivotItem>
  52. </phone:Pivot>

C#

  1. private void MainPivot_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
  2. {
  3.     var listsByItem = new Dictionary<PivotItem, LongListSelector> {{Item1, List1}, {Item2, List2}};
  4.  
  5.     foreach (PivotItem item in MainPivot.Items)
  6.     {
  7.         TurnstileFeatherEffect.SetFeatheringIndex(listsByItem[item], item == MainPivot.SelectedItem ? 1 : -1);
  8.     }                                                              
  9. }

VB

  1. Private Sub MainPivot_OnSelectionChanged(ByVal sender As Object, ByVal e As SelectionChangedEventArgs)
  2.     Dim listsByItem As New Dictionary(Of PivotItem, LongListSelector) From {{Item1, List1},
  3.                                                                             {Item2, List2}}
  4.  
  5.     For Each item As PivotItem In MainPivot.Items
  6.         TurnstileFeatherEffect.SetFeatheringIndex(listsByItem(item), If(item Is MainPivot.SelectedItem, 1, -1))
  7.     Next
  8. End Sub

With that code, every time the selection in the Pivot changes, the indexes of all the lists get reset.  If a PivotItem is selected then the LongListSelector it contains will have its index set to 1, otherwise the list index will be set to –1.  Run the project now and you’ll see that the animation is as intended for both lists.  My original project has four PivotItems and they all animate correctly so I’m assuming that it will work for an arbitrary number.

If anyone has a better solution then I’d love to hear about it but this seems to do the job well and without too much effort.

3 comments:

Clive107 said...

Nice! It solved my problem. Thanks a lot. :-)

Indy said...

Hey John. I just wanted to say a BIG "Thanks" for all the great posts you have created throughout the years. You've helped me enormously countless times!

All the best!
Bob (Indiana, USA)

Ryan said...

Running into this same problem, I'll try your solution but see if I can figure out what else is going on.