diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index c5287b6b82c..696298e3c8f 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -1,4 +1,4 @@ - + Debug @@ -439,7 +439,7 @@ - + @@ -626,6 +626,7 @@ + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Models/Sample.cs b/Microsoft.Toolkit.Uwp.SampleApp/Models/Sample.cs index f57a430d307..9be588da6be 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Models/Sample.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/Models/Sample.cs @@ -23,6 +23,7 @@ using Microsoft.Toolkit.Uwp.Helpers; using Microsoft.Toolkit.Uwp.Input.GazeInteraction; using Microsoft.Toolkit.Uwp.SampleApp.Models; +using Microsoft.Toolkit.Uwp.UI; using Microsoft.Toolkit.Uwp.UI.Animations; using Microsoft.Toolkit.Uwp.UI.Controls; using Microsoft.Toolkit.Uwp.UI.Media; @@ -674,8 +675,9 @@ private static Type LookForTypeByName(string typeName) // TODO Reintroduce graph controls // typeof(UserToPersonConverter)) // Search in Microsoft.Toolkit.Graph.Controls + ScrollItemPlacement.Default.GetType(), // Search in Microsoft.Toolkit.Uwp.UI EasingType.Default.GetType(), // Microsoft.Toolkit.Uwp.UI.Animations - ImageBlendMode.Multiply.GetType(), // Search in Microsoft.Toolkit.Uwp.UI + ImageBlendMode.Multiply.GetType(), // Search in Microsoft.Toolkit.Uwp.UI.Media Interaction.Enabled.GetType(), // Microsoft.Toolkit.Uwp.Input.GazeInteraction DataGridGridLinesVisibility.None.GetType(), // Microsoft.Toolkit.Uwp.UI.Controls.DataGrid GridSplitter.GridResizeDirection.Auto.GetType(), // Microsoft.Toolkit.Uwp.UI.Controls.Layout diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsCode.bind index 8d475644188..22f495a7fad 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsCode.bind @@ -1,32 +1,9 @@ - + - - - - +using Microsoft.Toolkit.Uwp.UI; - - - - +// Scrolling with index +await MyGridView.SmoothScrollIntoViewWithIndexAsync(index: int, itemPlacement: ScrollItemPlacement, disableAnimation: bool, scrollIfVisibile: bool, additionalHorizontalOffset: int, additionalVerticalOffset: int); - - - - - - \ No newline at end of file +// Scrolling with item +await MyGridView.SmoothScrollIntoViewWithItemAsync(item: object, itemPlacement: ScrollItemPlacement, disableAnimation: bool, scrollIfVisibile: bool, additionalHorizontalOffset: int, additionalVerticalOffset: int); diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml index 48841b7fa69..ae16844700f 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml @@ -1,32 +1,69 @@ - + + - + - + + + + + + + + - + + + + + Default + Left + Top + Center + Right + Bottom + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml.cs index 474fe362382..b79849ef196 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsPage.xaml.cs @@ -3,9 +3,8 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.ObjectModel; using System.Windows.Input; -using Microsoft.Toolkit.Uwp.SampleApp.Common; -using Microsoft.Toolkit.Uwp.SampleApp.Data; using Microsoft.Toolkit.Uwp.UI; using Windows.UI.Popups; using Windows.UI.Xaml; @@ -15,29 +14,93 @@ namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages { public sealed partial class ListViewExtensionsPage : Page, IXamlRenderListener { + private ListView sampleListView; + public ListViewExtensionsPage() { this.InitializeComponent(); + Load(); } - public ICommand SampleCommand => new DelegateCommand(OnExecuteSampleCommand); - - public async void OnXamlRendered(FrameworkElement control) + public void OnXamlRendered(FrameworkElement control) { - var sampleListView = control.FindChild("SampleListView") as ListView; + sampleListView = control.FindChild("SampleListView") as ListView; if (sampleListView != null) { - sampleListView.ItemsSource = await new Data.PhotosDataSource().GetItemsAsync(); + sampleListView.ItemsSource = GetOddEvenSource(201); } // Transfer Data Context so we can access SampleCommand control.DataContext = this; } - private async void OnExecuteSampleCommand(PhotoDataItem item) + private void Load() + { + SampleController.Current.RegisterNewCommand("Start Smooth Scroll", (sender, args) => + { + var index = int.TryParse(IndexInput.Text, out var i) ? i : 0; + var itemPlacement = ItemPlacementInput.SelectedItem switch + { + "Default" => ScrollItemPlacement.Default, + "Left" => ScrollItemPlacement.Left, + "Top" => ScrollItemPlacement.Top, + "Center" => ScrollItemPlacement.Center, + "Right" => ScrollItemPlacement.Right, + "Bottom" => ScrollItemPlacement.Bottom, + _ => ScrollItemPlacement.Default + }; + + var disableAnimation = DisableAnimationInput.IsChecked ?? false; + var scrollIfVisibile = ScrollIfVisibileInput.IsChecked ?? true; + var additionalHorizontalOffset = int.TryParse(AdditionalHorizontalOffsetInput.Text, out var ho) ? ho : 0; + var additionalVerticalOffset = int.TryParse(AdditionalVerticalOffsetInput.Text, out var vo) ? vo : 0; + sampleListView.SmoothScrollIntoViewWithIndexAsync(index, itemPlacement, disableAnimation, scrollIfVisibile, additionalHorizontalOffset, additionalVerticalOffset); + }); + + if (sampleListView != null) + { + sampleListView.ItemsSource = GetOddEvenSource(500); + } + } + + private ObservableCollection GetOddEvenSource(int count) + { + var oddEvenSource = new ObservableCollection(); + + for (int number = 0; number < count; number++) + { + var item = (number % 2) == 0 ? $"{number} - Even" : $"{number} - Odd"; + oddEvenSource.Add(item); + } + + return oddEvenSource; + } + } + +#pragma warning disable SA1402 // File may only contain a single class + internal class SampleCommand : ICommand +#pragma warning restore SA1402 // File may only contain a single class + { + event EventHandler ICommand.CanExecuteChanged + { + add { } + remove { } + } + + public bool CanExecute(object parameter) => true; + + public void Execute(object parameter) + { + if (parameter is string s) + { + OnExecuteSampleCommand(s); + } + } + + private static async void OnExecuteSampleCommand(string item) { - await new MessageDialog($"You clicked {item.Title} via the 'ListViewExtensions.Command' binding", "Item Clicked").ShowAsync(); + await new MessageDialog($"You clicked {item} via the 'ListViewExtensions.Command' binding", "Item Clicked").ShowAsync(); } } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsXaml.bind new file mode 100644 index 00000000000..0c3c8ea384b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ListViewExtensions/ListViewExtensionsXaml.bind @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json index 437e8a44333..89734b82abe 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json @@ -1105,7 +1105,8 @@ "Type": "ListViewExtensionsPage", "About": "Extensions for all controls that inherit from ListViewBase like ListView.", "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/main/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase", - "XamlCodeFile": "ListViewExtensionsCode.bind", + "CodeFile": "ListViewExtensionsCode.bind", + "XamlCodeFile": "ListViewExtensionsXaml.bind", "Icon": "/Assets/Helpers.png", "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/extensions/ListViewExtensions.md" }, diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.AlternateRows.cs similarity index 84% rename from Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.cs rename to Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.AlternateRows.cs index c9ad2e4cbf9..53d7355371f 100644 --- a/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.cs +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.AlternateRows.cs @@ -28,11 +28,6 @@ public static partial class ListViewExtensions /// public static readonly DependencyProperty AlternateItemTemplateProperty = DependencyProperty.RegisterAttached("AlternateItemTemplate", typeof(DataTemplate), typeof(ListViewExtensions), new PropertyMetadata(null, OnAlternateItemTemplatePropertyChanged)); - /// - /// Attached for setting the container content stretch direction on the - /// - public static readonly DependencyProperty ItemContainerStretchDirectionProperty = DependencyProperty.RegisterAttached("ItemContainerStretchDirection", typeof(ItemContainerStretchDirection), typeof(ListViewExtensions), new PropertyMetadata(null, OnItemContainerStretchDirectionPropertyChanged)); - /// /// Gets the alternate associated with the specified /// @@ -73,26 +68,6 @@ public static void SetAlternateItemTemplate(Windows.UI.Xaml.Controls.ListViewBas obj.SetValue(AlternateItemTemplateProperty, value); } - /// - /// Gets the stretch associated with the specified - /// - /// The to get the associated from - /// The associated with the - public static ItemContainerStretchDirection GetItemContainerStretchDirection(Windows.UI.Xaml.Controls.ListViewBase obj) - { - return (ItemContainerStretchDirection)obj.GetValue(ItemContainerStretchDirectionProperty); - } - - /// - /// Sets the stretch associated with the specified - /// - /// The to associate the with - /// The for binding to the - public static void SetItemContainerStretchDirection(Windows.UI.Xaml.Controls.ListViewBase obj, ItemContainerStretchDirection value) - { - obj.SetValue(ItemContainerStretchDirectionProperty, value); - } - private static void OnAlternateColorPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { Windows.UI.Xaml.Controls.ListViewBase listViewBase = sender as Windows.UI.Xaml.Controls.ListViewBase; diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs new file mode 100644 index 00000000000..419471d959b --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs @@ -0,0 +1,207 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Windows.Foundation; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; + +namespace Microsoft.Toolkit.Uwp.UI +{ + /// + /// Smooth scroll the list to bring specified item into view + /// + public static partial class ListViewExtensions + { + /// + /// Smooth scrolling the list to bring the specified index into view + /// + /// List to scroll + /// The index to bring into view. Index can be negative. + /// Set the item placement after scrolling + /// Set true to disable animation + /// Set false to disable scrolling when the corresponding item is in view + /// Adds additional horizontal offset + /// Adds additional vertical offset + /// Note: Even though this return , it will not wait until the scrolling completes + public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase listViewBase, int index, ScrollItemPlacement itemPlacement = ScrollItemPlacement.Default, bool disableAnimation = false, bool scrollIfVisible = true, int additionalHorizontalOffset = 0, int additionalVerticalOffset = 0) + { + if (index > (listViewBase.Items.Count - 1)) + { + index = listViewBase.Items.Count - 1; + } + + if (index < -listViewBase.Items.Count) + { + index = -listViewBase.Items.Count; + } + + index = (index < 0) ? (index + listViewBase.Items.Count) : index; + + bool isVirtualizing = default; + double previousXOffset = default, previousYOffset = default; + + var scrollViewer = listViewBase.FindDescendant(); + var selectorItem = listViewBase.ContainerFromIndex(index) as SelectorItem; + + // If selectorItem is null then the panel is virtualized. + // So in order to get the container of the item we need to scroll to that item first and then use ContainerFromIndex + if (selectorItem == null) + { + isVirtualizing = true; + + previousXOffset = scrollViewer.HorizontalOffset; + previousYOffset = scrollViewer.VerticalOffset; + + var tcs = new TaskCompletionSource(); + + void ViewChanged(object obj, ScrollViewerViewChangedEventArgs args) => tcs.TrySetResult(result: null); + + try + { + scrollViewer.ViewChanged += ViewChanged; + listViewBase.ScrollIntoView(listViewBase.Items[index], ScrollIntoViewAlignment.Leading); + await tcs.Task; + } + finally + { + scrollViewer.ViewChanged -= ViewChanged; + } + + selectorItem = (SelectorItem)listViewBase.ContainerFromIndex(index); + } + + var transform = selectorItem.TransformToVisual((UIElement)scrollViewer.Content); + var position = transform.TransformPoint(new Point(0, 0)); + + // Scrolling back to previous position + if (isVirtualizing) + { + var tcs = new TaskCompletionSource(); + + void ViewChanged(object obj, ScrollViewerViewChangedEventArgs args) => tcs.TrySetResult(result: null); + + try + { + scrollViewer.ViewChanged += ViewChanged; + scrollViewer.ChangeView(previousXOffset, previousYOffset, zoomFactor: null, disableAnimation: true); + await tcs.Task; + } + finally + { + scrollViewer.ViewChanged -= ViewChanged; + } + } + + var listViewBaseWidth = listViewBase.ActualWidth; + var selectorItemWidth = selectorItem.ActualWidth; + var listViewBaseHeight = listViewBase.ActualHeight; + var selectorItemHeight = selectorItem.ActualHeight; + + previousXOffset = scrollViewer.HorizontalOffset; + previousYOffset = scrollViewer.VerticalOffset; + + var minXPosition = position.X - listViewBaseWidth + selectorItemWidth; + var minYPosition = position.Y - listViewBaseHeight + selectorItemHeight; + + var maxXPosition = position.X; + var maxYPosition = position.Y; + + double finalXPosition, finalYPosition; + + // If the Item is in view and scrollIfVisible is false then we don't need to scroll + if (!scrollIfVisible && (previousXOffset <= maxXPosition && previousXOffset >= minXPosition) && (previousYOffset <= maxYPosition && previousYOffset >= minYPosition)) + { + finalXPosition = previousXOffset; + finalYPosition = previousYOffset; + } + else + { + switch (itemPlacement) + { + case ScrollItemPlacement.Default: + if (previousXOffset <= maxXPosition && previousXOffset >= minXPosition) + { + finalXPosition = previousXOffset + additionalHorizontalOffset; + } + else if (Math.Abs(previousXOffset - minXPosition) < Math.Abs(previousXOffset - maxXPosition)) + { + finalXPosition = minXPosition + additionalHorizontalOffset; + } + else + { + finalXPosition = maxXPosition + additionalHorizontalOffset; + } + + if (previousYOffset <= maxYPosition && previousYOffset >= minYPosition) + { + finalYPosition = previousYOffset + additionalVerticalOffset; + } + else if (Math.Abs(previousYOffset - minYPosition) < Math.Abs(previousYOffset - maxYPosition)) + { + finalYPosition = minYPosition + additionalVerticalOffset; + } + else + { + finalYPosition = maxYPosition + additionalVerticalOffset; + } + + break; + + case ScrollItemPlacement.Left: + finalXPosition = maxXPosition + additionalHorizontalOffset; + finalYPosition = previousYOffset + additionalVerticalOffset; + break; + + case ScrollItemPlacement.Top: + finalXPosition = previousXOffset + additionalHorizontalOffset; + finalYPosition = maxYPosition + additionalVerticalOffset; + break; + + case ScrollItemPlacement.Center: + var centreX = (listViewBaseWidth - selectorItemWidth) / 2.0; + var centreY = (listViewBaseHeight - selectorItemHeight) / 2.0; + finalXPosition = maxXPosition - centreX + additionalHorizontalOffset; + finalYPosition = maxYPosition - centreY + additionalVerticalOffset; + break; + + case ScrollItemPlacement.Right: + finalXPosition = minXPosition + additionalHorizontalOffset; + finalYPosition = previousYOffset + additionalVerticalOffset; + break; + + case ScrollItemPlacement.Bottom: + finalXPosition = previousXOffset + additionalHorizontalOffset; + finalYPosition = minYPosition + additionalVerticalOffset; + break; + + default: + finalXPosition = previousXOffset + additionalHorizontalOffset; + finalYPosition = previousYOffset + additionalVerticalOffset; + break; + } + } + + scrollViewer.ChangeView(finalXPosition, finalYPosition, zoomFactor: null, disableAnimation); + } + + /// + /// Smooth scrolling the list to bring the specified data item into view + /// + /// List to scroll + /// The data item to bring into view + /// Set the item placement after scrolling + /// Set true to disable animation + /// Set true to disable scrolling when the corresponding item is in view + /// Adds additional horizontal offset + /// Adds additional vertical offset + /// Note: Even though this return , it will not wait until the scrolling completes + public static async Task SmoothScrollIntoViewWithItemAsync(this ListViewBase listViewBase, object item, ScrollItemPlacement itemPlacement = ScrollItemPlacement.Default, bool disableAnimation = false, bool scrollIfVisibile = true, int additionalHorizontalOffset = 0, int additionalVerticalOffset = 0) + { + await SmoothScrollIntoViewWithIndexAsync(listViewBase, listViewBase.Items.IndexOf(item), itemPlacement, disableAnimation, scrollIfVisibile, additionalHorizontalOffset, additionalVerticalOffset); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.StretchItemContainer.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.StretchItemContainer.cs new file mode 100644 index 00000000000..3097c02fbb9 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ListViewExtensions.StretchItemContainer.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Media; + +namespace Microsoft.Toolkit.Uwp.UI +{ + /// + /// Provides attached dependency properties for the + /// + public static partial class ListViewExtensions + { + /// + /// Attached for setting the container content stretch direction on the + /// + public static readonly DependencyProperty ItemContainerStretchDirectionProperty = DependencyProperty.RegisterAttached("ItemContainerStretchDirection", typeof(ItemContainerStretchDirection), typeof(ListViewExtensions), new PropertyMetadata(null, OnItemContainerStretchDirectionPropertyChanged)); + + /// + /// Gets the stretch associated with the specified + /// + /// The to get the associated from + /// The associated with the + public static ItemContainerStretchDirection GetItemContainerStretchDirection(Windows.UI.Xaml.Controls.ListViewBase obj) + { + return (ItemContainerStretchDirection)obj.GetValue(ItemContainerStretchDirectionProperty); + } + + /// + /// Sets the stretch associated with the specified + /// + /// The to associate the with + /// The for binding to the + public static void SetItemContainerStretchDirection(Windows.UI.Xaml.Controls.ListViewBase obj, ItemContainerStretchDirection value) + { + obj.SetValue(ItemContainerStretchDirectionProperty, value); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ScrollItemPlacement.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ScrollItemPlacement.cs new file mode 100644 index 00000000000..4b5e2a583fd --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI/Extensions/ListViewBase/ScrollItemPlacement.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Toolkit.Uwp.UI +{ + /// + /// Item Position + /// + public enum ScrollItemPlacement + { + /// + /// If visible then it will not scroll, if not then item will be aligned to the nearest edge + /// + Default, + + /// + /// Aligned left + /// + Left, + + /// + /// Aligned top + /// + Top, + + /// + /// Aligned center + /// + Center, + + /// + /// Aligned right + /// + Right, + + /// + /// Aligned bottom + /// + Bottom + } +}