altso
altso

Reputation: 2370

ListView in Flyout transition issue

I'm using a grouped ListView inside a Flyout and get weird UI issue with group header when popup opens. It happens for a fraction of second, but still notable by most of the users.

header animation bug

XAML (excerpt from full repro sample http://ge.tt/1DWlXbq1/v/0?c):

<Page.Resources>
    <DataTemplate x:Key="GroupHeaderTemplate">
        <ContentControl Content="{Binding Key}"
                        FontWeight="Bold"
                        FontSize="{ThemeResource TextStyleLargeFontSize}"
                        Foreground="{ThemeResource PhoneAccentBrush}"
                        Margin="0 20" />
    </DataTemplate>
    <CollectionViewSource x:Key="ItemsViewSource"
                          IsSourceGrouped="True"
                          Source="{Binding Items}" />
</Page.Resources>

<Page.BottomAppBar>
    <CommandBar>
        <AppBarButton Icon="Caption">
            <AppBarButton.Flyout>
                <Flyout>
                    <ListView ItemsSource="{Binding Source={StaticResource ItemsViewSource}}"
                              Margin="20 0">
                        <ListView.GroupStyle>
                            <GroupStyle HeaderTemplate="{StaticResource GroupHeaderTemplate}" />
                        </ListView.GroupStyle>
                    </ListView>
                </Flyout>
            </AppBarButton.Flyout>
        </AppBarButton>
    </CommandBar>
</Page.BottomAppBar>

I cannot use built-in ListPickerFlyout as it doesn't support grouping.

I tried to find corresponding storyboard or transition in default styles for ListView/Flyout, but wasn't able to.

I'd like to fix that animation or disable it at all. Any help is appreciated.

Upvotes: 3

Views: 1376

Answers (3)

altso
altso

Reputation: 2370

As it turned out, weird animation comes from ItemsStackPanel. Therefore, if (and only if) virtualization is not required, it's possible to specify StackPanel as ItemsPanel:

<Flyout>
    <ListView ItemsSource="{Binding Source={StaticResource ItemsViewSource}}"
              Margin="20 0">
        <ListView.GroupStyle>
            <GroupStyle HeaderTemplate="{StaticResource GroupHeaderTemplate}" />
        </ListView.GroupStyle>
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel />
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
    </ListView>
</Flyout>

Upvotes: 0

Justin XL
Justin XL

Reputation: 39006

One way to get rid of the weird animation bug is to let the Flyout control's animation run first, and then after the animation finishes, show the ListView.

To do this, you need to subscribe to the following events in the Flyout control. Also you need to give the ListView a name and set its Opacity to 0 to start with.

   <Flyout Opened="Flyout_Opened" Closed="Flyout_Closed">
       <ListView x:Name="MyListView" Opacity="0" ItemsSource="{Binding Source={StaticResource ItemsViewSource}}" Margin="20 0">

Then in the code behind, you show the ListView after a short delay. I created a little Opacity animation for the ListView just to make the whole transition run more smoothly. Each time the Flyout is closed, we reset the ListView back to invisible.

private async void Flyout_Opened(object sender, object e)
{
    // a short delay to allow the Flyout in animation to take place
    await Task.Delay(400);

    // animate in the ListView
    var animation = new DoubleAnimation
    {
        Duration = TimeSpan.FromMilliseconds(200),
        To = 1
    };
    Storyboard.SetTarget(animation, this.MyListView);
    Storyboard.SetTargetProperty(animation, "Opacity");

    var storyboard = new Storyboard();
    storyboard.Children.Add(animation);
    storyboard.Begin();
}

private void Flyout_Closed(object sender, object e)
{
    this.MyListView.Opacity = 0;
}

However, having provided a possible solution, I don't think using a Flyout control to change the visual style is the right approach here.

The Flyout control is not designed to handle large amount of data. It doesn't support virtualization (I think). For example, if you increase the item count from 30 to 300, it's going to take quite a few seconds to load once you hit the button.

Update (Working sample included)

I was thinking maybe I could create a control that handles all this, 'cause at the end of the day you do want to be able to retrieve the item that you clicked in the list as well as close the popup.

Unfortunately ListPickerFlyout is sealed so I chose to create a control that inherits from Flyout.

It's quite straight forward. Basically the control exposes properties like ItemsSource, SelectedItem, etc. Also it subscribes to the ItemClick event of the ListView so whenever an item is clicked, it closes the Flyout and populate the SelectedItem.

public class ListViewFlyout : Flyout
{
    private ListView _listView;

    public object ItemsSource
    {
        get { return (object)GetValue(ItemsSourceProperty); }
        set { SetValue(ItemsSourceProperty, value); }
    }

    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(object), typeof(ListViewFlyout), new PropertyMetadata(null));

    public DataTemplate HeaderTemplate
    {
        get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
        set { SetValue(HeaderTemplateProperty, value); }
    }

    public static readonly DependencyProperty HeaderTemplateProperty =
        DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate), typeof(ListViewFlyout), new PropertyMetadata(null));

    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate)GetValue(ItemTemplateProperty); }
        set { SetValue(ItemTemplateProperty, value); }
    }

    public static readonly DependencyProperty ItemTemplateProperty =
        DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(ListViewFlyout), new PropertyMetadata(null));

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(ListViewFlyout), new PropertyMetadata(null));

    public ListViewFlyout()
    {
        // initialization
        this.Placement = FlyoutPlacementMode.Full;
        _listView = new ListView
        {
            Opacity = 0,
            IsItemClickEnabled = true
        };

        this.Opened += ListViewFlyout_Opened;
        this.Closed += ListViewFlyout_Closed;
    }

    private async void ListViewFlyout_Opened(object sender, object e)
    {
        await Task.Delay(400);

        if (!_listView.Items.Any())
        {
            // assign the listView as the Content of this 'custom control'
            _listView.ItemsSource = this.ItemsSource;
            _listView.ItemTemplate = this.ItemTemplate;
            _listView.GroupStyle.Add(new GroupStyle { HeaderTemplate = this.HeaderTemplate });
            this.Content = _listView;

            // whenever an item is clicked, we close the Layout and assign the SelectedItem
            _listView.ItemClick += ListView_ItemClick;
        }

        // animate in the list
        var animation = new DoubleAnimation
        {
            Duration = TimeSpan.FromMilliseconds(200),
            To = 1
        };
        Storyboard.SetTarget(animation, _listView);
        Storyboard.SetTargetProperty(animation, "Opacity");
        var storyboard = new Storyboard();
        storyboard.Children.Add(animation);
        storyboard.Begin();
    }

    private void ListViewFlyout_Closed(object sender, object e)
    {
        _listView.Opacity = 0;
    }

    private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
    {
        this.SelectedItem = e.ClickedItem;
        this.Hide();

        // to be removed
        await Task.Delay(1000);
        var dialog = new MessageDialog(e.ClickedItem.ToString() + " was clicked 1 sec ago!");
        await dialog.ShowAsync();
    }
}

