Reputation: 2370
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.
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
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
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
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