Skip to content

Commit 6131961

Browse files
committed
Fix SegmentedMeter Animation
1 parent 6cfc6a6 commit 6131961

3 files changed

Lines changed: 197 additions & 46 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
namespace DevWinUI;
2+
3+
internal sealed partial class BarSlot
4+
{
5+
public readonly MeterSegment Segment;
6+
public readonly Border Element;
7+
public readonly ColumnDefinition? Column;
8+
public readonly RowDefinition? Row;
9+
public readonly double From;
10+
public readonly double To;
11+
public double Current;
12+
13+
public BarSlot(MeterSegment segment, Border element, ColumnDefinition? column, RowDefinition? row, double from, double to)
14+
{
15+
Segment = segment;
16+
Element = element;
17+
Column = column;
18+
Row = row;
19+
From = from;
20+
To = to;
21+
Current = from;
22+
}
23+
}

‎dev/DevWinUI/Controls/Native/SegmentedMeter/SegmentedMeter.Properties.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public bool Animated
106106
}
107107

108108
public static readonly DependencyProperty AnimatedProperty =
109-
DependencyProperty.Register(nameof(Animated), typeof(bool), typeof(SegmentedMeter), new PropertyMetadata(false));
109+
DependencyProperty.Register(nameof(Animated), typeof(bool), typeof(SegmentedMeter), new PropertyMetadata(true));
110110

111111
public object? StartContent
112112
{

‎dev/DevWinUI/Controls/Native/SegmentedMeter/SegmentedMeter.cs‎

Lines changed: 173 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Specialized;
2+
using System.Diagnostics;
23
using System.Globalization;
34
using System.Text;
45
using Microsoft.UI.Dispatching;
@@ -42,6 +43,7 @@ public sealed partial class SegmentedMeter : Control
4243
private const double FallbackThickness = 8;
4344
private const double FallbackMarkerSize = 8;
4445
private const double FallbackVerticalMinLength = 120;
46+
private const double AnimationDuration = 450;
4547

4648
private static readonly int KindCount = Enum.GetValues<MeterSegmentKind>().Length;
4749
private static readonly int SizeCount = Enum.GetValues<MeterBarSize>().Length;
@@ -58,10 +60,13 @@ public sealed partial class SegmentedMeter : Control
5860
private readonly ObservableCollection<MeterSegment> _segments = new();
5961
private readonly List<MeterSegment> _tracked = new();
6062
private readonly List<double> _fractions = new();
63+
private readonly List<BarSlot> _bars = new();
64+
private readonly Dictionary<MeterSegment, double> _previous = new();
6165
private readonly Style?[] _segmentStyles;
6266
private readonly Style?[] _iconStyles;
6367
private readonly double[] _thicknesses;
6468
private readonly DispatcherQueueHandler _rebuildHandler;
69+
private readonly EventHandler<object> _renderingHandler;
6570

6671
private FrameworkElement _root = null!;
6772
private ContentPresenter _start = null!;
@@ -71,12 +76,23 @@ public sealed partial class SegmentedMeter : Control
7176
private MeterLegendPanel _legend = null!;
7277
private ContentPresenter _end = null!;
7378

79+
private ColumnDefinition? _restColumn;
80+
private RowDefinition? _restRow;
7481
private Style? _legendItemStyle;
7582
private Style? _legendLabelStyle;
7683
private Style? _legendValueStyle;
7784
private double _markerSize = FallbackMarkerSize;
7885
private double _verticalMinLength = FallbackVerticalMinLength;
7986

87+
private bool _barHorizontal;
88+
private double _barRadius;
89+
private double _barGap;
90+
private int _shapeFirst;
91+
private int _shapeLast;
92+
private bool _shapeFull;
93+
private long _animationStart;
94+
private bool _animating;
95+
8096
private bool _loaded;
8197
private bool _templated;
8298
private bool _firstShow = true;
@@ -95,6 +111,7 @@ public SegmentedMeter()
95111
_iconStyles = new Style?[KindCount];
96112
_thicknesses = new double[SizeCount];
97113
_rebuildHandler = OnRebuildRequested;
114+
_renderingHandler = OnRendering;
98115

99116
_segments.CollectionChanged += OnSegmentsChanged;
100117
Loaded += OnLoaded;
@@ -162,6 +179,7 @@ private void OnLoaded(object sender, RoutedEventArgs e)
162179
private void OnUnloaded(object sender, RoutedEventArgs e)
163180
{
164181
_loaded = false;
182+
StopAnimation();
165183
DetachSegments();
166184
}
167185

@@ -255,7 +273,9 @@ private void Rebuild()
255273

256274
bool legend = BuildLegend();
257275
ArrangeBody(horizontal, legend);
258-
BuildBar(horizontal, used);
276+
277+
if (BuildBar(horizontal, used)) StartAnimation();
278+
else StopAnimation();
259279
}
260280

