From 82d8634750c2f0c481bedf5119f446ec8945362b Mon Sep 17 00:00:00 2001 From: Violet Hansen Date: Fri, 28 Feb 2025 10:28:19 +0200 Subject: [PATCH] Added CenterHorizontally and CenterVertically Added CenterHorizontally and CenterVertically Fixes this issue: https://github.com/CommunityToolkit/Windows/issues/647 --- ...ListViewExtensions.SmoothScrollIntoView.cs | 235 +++++++++++++++++- .../src/ListViewBase/ScrollItemPlacement.cs | 12 +- 2 files changed, 245 insertions(+), 2 deletions(-) diff --git a/components/Extensions/src/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs b/components/Extensions/src/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs index 8e529f50..ff01a5ab 100644 --- a/components/Extensions/src/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs +++ b/components/Extensions/src/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs @@ -9,6 +9,230 @@ namespace CommunityToolkit.WinUI; /// public static partial class ListViewExtensions { + #region New Horizontal/Vertical Centering Methods + + /// + /// Smooth scrolling the list to bring the specified index into view, centering it horizontally. + /// + /// List to scroll + /// The index to bring into view. Index can be negative. + /// Set true to disable animation + /// Set false to disable scrolling when the corresponding item is in view horizontally + /// Adds additional horizontal offset + /// Returns that completes after scrolling + public static async Task SmoothScrollHorizontallyIntoViewWithIndexAsync(this ListViewBase listViewBase, int index, bool disableAnimation = false, bool scrollIfVisible = true, int additionalHorizontalOffset = 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 (scrollViewer == null) + { + return; + } + + // If selectorItem is null then the panel is virtualized. + // Scroll into view to materialize the container. + if (selectorItem == null) + { + isVirtualizing = true; + previousXOffset = scrollViewer.HorizontalOffset; + previousYOffset = scrollViewer.VerticalOffset; + + var tcs = new TaskCompletionSource(); + void ViewChanged(object? _, ScrollViewerViewChangedEventArgs __) => tcs.TrySetResult(result: default); + + 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)); + + // If we had to scroll to materialize the item, scroll back to the previous view. + if (isVirtualizing) + { + await scrollViewer.ChangeViewAsync(previousXOffset, previousYOffset, zoomFactor: null, disableAnimation: true); + } + + 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 maxXPosition = position.X; + double finalXPosition, finalYPosition; + + // Check horizontal visibility; if the item is already in view, no horizontal scrolling is needed. + if (!scrollIfVisible && (previousXOffset <= maxXPosition && previousXOffset >= minXPosition)) + { + finalXPosition = previousXOffset; + } + else + { + var centreX = (listViewBaseWidth - selectorItemWidth) / 2.0; + finalXPosition = maxXPosition - centreX + additionalHorizontalOffset; + } + + // Keep vertical position unchanged. + finalYPosition = previousYOffset; + + await scrollViewer.ChangeViewAsync(finalXPosition, finalYPosition, zoomFactor: null, disableAnimation); + } + + /// + /// Smooth scrolling the list to bring the specified data item into view, centering it horizontally. + /// + /// List to scroll + /// The data item to bring into view + /// Set true to disable animation + /// Set false to disable scrolling when the corresponding item is in view horizontally + /// Adds additional horizontal offset + /// Returns that completes after scrolling + public static async Task SmoothScrollHorizontallyIntoViewWithItemAsync(this ListViewBase listViewBase, object item, bool disableAnimation = false, bool scrollIfVisible = true, int additionalHorizontalOffset = 0) + { + await SmoothScrollHorizontallyIntoViewWithIndexAsync(listViewBase, listViewBase.Items.IndexOf(item), disableAnimation, scrollIfVisible, additionalHorizontalOffset); + } + + /// + /// Smooth scrolling the list to bring the specified index into view, centering it vertically. + /// + /// List to scroll + /// The index to bring into view. Index can be negative. + /// Set true to disable animation + /// Set false to disable scrolling when the corresponding item is in view vertically + /// Adds additional vertical offset + /// Returns that completes after scrolling + public static async Task SmoothScrollVerticallyIntoViewWithIndexAsync(this ListViewBase listViewBase, int index, bool disableAnimation = false, bool scrollIfVisible = true, 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 (scrollViewer == null) + { + return; + } + + // If selectorItem is null then the panel is virtualized. + // Scroll into view to materialize the container. + if (selectorItem == null) + { + isVirtualizing = true; + previousXOffset = scrollViewer.HorizontalOffset; + previousYOffset = scrollViewer.VerticalOffset; + + var tcs = new TaskCompletionSource(); + void ViewChanged(object? _, ScrollViewerViewChangedEventArgs __) => tcs.TrySetResult(result: default); + + 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)); + + // If we had to scroll to materialize the item, scroll back to the previous view. + if (isVirtualizing) + { + await scrollViewer.ChangeViewAsync(previousXOffset, previousYOffset, zoomFactor: null, disableAnimation: true); + } + + var listViewBaseWidth = listViewBase.ActualWidth; + var selectorItemWidth = selectorItem.ActualWidth; + var listViewBaseHeight = listViewBase.ActualHeight; + var selectorItemHeight = selectorItem.ActualHeight; + + previousXOffset = scrollViewer.HorizontalOffset; + previousYOffset = scrollViewer.VerticalOffset; + + var minYPosition = position.Y - listViewBaseHeight + selectorItemHeight; + var maxYPosition = position.Y; + double finalXPosition, finalYPosition; + + // Check vertical visibility; if the item is already in view, no vertical scrolling is needed. + if (!scrollIfVisible && (previousYOffset <= maxYPosition && previousYOffset >= minYPosition)) + { + finalYPosition = previousYOffset; + } + else + { + var centreY = (listViewBaseHeight - selectorItemHeight) / 2.0; + finalYPosition = maxYPosition - centreY + additionalVerticalOffset; + } + + // Keep horizontal position unchanged. + finalXPosition = previousXOffset; + + await scrollViewer.ChangeViewAsync(finalXPosition, finalYPosition, zoomFactor: null, disableAnimation); + } + + /// + /// Smooth scrolling the list to bring the specified data item into view, centering it vertically. + /// + /// List to scroll + /// The data item to bring into view + /// Set true to disable animation + /// Set false to disable scrolling when the corresponding item is in view vertically + /// Adds additional vertical offset + /// Returns that completes after scrolling + public static async Task SmoothScrollVerticallyIntoViewWithItemAsync(this ListViewBase listViewBase, object item, bool disableAnimation = false, bool scrollIfVisible = true, int additionalVerticalOffset = 0) + { + await SmoothScrollVerticallyIntoViewWithIndexAsync(listViewBase, listViewBase.Items.IndexOf(item), disableAnimation, scrollIfVisible, additionalVerticalOffset); + } + + #endregion + /// /// Smooth scrolling the list to bring the specified index into view /// @@ -133,7 +357,6 @@ public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase li { finalYPosition = maxYPosition + additionalVerticalOffset; } - break; case ScrollItemPlacement.Left: @@ -153,6 +376,16 @@ public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase li finalYPosition = maxYPosition - centreY + additionalVerticalOffset; break; + case ScrollItemPlacement.CenterHorizontally: + finalXPosition = maxXPosition - ((listViewBaseWidth - selectorItemWidth) / 2.0) + additionalHorizontalOffset; + finalYPosition = previousYOffset + additionalVerticalOffset; + break; + + case ScrollItemPlacement.CenterVertically: + finalXPosition = previousXOffset + additionalHorizontalOffset; + finalYPosition = maxYPosition - ((listViewBaseHeight - selectorItemHeight) / 2.0) + additionalVerticalOffset; + break; + case ScrollItemPlacement.Right: finalXPosition = minXPosition + additionalHorizontalOffset; finalYPosition = previousYOffset + additionalVerticalOffset; diff --git a/components/Extensions/src/ListViewBase/ScrollItemPlacement.cs b/components/Extensions/src/ListViewBase/ScrollItemPlacement.cs index 1225d8ce..6473c74f 100644 --- a/components/Extensions/src/ListViewBase/ScrollItemPlacement.cs +++ b/components/Extensions/src/ListViewBase/ScrollItemPlacement.cs @@ -25,10 +25,20 @@ public enum ScrollItemPlacement Top, /// - /// Aligned center + /// Aligned center (both horizontally and vertically) /// Center, + /// + /// Aligned center horizontally + /// + CenterHorizontally, + + /// + /// Aligned center vertically + /// + CenterVertically, + /// /// Aligned right ///