-
Notifications
You must be signed in to change notification settings - Fork 83
Expand file tree
/
Copy pathMarquee.cs
More file actions
328 lines (279 loc) · 12.5 KB
/
Marquee.cs
File metadata and controls
328 lines (279 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace CommunityToolkit.WinUI.Controls;
/// <summary>
/// A Control that displays Text in a Marquee style.
/// </summary>
[TemplatePart(Name = MarqueeContainerPartName, Type = typeof(Panel))]
[TemplatePart(Name = Segment1PartName, Type = typeof(ContentPresenter))]
[TemplatePart(Name = Segment2PartName, Type = typeof(ContentPresenter))]
[TemplatePart(Name = MarqueeTransformPartName, Type = typeof(TranslateTransform))]
[TemplateVisualState(GroupName = DirectionVisualStateGroupName, Name = LeftwardsVisualStateName)]
[TemplateVisualState(GroupName = DirectionVisualStateGroupName, Name = RightwardsVisualStateName)]
[TemplateVisualState(GroupName = DirectionVisualStateGroupName, Name = UpwardsVisualStateName)]
[TemplateVisualState(GroupName = DirectionVisualStateGroupName, Name = DownwardsVisualStateName)]
[TemplateVisualState(GroupName = BehaviorVisualStateGroupName, Name = TickerVisualStateName)]
[TemplateVisualState(GroupName = BehaviorVisualStateGroupName, Name = LoopingVisualStateName)]
[TemplateVisualState(GroupName = BehaviorVisualStateGroupName, Name = BouncingVisualStateName)]
public partial class Marquee : ContentControl
{
private const string MarqueeContainerPartName = "MarqueeContainer";
private const string Segment1PartName = "Segment1";
private const string Segment2PartName = "Segment2";
private const string MarqueeTransformPartName = "MarqueeTransform";
private const string MarqueeActiveState = "MarqueeActive";
private const string MarqueeStoppedState = "MarqueeStopped";
private const string DirectionVisualStateGroupName = "DirectionStateGroup";
private const string LeftwardsVisualStateName = "Leftwards";
private const string RightwardsVisualStateName = "Rightwards";
private const string UpwardsVisualStateName = "Upwards";
private const string DownwardsVisualStateName = "Downwards";
private const string BehaviorVisualStateGroupName = "BehaviorStateGroup";
private const string TickerVisualStateName = "Ticker";
private const string LoopingVisualStateName = "Looping";
private const string BouncingVisualStateName = "Bouncing";
private Panel? _marqueeContainer;
private ContentPresenter? _segment1;
private ContentPresenter? _segment2;
private TranslateTransform? _marqueeTransform;
private Storyboard? _marqueeStoryboard;
private bool _isActive;
/// <summary>
/// Initializes a new instance of the <see cref="Marquee"/> class.
/// </summary>
public Marquee()
{
DefaultStyleKey = typeof(Marquee);
}
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Explicit casting throws early when parts are missing from the template
_marqueeContainer = (Panel)GetTemplateChild(MarqueeContainerPartName);
_segment1 = (ContentPresenter)GetTemplateChild(Segment1PartName);
_segment2 = (ContentPresenter)GetTemplateChild(Segment2PartName);
_marqueeTransform = (TranslateTransform)GetTemplateChild(MarqueeTransformPartName);
_marqueeContainer.SizeChanged += Container_SizeChanged;
_segment1.SizeChanged += Segment_SizeChanged;
// Swapping tabs in TabView caused errors where the control would unload and never reattach events.
// Hotfix: Track the loaded event. This should be fine because the GC will handle detaching the Loaded
// event on disposal. However, more research is required
Loaded += this.Marquee_Loaded;
VisualStateManager.GoToState(this, GetVisualStateName(Direction), false);
VisualStateManager.GoToState(this, GetVisualStateName(Behavior), false);
}
private static string GetVisualStateName(MarqueeDirection direction)
{
return direction switch
{
MarqueeDirection.Left => LeftwardsVisualStateName,
MarqueeDirection.Right => RightwardsVisualStateName,
MarqueeDirection.Up => UpwardsVisualStateName,
MarqueeDirection.Down => DownwardsVisualStateName,
_ => LeftwardsVisualStateName,
};
}
private static string GetVisualStateName(MarqueeBehavior behavior)
{
return behavior switch
{
MarqueeBehavior.Ticker => TickerVisualStateName,
MarqueeBehavior.Looping => LoopingVisualStateName,
#if !HAS_UNO
MarqueeBehavior.Bouncing => BouncingVisualStateName,
#endif
_ => TickerVisualStateName,
};
}
/// <summary>
/// Begins the Marquee animation if not running.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when template parts are not supplied.</exception>
public void StartMarquee()
{
bool initial = _isActive;
_isActive = true;
bool playing = UpdateAnimation(initial);
// Invoke MarqueeBegan if Marquee is now playing and was not before
if (playing && !initial)
{
MarqueeBegan?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Stops the Marquee animation.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when template parts are not supplied.</exception>
public void StopMarquee()
{
StopMarquee(_isActive);
}
private void StopMarquee(bool initialState)
{
// Set _isActive and update the animation to match
_isActive = false;
bool playing = UpdateAnimation(false);
// Invoke MarqueeStopped if Marquee is not playing and was before
if (!playing && initialState)
{
MarqueeStopped?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Updates the animation to match the current control state.
/// </summary>
/// <param name="resume">True if animation should resume from its current position, false if it should restart.</param>
/// <exception cref="InvalidOperationException">Thrown when template parts are not supplied.</exception>
/// <returns>True if the Animation is now playing.</returns>
private bool UpdateAnimation(bool resume = true)
{
// Crucial template parts are missing!
// This can happen during initialization of certain properties.
// Gracefully return when this happens. Proper checks for these template parts happen in OnApplyTemplate.
if (_marqueeContainer is null ||
_marqueeTransform is null ||
_segment1 is null ||
_segment2 is null)
{
return false;
}
// The marquee is stopped.
// Update the animation to the stopped position.
if (!_isActive)
{
VisualStateManager.GoToState(this, MarqueeStoppedState, false);
return false;
}
// Get the size of the container and segment, based on the orientation.
// Also track the property to adjust, also based on the orientation.
double containerSize;
double segmentSize;
double value;
DependencyProperty dp;
string targetProperty;
if (IsDirectionHorizontal)
{
// The direction is horizontal, so the sizes, value, and properties
// are defined by width and X coordinates.
containerSize = _marqueeContainer.ActualWidth;
segmentSize = _segment1.ActualWidth;
value = _marqueeTransform.X;
dp = TranslateTransform.XProperty;
targetProperty = "(TranslateTransform.X)";
}
else
{
// The direction is vertical, so the sizes, value, and properties
// are defined by height and Y coordinates.
containerSize = _marqueeContainer.ActualHeight;
segmentSize = _segment1.ActualHeight;
value = _marqueeTransform.Y;
dp = TranslateTransform.YProperty;
targetProperty = "(TranslateTransform.Y)";
}
if (IsLooping && segmentSize < containerSize)
{
// If the marquee is in looping mode and the segment is smaller
// than the container, then the animation does not not need to play.
// NOTE: Use resume as initial because _isActive is updated before
// calling update animation. If _isActive were passed, it would allow for
// MarqueeStopped to be invoked when the marquee was already stopped.
StopMarquee(resume);
_segment2.Visibility = Visibility.Collapsed;
return false;
}
// The start position is offset 100% if in ticker mode
// Otherwise it's 0
double start = IsTicker ? containerSize : 0;
// The end is when the end of the text reaches the border if in bouncing mode
// Otherwise it is when the first set of text is 100% out of view
double end = IsBouncing ? containerSize - segmentSize : -segmentSize;
// The distance is used for calculating the duration and the previous
// animation progress if resuming
double distance = Math.Abs(start - end);
// If the distance is zero, don't play an animation
if (distance is 0)
{
return false;
}
// Swap the start and end to inverse direction for right or upwards
if (IsDirectionInverse)
{
(start, end) = (end, start);
}
// The second segment of text should be hidden if the marquee is not in looping mode
_segment2.Visibility = IsLooping ? Visibility.Visible : Visibility.Collapsed;
// Calculate the animation duration by dividing the distance by the speed
TimeSpan duration = TimeSpan.FromSeconds(distance / Speed);
// Unbind events from the old storyboard
if (_marqueeStoryboard is not null)
{
_marqueeStoryboard.Completed -= StoryBoard_Completed;
}
// Create new storyboard and animation
_marqueeStoryboard = CreateMarqueeStoryboardAnimation(start, end, duration, targetProperty);
// Bind the storyboard completed event
_marqueeStoryboard.Completed += StoryBoard_Completed;
// Set the visual state to active and begin the animation
VisualStateManager.GoToState(this, MarqueeActiveState, true);
_marqueeStoryboard.Begin();
// If resuming, seek the animation so the text resumes from its current position.
if (resume)
{
double progress = Math.Abs(start - value) / distance;
_marqueeStoryboard.Seek(TimeSpan.FromTicks((long)(duration.Ticks * progress)));
}
// NOTE: Can this be optimized to remove or reduce the need for this callback?
// Invalidate the segment measures when the transform changes.
// This forces virtualized panels to re-measure the segments
_marqueeTransform.RegisterPropertyChangedCallback(dp, (sender, dp) =>
{
_segment1.InvalidateMeasure();
_segment2.InvalidateMeasure();
});
return true;
}
private Storyboard CreateMarqueeStoryboardAnimation(double start, double end, TimeSpan duration, string targetProperty)
{
// Initialize the new storyboard
var marqueeStoryboard = new Storyboard
{
Duration = duration,
RepeatBehavior = RepeatBehavior,
#if !HAS_UNO
AutoReverse = IsBouncing,
#endif
};
// Create a new double animation, moving from [start] to [end] positions in [duration] time.
var animation = new DoubleAnimationUsingKeyFrames
{
Duration = duration,
RepeatBehavior = RepeatBehavior,
#if !HAS_UNO
AutoReverse = IsBouncing,
#endif
};
// Create the key frames
var frame1 = new DiscreteDoubleKeyFrame
{
KeyTime = KeyTime.FromTimeSpan(TimeSpan.Zero),
Value = start,
};
var frame2 = new EasingDoubleKeyFrame
{
KeyTime = KeyTime.FromTimeSpan(duration),
Value = end,
};
// Add the key frames to the animation
animation.KeyFrames.Add(frame1);
animation.KeyFrames.Add(frame2);
// Add the double animation to the storyboard
marqueeStoryboard.Children.Add(animation);
// Set the storyboard target and target property
Storyboard.SetTarget(animation, _marqueeTransform);
Storyboard.SetTargetProperty(animation, targetProperty);
return marqueeStoryboard;
}
}