261281
private double Fraction(double value)
@@ -326,12 +346,16 @@ private void ArrangeBody(bool horizontal, bool legend)
326346
}
327347
}
328348

329-
private void BuildBar(bool horizontal, double used)
349+
private bool BuildBar(bool horizontal, double used)
330350
{
331351
double thickness = ResolveThickness();
332352
double radius = thickness / 2;
333-
double gap = Math.Max(0, Gap);
334-
bool full = used >= 1 - Tolerance;
353+
354+
_barHorizontal = horizontal;
355+
_barRadius = radius;
356+
_barGap = Math.Max(0, Gap);
357+
_shapeFirst = -2;
358+
_shapeLast = -2;
335359

336360
_track.CornerRadius = new CornerRadius(radius);
337361

@@ -348,87 +372,191 @@ private void BuildBar(bool horizontal, double used)
348372
_track.MinHeight = _verticalMinLength;
349373
}
350374

375+
_previous.Clear();
376+
for (int i = 0; i < _bars.Count; i++) _previous[_bars[i].Segment] = _bars[i].Current;
377+
_bars.Clear();
378+
351379
_barGrid.Children.Clear();
352380
_barGrid.ColumnDefinitions.Clear();
353381
_barGrid.RowDefinitions.Clear();
354382

355-
int last = -1;
356-
for (int i = _fractions.Count - 1; i >= 0; i--)
383+
bool reveal = false;
384+
if (_firstShow && used > 0)
357385
{
358-
if (_fractions[i] > 0)
359-
{
360-
last = i;
361-
break;
362-
}
386+
_firstShow = false;
387+
reveal = Animated;
363388
}
364389

390+
bool animate = Animated && !reveal;
391+
bool changed = false;
392+
365393
Compositor? compositor = null;
366394
CubicBezierEasingFunction? easing = null;
367395

368-
if (_firstShow && last >= 0)
396+
if (reveal)
369397
{
370-
_firstShow = false;
371-
372-
if (Animated)
373-
{
374-
compositor = ElementCompositionPreview.GetElementVisual(_barGrid).Compositor;
375-
easing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.22f, 1f), new Vector2(0.36f, 1f));
376-
}
398+
compositor = ElementCompositionPreview.GetElementVisual(_barGrid).Compositor;
399+
easing = compositor.CreateCubicBezierEasingFunction(new Vector2(0.22f, 1f), new Vector2(0.36f, 1f));
377400
}
378401

379402
int ordinal = 0;
380403
for (int i = 0; i < _fractions.Count; i++)
381404
{
382-
double fraction = _fractions[i];
383-
if (fraction <= 0) continue;
384-
385405
var segment = _segments[i];
386-
bool isLast = i == last;
387-
double start = ordinal == 0 ? radius : 0;
388-
double end = isLast && full ? radius : 0;
389-
double trailingGap = isLast ? 0 : gap;
390-
391-
var bar = new Border();
406+
double target = _fractions[i];
407+
double from = target;
392408

393-
if (horizontal)
394-
{
395-
bar.Margin = new Thickness(0, 0, trailingGap, 0);
396-
bar.CornerRadius = new CornerRadius(start, end, end, start);
397-
}
398-
else
399-
{
400-
bar.Margin = new Thickness(0, 0, 0, trailingGap);
401-
bar.CornerRadius = new CornerRadius(start, start, end, end);
402-
}
409+
if (animate) from = _previous.TryGetValue(segment, out var shown) ? shown : 0;
410+
if (target <= 0 && from <= 0) continue;
403411

412+
var bar = new Border();
404413
Paint(bar, ResolveKind(segment, i), segment.Fill);
405414
ToolTipService.SetToolTip(bar, FormatTip(segment));
406415

407-
var length = new GridLength(fraction, GridUnitType.Star);
416+
var length = new GridLength(from, GridUnitType.Star);
417+
ColumnDefinition? column = null;
418+
RowDefinition? row = null;
419+
408420
if (horizontal)
409421
{
410-
_barGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = length });
422+
column = new ColumnDefinition { Width = length };
423+
_barGrid.ColumnDefinitions.Add(column);
411424
Grid.SetColumn(bar, ordinal);
412425
}
413426
else
414427
{
415-
_barGrid.RowDefinitions.Add(new RowDefinition { Height = length });
428+
row = new RowDefinition { Height = length };
429+
_barGrid.RowDefinitions.Add(row);
416430
Grid.SetRow(bar, ordinal);
417431
}
418432