The xaml becomes as simple as this.

    <AppBarButton Icon="Caption">
        <AppBarButton.Flyout>
            <local:ListViewFlyout ItemsSource="{Binding Source={StaticResource ItemsViewSource}}" ItemTemplate="{StaticResource ListViewItemTemplate}" HeaderTemplate="{StaticResource GroupHeaderTemplate}" FlyoutPresenterStyle="{StaticResource FlyoutPresenterStyle}" />
        </AppBarButton.Flyout>
    </AppBarButton>

Note in the FlyoutPresenterStyle style I have created a Title for the popup too.

I have also included a fully functional sample here.

Upvotes: 3

Lucas Loegel
Lucas Loegel

Reputation: 101

I have the same issue and I found a workaround. I find performances are pretty poor, even without the workaround. It takes about 1s to load completely on my device (Lumia 920).

I believe the responsible is the ItemsStackPanel, which is the default ItemsPanel for the ListView. When I use another panel the issue does not occurs. However the scrollviewer offset does not reset when I close and reopen the flyout, therefore I have to do it manually.

So I used a VirtalizingStackPanel in order to keep the virtualization.

<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel />
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

And when the ListView loads, we find the ListView ScrollViewer, and scroll to the top.

private void ListView_Loaded(object sender, RoutedEventArgs e)
    {
        var listView = (ListView)sender;            
        var scrollviewer = listView.FindFirstChild<ScrollViewer>();
        scrollviewer.ScrollToVerticalOffset(0);
    }

listView.FindFirstChild<ScrollViewer>(); is just a helper I have to find child controls with VisualTreeHelper.GetChild.

I find this solution a little better in terms of performance : I set the ListView Visibility to Collapsed by default :

<ListView Visibility="Collapsed" />

I subscribe to the Flyout Opened and Closed events :

<Flyout Placement="Full"
        Opened="Flyout_Opened"
        Closed="Flyout_Closed" />

Then, when the flyout open, I change the visibility after 400ms.

private async void Flyout_Opened(object sender, object e)
    {
        await Task.Delay(400);
        var listView = (ListView)((Flyout)sender).Content;
        listView.Visibility = Windows.UI.Xaml.Visibility.Visible;
    }

    private void Flyout_Closed(object sender, object e)
    {
        var listView = (ListView)((Flyout)sender).Content;
        listView.Visibility = Windows.UI.Xaml.Visibility.Collapsed;
    }

Also, by default, the flyout has a ScrollViewer, which will break the virtualization. You need to remove it from the FlyoutPresenter control Template, or disable it with ScrollViewer.VerticalScrollMode.

Upvotes: 1

Related Questions