Enabling ScrollViewer HorizontalSnapPoints with bindable collection

后端 未结 2 758
情歌与酒
情歌与酒 2021-02-04 09:13

I\'m trying to create a similar experience as in the ScrollViewerSample from the Windows 8 SDK samples to be able to snap to the items inside a ScrollViewer when scrolling left

相关标签:
2条回答
  • 2021-02-04 09:55

    Getting snap points to work for bound collections can be tricky. For snap points to work immediate child of ScrollViewer should implement IScrollSnapPointsInfo interface. ItemsControl doesn't implement IScrollSnapPointsInfo and consequently you wouldn't see snapping behaviour.

    To work around this issue you got couple options:

    • Create custom class derived from ItemsControl and implement IScrollSnapPointsInfo interface.
    • Create custom style for items control and set HorizontalSnapPointsType property on ScrollViewer inside the style.

    I've implemented former approach and can confirm that it works, but in your case custom style could be a better choice.

    0 讨论(0)
  • 2021-02-04 10:08

    Ok, here is the simplest (and standalone) example for horizontal ListView with binded items and correctly working snapping (see comments in following code).

    xaml:

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

    background code:

        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 http://stackoverflow.com/questions/11084493/snapping-scrollviewer-in-windows-8-metro-in-wide-screens-not-snapping-to-the-las)
    
            //** 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;
            }
        }
    

    About this example.

    1. Most of checks and errors handling is omitted.

    2. If you override ListView Style/Template, VisualTree search parts must be changed accordingly

    3. I'd rather create inherited from ListView control with this logic, than use provided example as-is in real code.
    4. Same code works for Vertical case (or both) with small changes.
    5. Mentioned snapping bug - ScrollViewer bug of handling SnapPointsType.MandatorySingle and SnapPointsType.Mandatory cases. It appears for items with not-fixed sizes

    .

    0 讨论(0)
提交回复
热议问题