419433
_barGrid.Children.Add(bar);
434+
_bars.Add(new BarSlot(segment, bar, column, row, from, target));
420435

436+
if (from != target) changed = true;
421437
if (compositor is not null && easing is not null) Reveal(bar, ordinal, compositor, easing, horizontal);
422438

423439
ordinal++;
424440
}
425441

426-
if (!full)
442+
_restColumn = null;
443+
_restRow = null;
444+
445+
if (horizontal)
446+
{
447+
_restColumn = new ColumnDefinition();
448+
_barGrid.ColumnDefinitions.Add(_restColumn);
449+
}
450+
else
427451
{
428-
var rest = new GridLength(1 - used, GridUnitType.Star);
429-
if (horizontal) _barGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = rest });
430-
else _barGrid.RowDefinitions.Add(new RowDefinition { Height = rest });
452+
_restRow = new RowDefinition();
453+
_barGrid.RowDefinitions.Add(_restRow);
431454
}
455+
456+
ApplyFractions();
457+
return changed;
458+
}
459+
460+
private void ApplyFractions()
461+
{
462+
double used = 0;
463+
int first = -1;
464+
int last = -1;
465+
466+
for (int i = 0; i < _bars.Count; i++)
467+
{
468+
double current = _bars[i].Current;
469+
470+
if (current > 0)
471+
{
472+
if (first < 0) first = i;
473+
last = i;
474+
}
475+
476+
used += current;
477+
}
478+
479+
bool full = used >= 1 - Tolerance;
480+
481+
if (first != _shapeFirst || last != _shapeLast || full != _shapeFull)
482+
{
483+
_shapeFirst = first;
484+
_shapeLast = last;
485+
_shapeFull = full;
486+
ApplyShape(first, last, full);
487+
}
488+
489+
var rest = new GridLength(Math.Max(0, 1 - used), GridUnitType.Star);
490+
if (_restColumn is not null) _restColumn.Width = rest;
491+
if (_restRow is not null) _restRow.Height = rest;
492+
}
493+
494+
private void ApplyShape(int first, int last, bool full)
495+
{
496+
for (int i = 0; i < _bars.Count; i++)
497+
{
498+
var bar = _bars[i].Element;
499+
double start = i == first ? _barRadius : 0;
500+
double end = i == last && full ? _barRadius : 0;
501+
double trailingGap = i == last ? 0 : _barGap;
502+
503+
if (_barHorizontal)
504+
{
505+
bar.Margin = new Thickness(0, 0, trailingGap, 0);
506+
bar.CornerRadius = new CornerRadius(start, end, end, start);
507+
}
508+
else
509+
{
510+
bar.Margin = new Thickness(0, 0, 0, trailingGap);
511+
bar.CornerRadius = new CornerRadius(start, start, end, end);
512+
}
513+
}
514+
}
515+
516+
private void StartAnimation()
517+
{
518+
_animationStart = Stopwatch.GetTimestamp();
519+
if (_animating) return;
520+
521+
_animating = true;
522+
Microsoft.UI.Xaml.Media.CompositionTarget.Rendering += _renderingHandler;
523+
}
524+
525+
private void StopAnimation()
526+
{
527+
if (!_animating) return;
528+
529+
_animating = false;
530+
Microsoft.UI.Xaml.Media.CompositionTarget.Rendering -= _renderingHandler;
531+
}
532+
533+
private void OnRendering(object? sender, object e)
534+
{
535+
if (!Active)
536+
{
537+
StopAnimation();
538+
return;
539+
}
540+
541+
double progress = Math.Min(1, Stopwatch.GetElapsedTime(_animationStart).TotalMilliseconds / AnimationDuration);
542+
double inverse = 1 - progress;
543+
double eased = 1 - inverse * inverse * inverse;
544+
545+
for (int i = 0; i < _bars.Count; i++)
546+
{
547+
var slot = _bars[i];
548+
if (slot.From == slot.To) continue;
549+
550+
slot.Current = progress >= 1 ? slot.To : slot.From + (slot.To - slot.From) * eased;
551+
552+
var length = new GridLength(slot.Current, GridUnitType.Star);
553+
if (slot.Column is not null) slot.Column.Width = length;
554+
else if (slot.Row is not null) slot.Row.Height = length;
555+
}
556+
557+
ApplyFractions();
558+
559+
if (progress >= 1) StopAnimation();
432560
}
433561

434562
private bool BuildLegend()

0 commit comments

Comments
 (0)