Skip to content

Commit 82d8634

Browse files
committed
Added CenterHorizontally and CenterVertically
Added CenterHorizontally and CenterVertically Fixes this issue: CommunityToolkit#647
1 parent e2eb2c7 commit 82d8634

File tree

2 files changed

+245
-2
lines changed

2 files changed

+245
-2
lines changed

components/Extensions/src/ListViewBase/ListViewExtensions.SmoothScrollIntoView.cs

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,230 @@ namespace CommunityToolkit.WinUI;
99
/// </summary>
1010
public static partial class ListViewExtensions
1111
{
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+
12236
/// <summary>
13237
/// Smooth scrolling the list to bring the specified index into view
14238
/// </summary>
@@ -133,7 +357,6 @@ public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase li
133357
{
134358
finalYPosition = maxYPosition + additionalVerticalOffset;
135359
}
136-
137360
break;
138361

139362
case ScrollItemPlacement.Left:
@@ -153,6 +376,16 @@ public static async Task SmoothScrollIntoViewWithIndexAsync(this ListViewBase li
153376
finalYPosition = maxYPosition - centreY + additionalVerticalOffset;
154377
break;
155378

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+
156389
case ScrollItemPlacement.Right:
157390
finalXPosition = minXPosition + additionalHorizontalOffset;
158391
finalYPosition = previousYOffset + additionalVerticalOffset;

components/Extensions/src/ListViewBase/ScrollItemPlacement.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ public enum ScrollItemPlacement
2525
Top,
2626

2727
/// <summary>
28-
/// Aligned center
28+
/// Aligned center (both horizontally and vertically)
2929
/// </summary>
3030
Center,
3131

32+
/// <summary>
33+
/// Aligned center horizontally
34+
/// </summary>
35+
CenterHorizontally,
36+
37+
/// <summary>
38+
/// Aligned center vertically
39+
/// </summary>
40+
CenterVertically,
41+
3242
/// <summary>
3343
/// Aligned right
3444
/// </summary>

0 commit comments

Comments
 (0)