Adding markers to the scrubber. (#318)

* Adding markers to the scrubber.

This is a rewrite of the Scrubber to support markers. It also is a much nicer implementation of the
scrubber that reuses more of the XAML Slider functionality, and reduces the amount of copied XAML from
the default Slider style (from generic.xaml).

As I was adding this change, it became apparent that the way the XAML slider draws its track would
not work well with markers at 0.0 or 1.0 because the track ends up extending before and after the
0.0 and 1.0 positions which would make the markers look like they were incorrectly placed. So now
we do all of the drawing (track, decrease rectangle, and thumb) using Composition.

We also now mimic the mouse-over behavior of Slider so that the control feels a little more engaging.
This required hooking Composition into the VisualStateManager, which is achieved by replacing the
default VisualStateManager with our own implementation.
This commit is contained in:
Simeon 2020-08-06 13:57:51 -07:00 коммит произвёл GitHub
Родитель 9aa6344e60
Коммит 9ddf5e8113
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 563 добавлений и 372 удалений

Просмотреть файл

@ -15,31 +15,29 @@ namespace LottieViewer
/// </summary>
sealed class LottieVisualDiagnosticsViewModel : INotifyPropertyChanged
{
LottieVisualDiagnostics _wrapped;
public event PropertyChangedEventHandler PropertyChanged;
public object DiagnosticsObject
{
get => _wrapped;
get => LottieVisualDiagnostics;
set
{
_wrapped = (LottieVisualDiagnostics)value;
LottieVisualDiagnostics = (LottieVisualDiagnostics)value;
PlayerIssues.Clear();
Markers.Clear();
if (value != null)
{
foreach (var issue in _wrapped.JsonParsingIssues.
Concat(_wrapped.LottieValidationIssues).
Concat(_wrapped.TranslationIssues).
foreach (var issue in LottieVisualDiagnostics.JsonParsingIssues.
Concat(LottieVisualDiagnostics.LottieValidationIssues).
Concat(LottieVisualDiagnostics.TranslationIssues).
OrderBy(a => a.Code).
ThenBy(a => a.Description))
{
PlayerIssues.Add(issue);
}
foreach (var marker in _wrapped.Markers)
foreach (var marker in LottieVisualDiagnostics.Markers)
{
Markers.Add((marker.Key, marker.Value));
}
@ -57,14 +55,16 @@ namespace LottieViewer
}
}
public string DurationText => _wrapped is null ? string.Empty : $"{_wrapped.Duration.TotalSeconds} secs";
public LottieVisualDiagnostics LottieVisualDiagnostics { get; private set; }
public string FileName => _wrapped?.FileName ?? string.Empty;
public string DurationText => LottieVisualDiagnostics is null ? string.Empty : $"{LottieVisualDiagnostics.Duration.TotalSeconds} secs";
public string FileName => LottieVisualDiagnostics?.FileName ?? string.Empty;
public ObservableCollection<(string Name, double Offset)> Markers { get; } = new ObservableCollection<(string, double)>();
public string MarkersText =>
_wrapped is null ? string.Empty : string.Join(", ", Markers.Select(value => $"{value.Name}={value.Offset:0.###}"));
LottieVisualDiagnostics is null ? string.Empty : string.Join(", ", Markers.Select(value => $"{value.Name}={value.Offset:0.###}"));
public bool PlayerHasIssues => PlayerIssues.Count > 0;
@ -74,13 +74,13 @@ namespace LottieViewer
{
get
{
if (_wrapped is null)
if (LottieVisualDiagnostics is null)
{
return string.Empty;
}
var aspectRatio = FloatToRatio(_wrapped.LottieWidth / _wrapped.LottieHeight);
return $"{_wrapped.LottieWidth}x{_wrapped.LottieHeight} ({aspectRatio.Item1:0.##}:{aspectRatio.Item2:0.##})";
var aspectRatio = FloatToRatio(LottieVisualDiagnostics.LottieWidth / LottieVisualDiagnostics.LottieHeight);
return $"{LottieVisualDiagnostics.LottieWidth}x{LottieVisualDiagnostics.LottieHeight} ({aspectRatio.Item1:0.##}:{aspectRatio.Item2:0.##})";
}
}

Просмотреть файл

@ -223,7 +223,8 @@
RelativePanel.AlignRightWithPanel="True"
RelativePanel.AlignTopWithPanel="True"
RelativePanel.RightOf="_playStopButton"
ValueChanged="ProgressSliderChanged" />
ValueChanged="ProgressSliderChanged"
DiagnosticsObject="{x:Bind _stage.Player.Diagnostics, Mode=OneWay}" />
</RelativePanel>
</Grid>
</RelativePanel>

Просмотреть файл

@ -1,262 +1,222 @@
<UserControl x:Class="LottieViewer.Scrubber"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:LottieViewer"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="ScrubberSliderStyle"
TargetType="Slider">
<Setter Property="Background" Value="{ThemeResource SliderTrackFill}" />
<Setter Property="BorderThickness" Value="{ThemeResource SliderBorderThemeThickness}" />
<Setter Property="Foreground" Value="{ThemeResource SliderTrackValueFill}" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="ManipulationMode" Value="None" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-7,0,-7,0" />
<Setter Property="IsFocusEngagementEnabled" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style x:Key="SliderThumbStyle"
TargetType="Thumb">
<Setter Property="BorderThickness" Value="0" />
<!--<Setter Property="Background" Value="{ThemeResource SliderThumbBackground}"/>-->
<Setter Property="Background" Value="{StaticResource LottieBasicBrush}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Ellipse x:Name="ellipse"
Fill="{TemplateBinding Foreground}"
Stroke="{TemplateBinding Background}"
StrokeThickness="2" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
Margin="{ThemeResource SliderHeaderThemeMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate"
MinHeight="44">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="18" />
<RowDefinition Height="Auto" />
<RowDefinition Height="18" />
</Grid.RowDefinitions>
<Rectangle x:Name="HorizontalTrackRect"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderTrackThemeHeight}"
Fill="{TemplateBinding Background}" />
<Rectangle x:Name="HorizontalDecreaseRect"
Grid.Row="1"
Fill="{StaticResource LottieBasicBrush}" />
<TickBar x:Name="TopTickBar"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,0,0,4"
VerticalAlignment="Bottom"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<TickBar x:Name="HorizontalInlineTickBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderTrackThemeHeight}"
Fill="{ThemeResource SliderInlineTickBarFill}"
Visibility="Collapsed" />
<TickBar x:Name="BottomTickBar"
Grid.Row="2"
Grid.ColumnSpan="3"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
Margin="0,4,0,0"
VerticalAlignment="Top"
Fill="{ThemeResource SliderTickBarFill}"
Visibility="Collapsed" />
<!--<Thumb x:Name="HorizontalThumb"
AutomationProperties.AccessibilityView="Raw"
Grid.Column="1"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Height="20" Width="20"
Grid.RowSpan="3" Grid.Row="0" Style="{StaticResource SliderThumbStyle}" />-->
<Thumb x:Name="HorizontalThumb"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Width="24"
Height="24"
AutomationProperties.AccessibilityView="Raw"
DataContext="{TemplateBinding Value}"
FocusVisualMargin="-14,-6,-14,-6"
Opacity="0"
Style="{StaticResource SliderThumbStyle}" />
</Grid>
<UserControl
x:Class="LottieViewer.Scrubber"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LottieViewer"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="40"
d:DesignWidth="400">
<Grid>
<!-- Markers are drawn here, under the slider. -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="3*"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid x:Name="_markersTop"/>
<Grid x:Name="_markersBottom" Grid.Row="2"/>
</Grid>
<Slider x:Name="_slider"
Maximum="1"
StepFrequency="0.0001"
SmallChange="0.001"
AutomationProperties.Name="Progress slider">
<Slider.Template>
<!-- This is a copy of the default Slider template, but modified:
* Made mostly transparent so that the drawing can be done by Composition instead of XAML.
* Thumb is an ellipse instead of a rounded rectangle.
* Only supports horizontal orientation (vertical stuff is stripped out).
-->
<ControlTemplate TargetType="Slider">
<Grid Margin="{TemplateBinding Padding}">
<Grid.Resources>
<Style TargetType="Thumb" x:Key="SliderThumbStyle">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{ThemeResource SliderThumbBackground}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<!-- The content of the Thumb has been modified to make it an Ellipse instead of
a border with rounded corners. -->
<Ellipse
Opacity="0"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<VisualStateManager.CustomVisualStateManager>
<local:SliderVisualStateManager/>
</VisualStateManager.CustomVisualStateManager>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalTrackRect" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderContainerBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalDecreaseRect" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderHeaderForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalDecreaseRect" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalTrackRect" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderContainerBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalTrackRect" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderContainerBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalDecreaseRect" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusEngagementStates">
<VisualState x:Name="FocusDisengaged" />
<VisualState x:Name="FocusEngagedHorizontal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer" Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
<DiscreteObjectKeyFrame KeyTime="0" Value="False" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalThumb" Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
<DiscreteObjectKeyFrame KeyTime="0" Value="True" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="{ThemeResource SliderHeaderThemeFontWeight}"
Foreground="{ThemeResource SliderHeaderForeground}"
Margin="{ThemeResource SliderTopHeaderMargin}"
TextWrapping="Wrap"
Visibility="Collapsed"
x:DeferLoadStrategy="Lazy"/>
<Grid x:Name="SliderContainer"
Grid.Row="1"
Background="{ThemeResource SliderContainerBackground}"
Control.IsTemplateFocusTarget="True">
<Grid x:Name="HorizontalTemplate" MinHeight="{ThemeResource SliderHorizontalHeight}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="{ThemeResource SliderPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{ThemeResource SliderPostContentMargin}" />
</Grid.RowDefinitions>
<Rectangle x:Name="HorizontalTrackRect"
Opacity="0"
Fill="{TemplateBinding Background}"
Height="{ThemeResource SliderTrackThemeHeight}"
Grid.Row="1"
Grid.ColumnSpan="3" />
<Rectangle x:Name="HorizontalDecreaseRect" Fill="{TemplateBinding Foreground}" Grid.Row="1" Opacity="0" />
<TickBar x:Name="TopTickBar"
Visibility="Collapsed"
Fill="{ThemeResource SliderTickBarFill}"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
VerticalAlignment="Bottom"
Margin="0,0,0,4"
Grid.ColumnSpan="3" />
<TickBar x:Name="HorizontalInlineTickBar"
Visibility="Collapsed"
Fill="{ThemeResource SliderInlineTickBarFill}"
Height="{ThemeResource SliderTrackThemeHeight}"
Grid.Row="1"
Grid.ColumnSpan="3" />
<TickBar x:Name="BottomTickBar"
Visibility="Collapsed"
Fill="{ThemeResource SliderTickBarFill}"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
VerticalAlignment="Top"
Margin="0,4,0,0"
Grid.Row="2"
Grid.ColumnSpan="3" />
<!-- The left and right TickBars are needed even though we are always horizontal. They are referenced
by the Scrubber when it is not enabled. -->
<TickBar x:Name="LeftTickBar"
Visibility="Collapsed"
Fill="{ThemeResource SliderTickBarFill}"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
VerticalAlignment="Top"
Margin="0,4,0,0"
Grid.Row="2"
Grid.ColumnSpan="3" />
<TickBar x:Name="RightTickBar"
Visibility="Collapsed"
Fill="{ThemeResource SliderTickBarFill}"
Height="{ThemeResource SliderOutsideTickBarThemeHeight}"
VerticalAlignment="Top"
Margin="0,4,0,0"
Grid.Row="2"
Grid.ColumnSpan="3" />
<!-- The Width of the Thumb has been modified to make it round. -->
<Thumb x:Name="HorizontalThumb"
Opacity="0"
Style="{StaticResource SliderThumbStyle}"
DataContext="{TemplateBinding Value}"
Height="20"
Width="20"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
FocusVisualMargin="-14,-6,-14,-6"
AutomationProperties.AccessibilityView="Raw" />
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalTrackRect"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderTrackFillPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalThumb"
Storyboard.TargetProperty="Background">
<!--<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundPressed}"/>-->
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource LottieBasicBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderContainerBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalDecreaseRect"
Storyboard.TargetProperty="Fill">
<!--<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillPressed}"/>-->
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource LottieBasicBrush}" />
</ObjectAnimationUsingKeyFrames>
<!-- Make the thumb appear when pressed -->
<!--<DoubleAnimationUsingKeyFrames Storyboard.TargetName="HorizontalThumb" Storyboard.TargetProperty="Opacity">
<DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>
</DoubleAnimationUsingKeyFrames>-->
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter"
Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderHeaderForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalDecreaseRect"
Storyboard.TargetProperty="Fill">
<!--<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackValueFillDisabled}"/>-->
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource DisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalTrackRect"
Storyboard.TargetProperty="Fill">
<!--<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTrackFillDisabled}"/>-->
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource DisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalThumb"
Storyboard.TargetProperty="Background">
<!--<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundDisabled}"/>-->
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource DisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="TopTickBar"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderTickBarFillDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BottomTickBar"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderTickBarFillDisabled}" />
</ObjectAnimationUsingKeyFrames>
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LeftTickBar" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTickBarFillDisabled}"/>
</ObjectAnimationUsingKeyFrames>-->
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="RightTickBar" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderTickBarFillDisabled}"/>
</ObjectAnimationUsingKeyFrames>-->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderContainerBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalTrackRect"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderTrackFillPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalThumb" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SliderThumbBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>-->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer"
Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderContainerBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalDecreaseRect"
Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SliderTrackValueFillPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusEngagementStates">
<VisualState x:Name="FocusDisengaged" />
<VisualState x:Name="FocusEngagedHorizontal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SliderContainer"
Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
<DiscreteObjectKeyFrame KeyTime="0"
Value="False" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalThumb"
Storyboard.TargetProperty="(Control.IsTemplateFocusTarget)">
<DiscreteObjectKeyFrame KeyTime="0"
Value="True" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Slider x:Name="_slider"
HorizontalAlignment="Stretch"
AutomationProperties.Name="Progress slider"
VerticalAlignment="Center"
LargeChange="0.001"
Maximum="1"
Minimum="0"
SmallChange="0.001"
StepFrequency="0.001"
Style="{StaticResource ScrubberSliderStyle}" />
</Grid>
</ControlTemplate>
</Slider.Template>
</Slider>
</Grid>
</UserControl>

