Flyout中ListView转换问题

3
我正在使用分组的 ListView 放在 Flyout 中,当弹出框打开时,分组标题会出现奇怪的UI问题。这只发生了一瞬间,但大多数用户仍然能够注意到。

header animation bug

XAML 代码(从完整的重现示例中摘录,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>

我无法使用内置的ListPickerFlyout,因为它不支持分组。

我尝试在默认样式中查找ListView/Flyout的对应故事板或转换,但未能成功。

我想修复该动画或完全禁用它。任何帮助都将不胜感激。


你是说你已经查看了所有的动画,还是只是找不到那个特定事物的动画? - Chris W.
@ChrisW。我找不到那个特定事物的那一个。 - altso
3个回答

3

解决奇怪的动画错误的一种方法是先让Flyout控件的动画运行,等动画结束后再显示ListView。为此,您需要在Flyout控件中订阅以下事件。同时,您需要给ListView命名并将其Opacity设置为0以开始。

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

然后,在代码后台,您需要在短暂延迟后显示ListView。我为ListView创建了一个小的Opacity动画,以使整个过渡更加流畅。每次关闭Flyout时,我们将ListView重置为不可见状态。

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;
}

然而,虽然提供了一种可能的解决方案,但我认为在这里使用 Flyout 控件来更改视觉样式并不是正确的方法。 Flyout 控件没有设计用于处理大量数据。它不支持虚拟化(我想)。例如,如果将项目数从 30 增加到 300,单击按钮后加载需要相当长的时间。
更新(包含工作示例)
我在想也许我可以创建一个控件来处理所有这些,因为最终你确实希望能够检索你在列表中单击的项目以及关闭弹出窗口。
不幸的是,ListPickerFlyout 是密封的,所以我选择创建一个从 Flyout 继承的控件。
这很简单。基本上,该控件公开像 ItemsSourceSelectedItem 等属性。此外,它订阅 ListViewItemClick 事件,因此每当单击项目时,它就会关闭 Flyout 并填充 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();
    }
}

这个xaml变得非常简单,只有这些内容。
    <AppBarButton Icon="Caption">
        <AppBarButton.Flyout>
            <local:ListViewFlyout ItemsSource="{Binding Source={StaticResource ItemsViewSource}}" ItemTemplate="{StaticResource ListViewItemTemplate}" HeaderTemplate="{StaticResource GroupHeaderTemplate}" FlyoutPresenterStyle="{StaticResource FlyoutPresenterStyle}" />
        </AppBarButton.Flyout>
    </AppBarButton>

请注意,在FlyoutPresenterStyle样式中,我还为弹出窗口创建了一个Title(标题)。

我还在此处包含了一个完全功能的示例,链接


我知道如何更改项目的外观,并寻找一种具有分组功能的选择器显示方式。唯一的问题是标准的“ListPickerFlyout”不支持该功能。 - altso
谢谢。我明白了你的意思。然而,当我在模拟器上第一次打开flyout时,动画仍然存在问题。我相信时间应该进一步调整。 - altso
嘿@altso,你是在真机上测试过这个应用程序还是在没有调试器的模拟器上运行的?因为当调试器连接时,运行应用程序会严重影响性能。我已经在520、920和1020上测试过了,都可以正常工作。 - Justin XL

1
我有同样的问题,并找到了一个解决方法。 即使没有这个解决方法,我也发现性能相当差。在我的设备(Lumia 920)上完全加载需要约1秒钟。 我认为问题出在ItemsStackPanel,它是ListView的默认ItemsPanel。当我使用另一个面板时,问题就不会发生。但是,当我关闭并重新打开flyout时,scrollviewer offset没有重置,因此我必须手动重置。 所以我使用了一个VirtalizingStackPanel来保持虚拟化。
<ListView.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel />
    </ItemsPanelTemplate>
</ListView.ItemsPanel>

当ListView加载时,我们找到ListView ScrollViewer,并滚动到顶部。
private void ListView_Loaded(object sender, RoutedEventArgs e)
    {
        var listView = (ListView)sender;            
        var scrollviewer = listView.FindFirstChild<ScrollViewer>();
        scrollviewer.ScrollToVerticalOffset(0);
    }

listView.FindFirstChild<ScrollViewer>();只是一个帮助我使用VisualTreeHelper.GetChild查找子控件的工具。

我觉得这个解决方案在性能方面略微好一些: 我默认将ListView的可见性设置为Collapsed:

<ListView Visibility="Collapsed" />

我订阅了Flyout的OpenedClosed事件:

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

然后,当弹出菜单打开时,我会在400毫秒后更改可见性。

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;
    }

此外,默认情况下,Flyout具有一个 ScrollViewer,这将破坏虚拟化。您需要从 FlyoutPresenter 控件模板中删除它,或使用 ScrollViewer.VerticalScrollMode 禁用它。

看起来只需设置 FlyoutPresenterScrollViewer.VerticalScrollMode 就足以启用虚拟化。感谢这个提示。 - altso

0

事实证明,奇怪的动画来自于ItemsStackPanel。因此,仅当不需要虚拟化时,可以将StackPanel指定为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>

还有一件需要注意的事情是,您也将失去粘性标题。 :) - Justin XL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接