@@ -9,6 +9,230 @@ namespace CommunityToolkit.WinUI;
9
9
/// </summary>
10
10
public static partial class ListViewExtensions
11
11
{
12
+ #region New Horizontal/Vertical Centering Methods
13
+
14
+ /// <summary>
15
+ /// Smooth scrolling the list to bring the specified index into view, centering it horizontally.
16
+ /// </summary>
17
+ /// <param name="listViewBase">List to scroll</param>
18
+ /// <param name="index">The index to bring into view. Index can be negative.</param>
19
+ /// <param name="disableAnimation">Set true to disable animation</param>
20
+ /// <param name="scrollIfVisible">Set false to disable scrolling when the corresponding item is in view horizontally</param>
21
+ /// <param name="additionalHorizontalOffset">Adds additional horizontal offset</param>
22
+ /// <returns>Returns <see cref="Task"/> that completes after scrolling</returns>
23
+ public static async Task SmoothScrollHorizontallyIntoViewWithIndexAsync ( this ListViewBase listViewBase , int index , bool disableAnimation = false , bool scrollIfVisible = true , int additionalHorizontalOffset = 0 )
24
+ {
25
+ if ( index > ( listViewBase . Items . Count - 1 ) )
26
+ {
27
+ index = listViewBase . Items . Count - 1 ;
28
+ }
29
+
30
+ if ( index < - listViewBase . Items . Count )
31
+ {
32
+ index = - listViewBase . Items . Count ;
33
+ }
34
+
35
+ index = ( index < 0 ) ? ( index + listViewBase . Items . Count ) : index ;
36
+
37
+ bool isVirtualizing = default ;
38
+ double previousXOffset = default , previousYOffset = default ;
39
+
40
+ var scrollViewer = listViewBase . FindDescendant < ScrollViewer > ( ) ;
41
+ var selectorItem = listViewBase . ContainerFromIndex ( index ) as SelectorItem ;
42
+
43
+ if ( scrollViewer == null )
44
+ {
45
+ return ;
46
+ }
47
+
48
+ // If selectorItem is null then the panel is virtualized.
49
+ // Scroll into view to materialize the container.
50
+ if ( selectorItem == null )
51
+ {
52
+ isVirtualizing = true ;
53
+ previousXOffset = scrollViewer . HorizontalOffset ;
54
+ previousYOffset = scrollViewer . VerticalOffset ;
55
+
56
+ var tcs = new TaskCompletionSource < object ? > ( ) ;
57
+ void ViewChanged ( object ? _ , ScrollViewerViewChangedEventArgs __ ) => tcs . TrySetResult ( result : default ) ;
58
+
59
+ try
60
+ {
61
+ scrollViewer . ViewChanged += ViewChanged ;
62
+ listViewBase . ScrollIntoView ( listViewBase . Items [ index ] , ScrollIntoViewAlignment . Leading ) ;
63
+ await tcs . Task ;
64
+ }
65
+ finally
66
+ {
67
+ scrollViewer . ViewChanged -= ViewChanged ;
68
+ }
69
+ selectorItem = ( SelectorItem ) listViewBase . ContainerFromIndex ( index ) ;
70
+ }
71
+
72
+ var transform = selectorItem . TransformToVisual ( ( UIElement ) scrollViewer . Content ) ;
73
+ var position = transform . TransformPoint ( new Point ( 0 , 0 ) ) ;
74
+
75
+ // If we had to scroll to materialize the item, scroll back to the previous view.
76
+ if ( isVirtualizing )
77
+ {
78
+ await scrollViewer . ChangeViewAsync ( previousXOffset , previousYOffset , zoomFactor : null , disableAnimation : true ) ;
79
+ }
80
+
81
+ var listViewBaseWidth = listViewBase . ActualWidth ;
82
+ var selectorItemWidth = selectorItem . ActualWidth ;
83
+ var listViewBaseHeight = listViewBase . ActualHeight ;
84
+ var selectorItemHeight = selectorItem . ActualHeight ;
85
+
86
+ previousXOffset = scrollViewer . HorizontalOffset ;
87
+ previousYOffset = scrollViewer . VerticalOffset ;
88
+
89
+ var minXPosition = position . X - listViewBaseWidth + selectorItemWidth ;
90
+ var maxXPosition = position . X ;
91
+ double finalXPosition , finalYPosition ;
92
+
93
+ // Check horizontal visibility; if the item is already in view, no horizontal scrolling is needed.
94
+ if ( ! scrollIfVisible && ( previousXOffset <= maxXPosition && previousXOffset >= minXPosition ) )
95
+ {
96
+ finalXPosition = previousXOffset ;
97
+ }
98
+ else
99
+ {
100
+ var centreX = ( listViewBaseWidth - selectorItemWidth ) / 2.0 ;
101
+ finalXPosition = maxXPosition - centreX + additionalHorizontalOffset ;
102
+ }
103
+
104
+ // Keep vertical position unchanged.
105
+ finalYPosition = previousYOffset ;
106
+
107
+ await scrollViewer . ChangeViewAsync ( finalXPosition , finalYPosition , zoomFactor : null , disableAnimation ) ;
108
+ }
109
+
110
+ /// <summary>
111
+ /// Smooth scrolling the list to bring the specified data item into view, centering it horizontally.
112
+ /// </summary>
113
+ /// <param name="listViewBase">List to scroll</param>
114
+ /// <param name="item">The data item to bring into view</param>
115
+ /// <param name="disableAnimation">Set true to disable animation</param>
116
+ /// <param name="scrollIfVisible">Set false to disable scrolling when the corresponding item is in view horizontally</param>
117
+ /// <param name="additionalHorizontalOffset">Adds additional horizontal offset</param>
118
+ /// <returns>Returns <see cref="Task"/> that completes after scrolling</returns>
119
+ public static async Task SmoothScrollHorizontallyIntoViewWithItemAsync ( this ListViewBase listViewBase , object item , bool disableAnimation = false , bool scrollIfVisible = true , int additionalHorizontalOffset = 0 )
120
+ {
121
+ await SmoothScrollHorizontallyIntoViewWithIndexAsync ( listViewBase , listViewBase . Items . IndexOf ( item ) , disableAnimation , scrollIfVisible , additionalHorizontalOffset ) ;
122
+ }
123
+
124
+ /// <summary>
125
+ /// Smooth scrolling the list to bring the specified index into view, centering it vertically.
126
+ /// </summary>
127
+ /// <param name="listViewBase">List to scroll</param>
128
+ /// <param name="index">The index to bring into view. Index can be negative.</param>
129
+ /// <param name="disableAnimation">Set true to disable animation</param>
130
+ /// <param name="scrollIfVisible">Set false to disable scrolling when the corresponding item is in view vertically</param>
131
+ /// <param name="additionalVerticalOffset">Adds additional vertical offset</param>
132
+ /// <returns>Returns <see cref="Task"/> that completes after scrolling</returns>
133
+ public static async Task SmoothScrollVerticallyIntoViewWithIndexAsync ( this ListViewBase listViewBase , int index , bool disableAnimation = false , bool scrollIfVisible = true , int additionalVerticalOffset = 0 )
134
+ {
135
+ if ( index > ( listViewBase . Items . Count - 1 ) )
136
+ {
137
+ index = listViewBase . Items . Count - 1 ;
138
+ }
139
+
140
+ if ( index < - listViewBase . Items . Count )
141
+ {
142
+ index = - listViewBase . Items . Count ;
143
+ }
144
+
145
+ index = ( index < 0 ) ? ( index + listViewBase . Items . Count ) : index ;
146
+
147
+ bool isVirtualizing = default ;
148
+ double previousXOffset = default , previousYOffset = default ;
149
+
150
+ var scrollViewer = listViewBase . FindDescendant < ScrollViewer > ( ) ;
151
+ var selectorItem = listViewBase . ContainerFromIndex ( index ) as SelectorItem ;
152
+
153
+ if ( scrollViewer == null )
154
+ {
155
+ return ;
156
+ }
157
+
158
+ // If selectorItem is null then the panel is virtualized.
159
+ // Scroll into view to materialize the container.
160
+ if ( selectorItem == null )
161
+ {
162
+ isVirtualizing = true ;
163
+ previousXOffset = scrollViewer . HorizontalOffset ;
164
+ previousYOffset = scrollViewer . VerticalOffset ;
165
+
166
+ var tcs = new TaskCompletionSource < object ? > ( ) ;
167
+ void ViewChanged ( object ? _ , ScrollViewerViewChangedEventArgs __ ) => tcs . TrySetResult ( result : default ) ;
168
+
169
+ try
170
+ {
171
+ scrollViewer . ViewChanged += ViewChanged ;
172
+ listViewBase . ScrollIntoView ( listViewBase . Items [ index ] , ScrollIntoViewAlignment . Leading ) ;
173
+ await tcs . Task ;
174
+ }
175
+ finally
176
+ {
177
+ scrollViewer . ViewChanged -= ViewChanged ;
178
+ }
179
+ selectorItem = ( SelectorItem ) listViewBase . ContainerFromIndex ( index ) ;
180
+ }
181
+
182
+ var transform = selectorItem . TransformToVisual ( ( UIElement ) scrollViewer . Content ) ;
183
+ var position = transform . TransformPoint ( new Point ( 0 , 0 ) ) ;
184
+
185
+ // If we had to scroll to materialize the item, scroll back to the previous view.
186
+ if ( isVirtualizing )
187
+ {
188
+ await scrollViewer . ChangeViewAsync ( previousXOffset , previousYOffset , zoomFactor : null , disableAnimation : true ) ;
189
+ }
190
+
191
+ var listViewBaseWidth = listViewBase . ActualWidth ;
192
+ var selectorItemWidth = selectorItem . ActualWidth ;
193
+ var listViewBaseHeight = listViewBase . ActualHeight ;
194
+ var selectorItemHeight = selectorItem . ActualHeight ;
195
+
196
+ previousXOffset = scrollViewer . HorizontalOffset ;
197
+ previousYOffset = scrollViewer . VerticalOffset ;
198
+
199
+ var minYPosition = position . Y - listViewBaseHeight + selectorItemHeight ;
200
+ var maxYPosition = position . Y ;
201
+ double finalXPosition , finalYPosition ;
202
+
203
+ // Check vertical visibility; if the item is already in view, no vertical scrolling is needed.
204
+ if ( ! scrollIfVisible && ( previousYOffset <= maxYPosition && previousYOffset >= minYPosition ) )
205
+ {
206
+ finalYPosition = previousYOffset ;
207
+ }
208
+ else
209
+ {
210
+ var centreY = ( listViewBaseHeight - selectorItemHeight ) / 2.0 ;
211
+ finalYPosition = maxYPosition - centreY + additionalVerticalOffset ;
212
+ }
213
+
214
+ // Keep horizontal position unchanged.
215
+ finalXPosition = previousXOffset ;
216
+
217
+ await scrollViewer . ChangeViewAsync ( finalXPosition , finalYPosition , zoomFactor : null , disableAnimation ) ;
218
+ }
219
+
220
+ /// <summary>
221
+ /// Smooth scrolling the list to bring the specified data item into view, centering it vertically.
222
+ /// </summary>
223
+ /// <param name="listViewBase">List to scroll</param>
224
+ /// <param name="item">The data item to bring into view</param>
225
+ /// <param name="disableAnimation">Set true to disable animation</param>
226
+ /// <param name="scrollIfVisible">Set false to disable scrolling when the corresponding item is in view vertically</param>
227
+ /// <param name="additionalVerticalOffset">Adds additional vertical offset</param>
228
+ /// <returns>Returns <see cref="Task"/> that completes after scrolling</returns>
229
+ public static async Task SmoothScrollVerticallyIntoViewWithItemAsync ( this ListViewBase listViewBase , object item , bool disableAnimation = false , bool scrollIfVisible = true , int additionalVerticalOffset = 0 )
230
+ {
231
+ await SmoothScrollVerticallyIntoViewWithIndexAsync ( listViewBase , listViewBase . Items . IndexOf ( item ) , disableAnimation , scrollIfVisible , additionalVerticalOffset ) ;
232
+ }
233
+
234
+ #endregion
235
+
12
236
/// <summary>
13
237
/// Smooth scrolling the list to bring the specified index into view
14
238
/// </summary>
@@ -133,7 +357,6 @@ public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase li
133
357
{
134
358
finalYPosition = maxYPosition + additionalVerticalOffset ;
135
359
}
136
-
137
360
break ;
138
361
139
362
case ScrollItemPlacement . Left :
@@ -153,6 +376,16 @@ public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase li
153
376
finalYPosition = maxYPosition - centreY + additionalVerticalOffset ;
154
377
break ;
155
378
379
+ case ScrollItemPlacement . CenterHorizontally :
380
+ finalXPosition = maxXPosition - ( ( listViewBaseWidth - selectorItemWidth ) / 2.0 ) + additionalHorizontalOffset ;
381
+ finalYPosition = previousYOffset + additionalVerticalOffset ;
382
+ break ;
383
+
384
+ case ScrollItemPlacement . CenterVertically :
385
+ finalXPosition = previousXOffset + additionalHorizontalOffset ;
386
+ finalYPosition = maxYPosition - ( ( listViewBaseHeight - selectorItemHeight ) / 2.0 ) + additionalVerticalOffset ;
387
+ break ;
388
+
156
389
case ScrollItemPlacement . Right :
157
390
finalXPosition = minXPosition + additionalHorizontalOffset ;
158
391
finalYPosition = previousYOffset + additionalVerticalOffset ;
0 commit comments