Просмотреть файл

@ -1,128 +1,138 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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.
using System;
using System.Collections.Specialized;
using System.Numerics;
using Microsoft.Toolkit.Uwp.UI.Lottie;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Hosting;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;
namespace LottieViewer
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public delegate void ScrubberValueChangedEventHandler(Scrubber sender, ScrubberValueChangedEventArgs args);
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable SA1649 // File name should match first type name
#pragma warning disable SA1402 // File may only contain a single type
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public sealed class ScrubberValueChangedEventArgs
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
#pragma warning restore SA1402 // File may only contain a single type
#pragma warning restore SA1649 // File name should match first type name
{
internal ScrubberValueChangedEventArgs(double oldValue, double newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public double OldValue { get; }
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public double NewValue { get; }
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
}
#pragma warning disable SA1303 // Constants must begin with an upper case letter.
#pragma warning disable SA1402 // File may only contain a single type.
/// <summary>
/// Scrubber.
/// A slider-like control for displaying and setting the position of a Lottie animation. Also
/// displays any markers that are part of the Lottie animation.
/// </summary>
public sealed partial class Scrubber : UserControl
{
// Set this to 0 for production. Offsets the Composition pieces to make them
// easier to see when the slider is opaque.
const float c_verticalOffset = 0;
// These values should not be changed - some of the values are hard coded and
// assume the current values.
const float c_thumbRadius = 9;
const float c_thumbStrokeThickness = 2;
const float c_trackWidth = 2;
// The margin on the left and right side of the margin.
const float c_trackMargin = 9;
readonly CompositionPropertySet _properties;
readonly SpriteVisual _trackbar;
readonly SpriteVisual _decreaseRectangle;
readonly SpriteVisual _trackRectangle;
readonly ShapeVisual _thumb;
readonly CompositionBrush _scrubberEnabledBrush;
readonly CompositionBrush _scrubberDisabledBrush;
readonly CompositionColorBrush _trackRectangleBrush;
readonly CompositionColorBrush _decreaseRectangleBrush;
readonly SolidColorBrush _markerBrush;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public event ScrubberValueChangedEventHandler ValueChanged;
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
readonly LottieVisualDiagnosticsViewModel _diagnostics = new LottieVisualDiagnosticsViewModel();
string _currentVisualStateName;
public static readonly DependencyProperty DiagnosticsObjectProperty =
DependencyProperty.Register("DiagnosticsObject", typeof(object), typeof(Scrubber), new PropertyMetadata(null, OnDiagnosticsObjectChanged));
public event TypedEventHandler<Scrubber, ScrubberValueChangedEventArgs> ValueChanged;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public Scrubber()
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
this.InitializeComponent();
const float thumbRadius = 8;
const float thumbStrokeThickness = 1.5F;
_diagnostics.Markers.CollectionChanged += Markers_CollectionChanged;
var c = Window.Current.Compositor;
// Create the brush used for markers.
_markerBrush = new SolidColorBrush(GetResourceBrushColor("LottieBasicBrush"));
// Get the brushes
_scrubberEnabledBrush = c.CreateColorBrush((Color)App.Current.Resources["LottieBasic"]);
_scrubberDisabledBrush = c.CreateColorBrush((Color)App.Current.Resources["DisabledColor"]);
// Set our tooltip converter so we are in charge of what is shown on the tooltip.
_slider.ThumbToolTipValueConverter = new ThumbTooltipConverter(this);
// Forward slider change events.
// Forward the slider change events to any event listeners.
_slider.ValueChanged += (sender, e) => ValueChanged?.Invoke(this, new ScrubberValueChangedEventArgs(e.OldValue, e.NewValue));
// Create comp objects for the trackbar and the thumb.
// Set up the Windows.UI.Composition pieces that will display the parts of the slider.
// The XAML slider has its parts set to Opacity=0 so they aren't visible and are
// only used to handle user input.
var c = Window.Current.Compositor;
var container = c.CreateContainerVisual();
// Save the properties. These will be used to animate the trackbard and thumb.
// Get the property set. This will be used to animate the trackbar and thumb.
_properties = container.Properties;
// Create the trackbar
_trackbar = c.CreateSpriteVisual();
container.Children.InsertAtTop(_trackbar);
// Add a property to scale the width of the track and decrease rectangles.
_properties.InsertScalar("Width", default);
// Move the trackbar into the horizontal track of the slider.
_trackbar.Offset = new System.Numerics.Vector3(1, 18, 0);
// Create the brushes and set up expression animations so their colors can
// be changed by writing to the property set.
var thumbFillBrush = CreateBoundColorBrush(_properties, "ThumbFillColor");
var thumbStrokeBrush = CreateBoundColorBrush(_properties, "ThumbStrokeColor");
_trackRectangleBrush = CreateBoundColorBrush(_properties, "TrackColor");
_decreaseRectangleBrush = CreateBoundColorBrush(_properties, "DecreaseRectangleColor");
// Create the track rectangle. This is the track that the thumb moves along.
_trackRectangle = c.CreateSpriteVisual();
_trackRectangle.Size = new Vector2(0, c_trackWidth);
_trackRectangle.Brush = _trackRectangleBrush;
container.Children.InsertAtTop(_trackRectangle);
// Create the decrease rectangle. This is the rectangle that will change in width as the
// slider position is changed. It shows on the left side of the thumb to indicate how
// far along the track the position is.
_decreaseRectangle = c.CreateSpriteVisual();
_decreaseRectangle.Size = new Vector2(0, c_trackWidth);
_decreaseRectangle.Brush = _decreaseRectangleBrush;
container.Children.InsertAtTop(_decreaseRectangle);
// Move the decrease and track rectangles into the track of the slider.
_trackRectangle.Offset = new Vector3(9, 15.5F + c_verticalOffset, 0);
_decreaseRectangle.Offset = new Vector3(9, 15.5F + c_verticalOffset, 0);
// Create the thumb.
_thumb = c.CreateShapeVisual();
var thumbEllipse = c.CreateEllipseGeometry();
thumbEllipse.Radius = new System.Numerics.Vector2(thumbRadius);
thumbEllipse.Center = new System.Numerics.Vector2(thumbRadius + thumbStrokeThickness);
thumbEllipse.Radius = new Vector2(c_thumbRadius);
thumbEllipse.Center = new Vector2(c_thumbRadius + c_thumbStrokeThickness);
var thumbShape = c.CreateSpriteShape(thumbEllipse);
thumbShape.FillBrush = _scrubberEnabledBrush;
thumbShape.StrokeBrush = _scrubberEnabledBrush;
thumbShape.StrokeThickness = thumbStrokeThickness;
thumbShape.FillBrush = thumbFillBrush;
thumbShape.StrokeBrush = thumbStrokeBrush;
thumbShape.StrokeThickness = c_thumbStrokeThickness;
_thumb.Shapes.Add(thumbShape);
_thumb.Size = new System.Numerics.Vector2((thumbRadius + thumbStrokeThickness) * 2);
_thumb.Offset = new System.Numerics.Vector3(0, 7, 0);
_thumb.Size = new Vector2((c_thumbRadius + c_thumbStrokeThickness) * 2);
// X value doesn't matter for Offset because it is controlled by an expression animation.
// The Y value is used to position the thumb vertically so it is centered over the track.
_thumb.Offset = new Vector3(0, c_thumbRadius + c_verticalOffset - 3.75F, 0);
container.Children.InsertAtTop(_thumb);
_trackbar.Brush = _scrubberEnabledBrush;
_properties.InsertScalar("Width", 0);
// Attach our custom-drawn UI as a child visual of the slider.
ElementCompositionPreview.SetElementChildVisual(_slider, container);
// Change colors on enabled/disabled transitions.
IsEnabledChanged += (sender, e) =>
{
if ((bool)e.NewValue)
{
// Becoming enabled.
_trackbar.Brush = _scrubberEnabledBrush;
thumbShape.FillBrush = _scrubberEnabledBrush;
thumbShape.StrokeBrush = _scrubberEnabledBrush;
}
else
{
// Becoming disabled.
_trackbar.Brush = _scrubberDisabledBrush;
thumbShape.FillBrush = _scrubberDisabledBrush;
thumbShape.StrokeBrush = _scrubberDisabledBrush;
}
};
// Set the initial colors.
UpdateColors();
}
/// <summary>
@ -134,33 +144,253 @@ namespace LottieViewer
set => _slider.Value = value;
}
// Associates the given object with the scrubber. The object is expected to have a property called "Progress"
// that the scrubber position will be bound to.
public object DiagnosticsObject
{
get { return (object)GetValue(DiagnosticsObjectProperty); }
set { SetValue(DiagnosticsObjectProperty, value); }
}
// Associates the given CompositionObject with the scrubber. The object is required
// to have a property called "Progress" that the scrubber position will be bound to.
internal void SetAnimatedCompositionObject(CompositionObject obj)
{
var c = Window.Current.Compositor;
var rectAnim = c.CreateExpressionAnimation("Vector2(comp.Progress * our.Width, 2)");
rectAnim.SetReferenceParameter("comp", obj);
rectAnim.SetReferenceParameter("our", _properties);
_trackbar.StartAnimation("Size", rectAnim);
// For thumbRadius = 11:
//var thumbAnim = c.CreateExpressionAnimation("Vector3((comp.Progress * (our.Width - 22)) - 1, 6, 0)");
// For thumbRadius = 8
var thumbAnim = c.CreateExpressionAnimation("Vector3((comp.Progress * (our.Width - 16)) - 1, 9.5, 0)");
thumbAnim.SetReferenceParameter("comp", obj);
thumbAnim.SetReferenceParameter("our", _properties);
_thumb.StartAnimation("Offset", thumbAnim);
var decreaseRectAnimation = c.CreateExpressionAnimation($"comp.Progress * (our.Width - {c_trackMargin * 2})");
decreaseRectAnimation.SetReferenceParameter("comp", obj);
decreaseRectAnimation.SetReferenceParameter("our", _properties);
_decreaseRectangle.StartAnimation("Size.X", decreaseRectAnimation);
var trackRectangleAnimation = c.CreateExpressionAnimation($"our.Width - {c_trackMargin * 2}");
trackRectangleAnimation.SetReferenceParameter("comp", obj);
trackRectangleAnimation.SetReferenceParameter("our", _properties);
_trackRectangle.StartAnimation("Size.X", trackRectangleAnimation);
var thumbPositionAnimation = c.CreateExpressionAnimation($"comp.Progress * (our.Width - {(c_thumbRadius + c_thumbStrokeThickness) * 1.78}) - 1");
thumbPositionAnimation.SetReferenceParameter("comp", obj);
thumbPositionAnimation.SetReferenceParameter("our", _properties);
_thumb.StartAnimation("Offset.X", thumbPositionAnimation);
}
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
protected override Size ArrangeOverride(Size finalSize)
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
static void OnDiagnosticsObjectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// Update the size of the progress rectangle.
_properties.InsertScalar("Width", (float)finalSize.Width - 2);
var me = (Scrubber)d;
me._diagnostics.DiagnosticsObject = (LottieVisualDiagnostics)e.NewValue;
}
return base.ArrangeOverride(finalSize);
protected override Size ArrangeOverride(Size finalSize)
{
// Arrange the elements. This has to be done before asking the
// slider for its size.
var result = base.ArrangeOverride(finalSize);
var sliderWidth = _slider.ActualWidth;
// Update the size of the progress rectangle and the position of the thumb.
_properties.InsertScalar("Width", (float)sliderWidth);
// Update the position of the markers.
// Subtract 1 to the width because we need to allow for a marker at offset 1, and
// the markers are 1 pixel wide.
var barWidth = sliderWidth - 1 - (c_trackMargin * 2);
// Adjust the position of the markers.
// Set the margin on each of the rectangles in the grid so that they match
// the offsets of the markers in the view model.
for (var i = 0; i < _diagnostics.Markers.Count; i++)
{
var topRect = (Rectangle)_markersTop.Children[i];
var bottomRect = (Rectangle)_markersBottom.Children[i];
var offset = _diagnostics.Markers[i].Offset;
topRect.Margin = new Thickness((offset * barWidth) + c_trackMargin, 0, 0, 0);
bottomRect.Margin = new Thickness((offset * barWidth) + c_trackMargin, 0, 0, 0);
}
return result;
}
// Returns a color brush which has its color bound to the property with the given name.
static CompositionColorBrush CreateBoundColorBrush(CompositionPropertySet propertySet, string propertyName)
{
var c = propertySet.Compositor;
var result = c.CreateColorBrush();
propertySet.InsertColor(propertyName, default);
var expressionAnimation = c.CreateExpressionAnimation($"our.{propertyName}");
expressionAnimation.SetReferenceParameter("our", propertySet);
result.StartAnimation(nameof(result.Color), expressionAnimation);
return result;
}
Brush GetResourceBrush(string resourceName) => (Brush)App.Current.Resources[resourceName];
Color GetResourceBrushColor(string resourceName) => ((SolidColorBrush)GetResourceBrush(resourceName)).Color;
void UpdateColors()
{
switch (_currentVisualStateName)
{
case null:
case "Normal":
SetColors(
markers: GetResourceBrushColor("LottieBasicBrush"),
thumbFill: GetResourceBrushColor("LottieBasicBrush"),
thumbStroke: Colors.Transparent,
track: GetResourceBrushColor("SliderTrackFill"),
decreaseRectangle: GetResourceBrushColor("LottieBasicBrush"));
break;
case "Disabled":
SetColors(
markers: Colors.Transparent,
thumbFill: GetResourceBrushColor("DisabledBrush"),
thumbStroke: Colors.Transparent,
track: GetResourceBrushColor("SliderTrackFillDisabled"),
decreaseRectangle: GetResourceBrushColor("SliderTrackValueFillDisabled"));
break;
case "Pressed":
case "PointerOver":
SetColors(
markers: Colors.White,
thumbFill: GetResourceBrushColor("LottieBasicBrush"),
thumbStroke: GetResourceBrushColor("LottieBasicBrush"),
track: GetResourceBrushColor("SliderTrackFillPointerOver"),
decreaseRectangle: GetResourceBrushColor("LottieBasicBrush"));
break;
default:
throw new InvalidOperationException();
}
}
void SetColors(Color markers, Color thumbFill, Color thumbStroke, Color track, Color decreaseRectangle)
{
_markerBrush.Color = markers;
_properties.InsertColor("ThumbFillColor", thumbFill);
_properties.InsertColor("ThumbStrokeColor", thumbStroke);
_properties.InsertColor("TrackColor", track);
_properties.InsertColor("DecreaseRectangleColor", decreaseRectangle);
}
// Called by our custom VisualStateManager when there is a transition to one of the CommonStates.
internal void OnSliderVisualStateChange(string stateName)
{
_currentVisualStateName = stateName;
UpdateColors();
}
// Returns a single-pixel-wide Rectangle for displaying a marker above or below the track.
Rectangle CreateMarkerRectangle() => new Rectangle() { Fill = _markerBrush, Width = 1, HorizontalAlignment = HorizontalAlignment.Left };
void Markers_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
// Add rectangles to display each marker. There are 2 rectangles - one
// that sits above the track, and one that sits below the track.
_markersTop.Children.Add(CreateMarkerRectangle());
_markersBottom.Children.Add(CreateMarkerRectangle());
break;
case NotifyCollectionChangedAction.Remove:
// One marker was removed - remove a rectangle from the top and bottom.
_markersTop.Children.RemoveAt(0);
_markersBottom.Children.RemoveAt(0);
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
// Moving and replacing doesn't affect the number of items, so nothing to do.
break;
case NotifyCollectionChangedAction.Reset:
// Remove all the rectangles.
_markersTop.Children.Clear();
_markersBottom.Children.Clear();
break;
default:
throw new InvalidOperationException();
}
// Force another arrange so that the markers can be set to the correct positions.
InvalidateArrange();
}
// Formats the tooltip text.
sealed class ThumbTooltipConverter : IValueConverter
{
readonly Scrubber _owner;
internal ThumbTooltipConverter(Scrubber owner)
{
_owner = owner;
}
object IValueConverter.Convert(object value, Type targetType, object parameter, string language)
{
var duration = _owner._diagnostics.LottieVisualDiagnostics.Duration;
return $" {_owner.Value:0.00}\r\n{_owner.Value * duration.TotalSeconds:0.00} secs";
}
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
// A VisualStateManager for the Slider so we can track its visual states and update
// the UI of the Scrubber in sync with the Slider.
internal sealed class SliderVisualStateManager : VisualStateManager
{
// Keep track of the previous state so we don't notify the Scrubber of
// the same state twice in succession.
string _previousCommonState;
protected override bool GoToStateCore(
Control control,
FrameworkElement templateRoot,
string stateName,
VisualStateGroup group,
VisualState state,
bool useTransitions)
{
// Find the Scrubber that this VisualStateManager is under.
var scrubber = GetOwner(control);
if (group?.Name == "CommonStates")
{
var newState = state?.Name;
// Check whether we have already reported this state.
if (_previousCommonState != newState)
{
_previousCommonState = newState;
scrubber.OnSliderVisualStateChange(newState);
}
}
// The base class does the work.
return base.GoToStateCore(control, templateRoot, stateName, group, state, useTransitions);
}
// Returns the Scrubber that the given object is a descendant of.
static Scrubber GetOwner(DependencyObject descendant)
{
var parent = VisualTreeHelper.GetParent(descendant);
return parent is Scrubber scrubber ? scrubber : GetOwner(parent);
}
}
public sealed class ScrubberValueChangedEventArgs
{
internal ScrubberValueChangedEventArgs(double oldValue, double newValue)
{
OldValue = oldValue;
NewValue = newValue;
}
public double NewValue { get; }
public double OldValue { get; }
}
}