Skip to content

Commit 0f07550

Browse files
Consolidate StickyHeaderBehavior and QuickReturnHeaderBehavior with new abstract HeaderBehaviorBase helper class
Is responsible for finding needed controls and properties for setting up composition animation. 90% of these two classes were doing the same thing, so now only the logic related to setting up/manipulating the composition animation is in each subclass. Going to investigate if FadeHeaderBehavior is similar enough to reuse as well.
1 parent 22cde03 commit 0f07550

File tree

3 files changed

+257
-395
lines changed

3 files changed

+257
-395
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
#if WINUI3
6+
using Microsoft.UI.Composition;
7+
using Microsoft.UI.Xaml.Hosting;
8+
using ListViewBase = Microsoft.UI.Xaml.Controls.ListViewBase;
9+
#else
10+
using Windows.UI.Composition;
11+
using Windows.UI.Xaml.Hosting;
12+
using ListViewBase = Windows.UI.Xaml.Controls.ListViewBase;
13+
#endif
14+
15+
namespace CommunityToolkit.WinUI.Behaviors.Internal;
16+
17+
/// <summary>
18+
/// Base class helper for header behaviors which manipulate an element within a viewport of a <see cref="ListViewBase"/> based control.
19+
/// </summary>
20+
public abstract class HeaderBehaviorBase : BehaviorBase<FrameworkElement>
21+
{
22+
protected ScrollViewer? _scrollViewer;
23+
protected CompositionPropertySet? _scrollProperties;
24+
protected CompositionPropertySet? _animationProperties;
25+
protected Visual? _headerVisual;
26+
27+
/// <summary>
28+
/// Gets or sets the target element for the Header behavior to be manipulated within the viewport.
29+
/// </summary>
30+
/// <remarks>
31+
/// Set this using the header of a ListView or GridView.
32+
/// </remarks>
33+
public UIElement HeaderElement
34+
{
35+
get { return (UIElement)GetValue(HeaderElementProperty); }
36+
set { SetValue(HeaderElementProperty, value); }
37+
}
38+
39+
/// <summary>
40+
/// Defines the Dependency Property for the <see cref="HeaderElement"/> property.
41+
/// </summary>
42+
public static readonly DependencyProperty HeaderElementProperty = DependencyProperty.Register(
43+
nameof(HeaderElement), typeof(UIElement), typeof(HeaderBehaviorBase), new PropertyMetadata(null, PropertyChangedCallback));
44+
45+
/// <summary>
46+
/// If any of the properties are changed then the animation is automatically started.
47+
/// </summary>
48+
/// <param name="d">The dependency object.</param>
49+
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
50+
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
51+
{
52+
if (d is HeaderBehaviorBase @base)
53+
{
54+
@base.AssignAnimation();
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Attaches the behavior to the associated object.
60+
/// </summary>
61+
/// <returns>
62+
/// <c>true</c> if attaching succeeded; otherwise <c>false</c>.
63+
/// </returns>
64+
protected override bool Initialize()
65+
{
66+
var result = AssignAnimation();
67+
return result;
68+
}
69+
70+
/// <summary>
71+
/// Detaches the behavior from the associated object.
72+
/// </summary>
73+
/// <returns>
74+
/// <c>true</c> if detaching succeeded; otherwise <c>false</c>.
75+
/// </returns>
76+
protected override bool Uninitialize()
77+
{
78+
RemoveAnimation();
79+
return true;
80+
}
81+
82+
/// <summary>
83+
/// Uses Composition API to get the UIElement and sets an ExpressionAnimation
84+
/// </summary>
85+
/// <returns><c>true</c> if the assignment was successful; otherwise, <c>false</c>.</returns>
86+
protected virtual bool AssignAnimation()
87+
{
88+
StopAnimation();
89+
90+
if (AssociatedObject == null)
91+
{
92+
return false;
93+
}
94+
95+
// TODO: What if we attach to the 'header element' and look up for the ScrollViewer?
96+
if (_scrollViewer == null)
97+
{
98+
_scrollViewer = AssociatedObject as ScrollViewer ?? AssociatedObject.FindDescendant<ScrollViewer>();
99+
}
100+
101+
if (_scrollViewer == null)
102+
{
103+
return false;
104+
}
105+
106+
var listView = AssociatedObject as ListViewBase ?? AssociatedObject.FindDescendant<ListViewBase>();
107+
108+
// TODO: Is this required?
109+
if (listView != null && listView.ItemsPanelRoot != null)
110+
{
111+
Canvas.SetZIndex(listView.ItemsPanelRoot, -1);
112+
}
113+
114+
if (_scrollProperties == null)
115+
{
116+
_scrollProperties = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(_scrollViewer);
117+
}
118+
119+
if (_scrollProperties == null)
120+
{
121+
return false;
122+
}
123+
124+
// Implicit operation: Find the Header object of the control if it uses ListViewBase
125+
if (HeaderElement == null && listView != null)
126+
{
127+
HeaderElement = (listView.Header as UIElement)!;
128+
}
129+
130+
var headerElement = HeaderElement as FrameworkElement;
131+
if (headerElement == null || headerElement.RenderSize.Height == 0)
132+
{
133+
return false;
134+
}
135+
136+
if (_headerVisual == null)
137+
{
138+
_headerVisual = ElementCompositionPreview.GetElementVisual(headerElement);
139+
}
140+
141+
if (_headerVisual == null)
142+
{
143+
return false;
144+
}
145+
146+
headerElement.SizeChanged -= ScrollHeader_SizeChanged;
147+
headerElement.SizeChanged += ScrollHeader_SizeChanged;
148+
149+
_scrollViewer.GotFocus -= ScrollViewer_GotFocus;
150+
_scrollViewer.GotFocus += ScrollViewer_GotFocus;
151+
152+
var compositor = _scrollProperties.Compositor;
153+
154+
if (_animationProperties == null)
155+
{
156+
_animationProperties = compositor.CreatePropertySet();
157+
}
158+
159+
return true;
160+
}
161+
162+
/// <summary>
163+
/// Stop the animation of the UIElement.
164+
/// </summary>
165+
protected abstract void StopAnimation();
166+
167+
/// <summary>
168+
/// Remove the animation from the UIElement.
169+
/// </summary>
170+
protected virtual void RemoveAnimation()
171+
{
172+
if (_scrollViewer != null)
173+
{
174+
_scrollViewer.GotFocus -= ScrollViewer_GotFocus;
175+
}
176+
177+
if (HeaderElement is FrameworkElement element)
178+
{
179+
element.SizeChanged -= ScrollHeader_SizeChanged;
180+
}
181+
182+
StopAnimation();
183+
}
184+
185+
private void ScrollHeader_SizeChanged(object sender, SizeChangedEventArgs e)
186+
{
187+
AssignAnimation();
188+
}
189+
190+
private void ScrollViewer_GotFocus(object sender, RoutedEventArgs e)
191+
{
192+
var scroller = (ScrollViewer)sender;
193+
194+
object focusedElement;
195+
if (ApiInformationHelper.IsXamlRootAvailable && scroller.XamlRoot != null)
196+
{
197+
focusedElement = FocusManager.GetFocusedElement(scroller.XamlRoot)!;
198+
}
199+
else
200+
{
201+
focusedElement = FocusManager.GetFocusedElement()!;
202+
}
203+
204+
// To prevent Popups (Flyouts...) from triggering the autoscroll, we check if the focused element has a valid parent.
205+
// Popups have no parents, whereas a normal Item would have the ListView as a parent.
206+
if (focusedElement is UIElement element && VisualTreeHelper.GetParent(element) != null)
207+
{
208+
FrameworkElement header = (FrameworkElement)HeaderElement;
209+
210+
var point = element.TransformToVisual(scroller).TransformPoint(new Point(0, 0));
211+
212+
if (point.Y < header.ActualHeight)
213+
{
214+
scroller.ChangeView(0, scroller.VerticalOffset - (header.ActualHeight - point.Y), 1, false);
215+
}
216+
}
217+
}
218+
}

0 commit comments

Comments
 (0)