使用可绑定集合启用ScrollViewer的HorizontalSnapPoints

13

我正在尝试创建与Windows 8 SDK示例中的ScrollViewerSample类似的体验,以便在水平滚动时可以捕捉到ScrollViewer内的项目。该示例的实现方式(有效)如下:

<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" 
              ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
    <StackPanel Orientation="Horizontal">
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None"  HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    </StackPanel>
</ScrollViewer>

我想实现的功能与所需实现的功能唯一的区别在于,我不想使用带有内部项目的StackPanel,而是想使用可以绑定的东西。我试图使用ItemsControl来完成这个目标,但出现了Snap行为无法启动的问题:

<ScrollViewer x:Name="scrollViewer" Width="480" Height="270"
              HorizontalAlignment="Left" VerticalAlignment="Top"
              VerticalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Auto" 
              ZoomMode="Disabled" HorizontalSnapPointsType="Mandatory">
    <ItemsControl>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a cliff" Source="images/cliff.jpg" Stretch="None"  HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Grapes" Source="images/grapes.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of Mount Rainier" Source="images/Rainier.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a sunset" Source="images/sunset.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
        <Image Width="480" Height="270" AutomationProperties.Name="Image of a valley" Source="images/valley.jpg" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/>
    </ItemsControl>
</ScrollViewer>

非常感谢Denis,最终我采用了以下样式在ItemsControl上,并完全移除了ScrollViewer和内联的ItemsPanelTemplate:

<Style x:Key="ItemsControlStyle" TargetType="ItemsControl">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ItemsControl">
                <ScrollViewer Style="{StaticResource HorizontalScrollViewerStyle}" HorizontalSnapPointsType="Mandatory">
                    <ItemsPresenter />
                </ScrollViewer>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
2个回答

14

让绑定集合的捕捉点工作可能有些棘手。为了使捕捉点起作用,即滚动视图容器的直接子元素应实现IScrollSnapPointsInfo接口。ItemsControl不实现IScrollSnapPointsInfo,因此您将看不到捕捉行为。

要解决此问题,您有几个选项:

  • 创建自定义类,派生自ItemsControl并实现IScrollSnapPointsInfo接口。
  • 为项目控件创建自定义样式,并在样式中设置ScrollViewer的HorizontalSnapPointsType属性。

我已经实施了前一种方法,并可以确认它有效,但在您的情况下,自定义样式可能是更好的选择。


4
可以给我们提供一个例子吗? - yalematta

1
Ok,这里是一个最简单(并且独立的)水平ListView的例子,它具有绑定项和正确工作的对齐功能(请参见以下代码中的注释)。 :
    <ListView x:Name="YourListView"
              ItemsSource="{x:Bind Path=Items}"
              Loaded="YourListView_OnLoaded">
        <!--Set items panel to horizontal-->
        <ListView.ItemsPanel>
            <ItemsPanelTemplate>
                <ItemsStackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListView.ItemsPanel>
        <!--Some item template-->
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding}"/>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>

