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
+ using Microsoft . Toolkit . Uwp . UI ;
6
+ using Microsoft . Toolkit . Uwp . UI . Behaviors ;
7
+ using Microsoft . Toolkit . Uwp . UI . Animations . Expressions ;
8
+ using Windows . Foundation ;
9
+ using Windows . UI . Composition ;
10
+ using Windows . UI . Xaml ;
11
+ using Windows . UI . Xaml . Controls ;
12
+ using Windows . UI . Xaml . Hosting ;
13
+ using Windows . UI . Xaml . Input ;
14
+ using Windows . UI . Xaml . Media ;
15
+ using Windows . Foundation . Metadata ;
16
+ using System . Linq ;
17
+
18
+ namespace Files . Behaviors
19
+ {
20
+ /// <summary>
21
+ /// Performs an animation on a ListView or GridView Header to make it sticky using composition.
22
+ /// </summary>
23
+ /// <seealso>
24
+ /// <cref>Microsoft.Xaml.Interactivity.Behavior{Windows.UI.Xaml.UIElement}</cref>
25
+ /// </seealso>
26
+ public class StickyHeaderBehavior : BehaviorBase < FrameworkElement >
27
+ {
28
+ public static bool IsXamlRootAvailable { get ; } = ApiInformation . IsPropertyPresent ( "Windows.UI.Xaml.UIElement" , "XamlRoot" ) ;
29
+
30
+ /// <summary>
31
+ /// Attaches the behavior to the associated object.
32
+ /// </summary>
33
+ /// <returns>
34
+ /// <c>true</c> if attaching succeeded; otherwise <c>false</c>.
35
+ /// </returns>
36
+ protected override bool Initialize ( )
37
+ {
38
+ var result = AssignAnimation ( ) ;
39
+ return result ;
40
+ }
41
+
42
+ /// <summary>
43
+ /// Detaches the behavior from the associated object.
44
+ /// </summary>
45
+ /// <returns>
46
+ /// <c>true</c> if detaching succeeded; otherwise <c>false</c>.
47
+ /// </returns>
48
+ protected override bool Uninitialize ( )
49
+ {
50
+ RemoveAnimation ( ) ;
51
+ return true ;
52
+ }
53
+
54
+ /// <summary>
55
+ /// The UIElement that will be faded.
56
+ /// </summary>
57
+ public static readonly DependencyProperty HeaderElementProperty = DependencyProperty . Register (
58
+ nameof ( HeaderElement ) , typeof ( UIElement ) , typeof ( StickyHeaderBehavior ) , new PropertyMetadata ( null , PropertyChangedCallback ) ) ;
59
+
60
+ private ScrollViewer _scrollViewer ;
61
+ private CompositionPropertySet _scrollProperties ;
62
+ private CompositionPropertySet _animationProperties ;
63
+ private Visual _headerVisual , _itemsPanelVisual ;
64
+ private InsetClip _contentClip ;
65
+
66
+ /// <summary>
67
+ /// Gets or sets the target element for the ScrollHeader behavior.
68
+ /// </summary>
69
+ /// <remarks>
70
+ /// Set this using the header of a ListView or GridView.
71
+ /// </remarks>
72
+ public UIElement HeaderElement
73
+ {
74
+ get { return ( UIElement ) GetValue ( HeaderElementProperty ) ; }
75
+ set { SetValue ( HeaderElementProperty , value ) ; }
76
+ }
77
+
78
+ /// <summary>
79
+ /// If any of the properties are changed then the animation is automatically started.
80
+ /// </summary>
81
+ /// <param name="d">The dependency object.</param>
82
+ /// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
83
+ private static void PropertyChangedCallback ( DependencyObject d , DependencyPropertyChangedEventArgs e )
84
+ {
85
+ var b = d as StickyHeaderBehavior ;
86
+ b ? . AssignAnimation ( ) ;
87
+ }
88
+
89
+ /// <summary>
90
+ /// Uses Composition API to get the UIElement and sets an ExpressionAnimation
91
+ /// The ExpressionAnimation uses the height of the UIElement to calculate an opacity value
92
+ /// for the Header as it is scrolling off-screen. The opacity reaches 0 when the Header
93
+ /// is entirely scrolled off.
94
+ /// </summary>
95
+ /// <returns><c>true</c> if the assignment was successful; otherwise, <c>false</c>.</returns>
96
+ private bool AssignAnimation ( )
97
+ {
98
+ StopAnimation ( ) ;
99
+
100
+ if ( AssociatedObject == null )
101
+ {
102
+ return false ;
103
+ }
104
+
105
+ if ( _scrollViewer == null )
106
+ {
107
+ _scrollViewer = AssociatedObject as ScrollViewer ?? AssociatedObject . FindDescendant < ScrollViewer > ( ) ;
108
+ }
109
+
110
+ if ( _scrollViewer == null )
111
+ {
112
+ return false ;
113
+ }
114
+
115
+ var listView = AssociatedObject as ListViewBase ?? AssociatedObject . FindDescendant < ListViewBase > ( ) ;
116
+
117
+ if ( listView != null && listView . ItemsPanelRoot != null )
118
+ {
119
+ Canvas . SetZIndex ( listView . ItemsPanelRoot , - 1 ) ;
120
+ }
121
+
122
+ if ( _scrollProperties == null )
123
+ {
124
+ _scrollProperties = ElementCompositionPreview . GetScrollViewerManipulationPropertySet ( _scrollViewer ) ;
125
+ }
126
+
127
+ if ( _scrollProperties == null )
128
+ {
129
+ return false ;
130
+ }
131
+
132
+ // Implicit operation: Find the Header object of the control if it uses ListViewBase
133
+ if ( HeaderElement == null && listView != null )
134
+ {
135
+ HeaderElement = listView . Header as UIElement ;
136
+ }
137
+
138
+ var headerElement = HeaderElement as FrameworkElement ;
139
+ if ( headerElement == null || headerElement . RenderSize . Height == 0 )
140
+ {
141
+ return false ;
142
+ }
143
+
144
+ if ( _headerVisual == null )
145
+ {
146
+ _headerVisual = ElementCompositionPreview . GetElementVisual ( headerElement ) ;
147
+ }
148
+
149
+ if ( _headerVisual == null )
150
+ {
151
+ return false ;
152
+ }
153
+
154
+ headerElement . SizeChanged -= ScrollHeader_SizeChanged ;
155
+ headerElement . SizeChanged += ScrollHeader_SizeChanged ;
156
+
157
+ _scrollViewer . GotFocus -= ScrollViewer_GotFocus ;
158
+ _scrollViewer . GotFocus += ScrollViewer_GotFocus ;
159
+
160
+ var compositor = _scrollProperties . Compositor ;
161
+
162
+ if ( _animationProperties == null )
163
+ {
164
+ _animationProperties = compositor . CreatePropertySet ( ) ;
165
+ _animationProperties . InsertScalar ( "OffsetY" , 0.0f ) ;
166
+ }
167
+
168
+ var propSetOffset = _animationProperties . GetReference ( ) . GetScalarProperty ( "OffsetY" ) ;
169
+ var scrollPropSet = _scrollProperties . GetSpecializedReference < ManipulationPropertySetReferenceNode > ( ) ;
170
+ var expressionAnimation = ExpressionFunctions . Max ( propSetOffset - scrollPropSet . Translation . Y , 0 ) ;
171
+
172
+ _headerVisual . StartAnimation ( "Offset.Y" , expressionAnimation ) ;
173
+
174
+ // Mod: clip items panel below header
175
+ var itemsPanel = listView . ItemsPanelRoot ;
176
+
177
+ if ( itemsPanel == null )
178
+ {
179
+ return true ;
180
+ }
181
+
182
+ if ( _itemsPanelVisual == null )
183
+ {
184
+ _itemsPanelVisual = ElementCompositionPreview . GetElementVisual ( itemsPanel ) ;
185
+ _contentClip = compositor . CreateInsetClip ( ) ;
186
+ _itemsPanelVisual . Clip = _contentClip ;
187
+ }
188
+
189
+ var expressionClipAnimation = ExpressionFunctions . Max ( - scrollPropSet . Translation . Y , 0 ) ;
190
+ _contentClip . StartAnimation ( "TopInset" , expressionClipAnimation ) ;
191
+
192
+ return true ;
193
+ }
194
+
195
+ /// <summary>
196
+ /// Remove the animation from the UIElement.
197
+ /// </summary>
198
+ private void RemoveAnimation ( )
199
+ {
200
+ if ( HeaderElement is FrameworkElement element )
201
+ {
202
+ element . SizeChanged -= ScrollHeader_SizeChanged ;
203
+ }
204
+
205
+ if ( _scrollViewer != null )
206
+ {
207
+ _scrollViewer . GotFocus -= ScrollViewer_GotFocus ;
208
+ }
209
+
210
+ StopAnimation ( ) ;
211
+ }
212
+
213
+ /// <summary>
214
+ /// Stop the animation of the UIElement.
215
+ /// </summary>
216
+ private void StopAnimation ( )
217
+ {
218
+ _headerVisual ? . StopAnimation ( "Offset.Y" ) ;
219
+
220
+ _animationProperties ? . InsertScalar ( "OffsetY" , 0.0f ) ;
221
+
222
+ _contentClip ? . StopAnimation ( "TopInset" ) ;
223
+
224
+ if ( _headerVisual != null )
225
+ {
226
+ var offset = _headerVisual . Offset ;
227
+ offset . Y = 0.0f ;
228
+ _headerVisual . Offset = offset ;
229
+ }
230
+ }
231
+
232
+ private void ScrollHeader_SizeChanged ( object sender , SizeChangedEventArgs e )
233
+ {
234
+ AssignAnimation ( ) ;
235
+ }
236
+
237
+ private void ScrollViewer_GotFocus ( object sender , RoutedEventArgs e )
238
+ {
239
+ var scroller = ( ScrollViewer ) sender ;
240
+
241
+ object focusedElement ;
242
+ if ( IsXamlRootAvailable && scroller . XamlRoot != null )
243
+ {
244
+ focusedElement = FocusManager . GetFocusedElement ( scroller . XamlRoot ) ;
245
+ }
246
+ else
247
+ {
248
+ focusedElement = FocusManager . GetFocusedElement ( ) ;
249
+ }
250
+
251
+ // To prevent Popups (Flyouts...) from triggering the autoscroll, we check if the focused element has a valid parent.
252
+ // Popups have no parents, whereas a normal Item would have the ListView as a parent.
253
+ if ( focusedElement is UIElement element && VisualTreeHelper . GetParent ( element ) != null )
254
+ {
255
+ // Mod: ignore if element is child of header
256
+ if ( ! element . FindAscendants ( ) . Any ( x => x == HeaderElement ) )
257
+ {
258
+ FrameworkElement header = ( FrameworkElement ) HeaderElement ;
259
+
260
+ var point = element . TransformToVisual ( scroller ) . TransformPoint ( new Point ( 0 , 0 ) ) ;
261
+
262
+ if ( point . Y < header . ActualHeight )
263
+ {
264
+ // Mod: do not change scroller horizontal offset
265
+ scroller . ChangeView ( scroller . HorizontalOffset , scroller . VerticalOffset - ( header . ActualHeight - point . Y ) , 1 , false ) ;
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }
0 commit comments