背景代码:

    private void YourListView_OnLoaded(object sender, RoutedEventArgs e)
    {
        //get ListView
        var yourList = sender as ListView;

        //*** yourList style-based changes ***
        //see Style here https://msdn.microsoft.com/en-us/library/windows/apps/mt299137.aspx

        //** Change orientation of scrollviewer (name in the Style "ScrollViewer") **
        //1. get scrollviewer (child element of yourList)
        var sv = GetFirstChildDependencyObjectOfType<ScrollViewer>(yourList);

        //2. enable ScrollViewer horizontal scrolling
        sv.HorizontalScrollMode =ScrollMode.Auto;
        sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
        sv.IsHorizontalRailEnabled = true;

        //3. disable ScrollViewer vertical scrolling
        sv.VerticalScrollMode = ScrollMode.Disabled;
        sv.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
        sv.IsVerticalRailEnabled = false;
        // //no we have horizontally scrolling ListView


        //** Enable snapping **
        sv.HorizontalSnapPointsType = SnapPointsType.MandatorySingle; //or you can use SnapPointsType.Mandatory
        sv.HorizontalSnapPointsAlignment = SnapPointsAlignment.Near; //example works only for Near case, for other there should be some changes
        // //no we have horizontally scrolling ListView with snapping and "scroll last item into view" bug (about bug see here https://dev59.com/smXWa4cB1Zd3GeqPJhqD)

        //** fix "scroll last item into view" bug **
        //1. Get items presenter (child element of yourList)
        var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(yourList);
        //  or   var ip = GetFirstChildDependencyObjectOfType<ItemsPresenter>(sv); //also will work here

        //2. Subscribe to its SizeChanged event
        ip.SizeChanged += ip_SizeChanged;

        //3. see the continuation in: private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
    }


    public static T GetFirstChildDependencyObjectOfType<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj is T) return depObj as T;

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);

            var result = GetFirstChildDependencyObjectOfType<T>(child);
            if (result != null) return result;
        }
        return null;
    }

    private void ip_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        //3.0 if rev size is same as new - do nothing
        //here should be one more condition added by && but it is a little bit complicated and rare, so it is omitted.
        //The condition is: yourList.Items.Last() must be equal to (yourList.Items.Last() used on previous call of ip_SizeChanged)
        if (e.PreviousSize.Equals(e.NewSize)) return;

        //3.1 get sender as our ItemsPresenter
        var ip = sender as ItemsPresenter;

        //3.2 get the ItemsPresenter parent to get "viewable" width of ItemsPresenter that is ActualWidth of the Scrollviewer (it is scrollviewer actually, but we need just its ActualWidth so - as FrameworkElement is used)
        var sv = ip.Parent as FrameworkElement;

        //3.3 get parent ListView to be able to get elements Containers
        var yourList = GetParent<ListView>(ip);

        //3.4 get last item ActualWidth
        var lastItem = yourList.Items.Last();
        var lastItemContainerObject = yourList.ContainerFromItem(lastItem);
        var lastItemContainer = lastItemContainerObject as FrameworkElement;
        if (lastItemContainer == null)
        {
            //NO lastItemContainer YET, wait for next call
            return;
        }
        var lastItemWidth = lastItemContainer.ActualWidth;

        //3.5 get margin fix value
        var rightMarginFixValue = sv.ActualWidth - lastItemWidth;

        //3.6. fix  "scroll last item into view" bug
        ip.Margin = new Thickness(ip.Margin.Left, 
            ip.Margin.Top, 
            ip.Margin.Right + rightMarginFixValue, //APPLY FIX
            ip.Margin.Bottom);
    }

    public static T GetParent<T>(DependencyObject reference) where T : class
    {
        var depObj = VisualTreeHelper.GetParent(reference);
        if (depObj == null) return (T)null;
        while (true)
        {
            var depClass = depObj as T;
            if (depClass != null) return depClass;
            depObj = VisualTreeHelper.GetParent(depObj);
            if (depObj == null) return (T)null;
        }
    }

关于这个示例。

  1. 大多数检查和错误处理被省略了。

  2. 如果您覆盖ListView的样式/模板,则必须相应更改VisualTree搜索部分。

  3. 我宁愿创建继承自ListView控件并具有此逻辑,而不是在实际代码中使用提供的示例。
  4. 同样的代码对于垂直情况(或两种情况)也适用,只需进行小的更改即可。
  5. 提到的捕捉错误-ScrollViewer在处理SnapPointsType.MandatorySingle和SnapPointsType.Mandatory情况时出现错误。它适用于大小不固定的项目。

.


谢谢提供这个示例,对于这个重要的问题有它确实很好。顺便说一下,代码背后并不是必需的。您可以使用样式来设置在代码背后设置的所有属性。(不过我不知道你提到的错误) - MB.

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