Reputation: 6922
Is there any easy way to create a dashed ellipse made up of individual horizontal dashes, where the dash sizes are consistent, and their amount can be specified?
Something like this:
I want to be able to control each dash individually, like changing its color or binding it to an action in my viewmodel.
The only way I can think of to achieve this, is to create a custom control that contains a Path element for each dash, together making up an ellipse shape, having to calculate the Path data based on the amount of dashes and size of the ellipse.
Upvotes: 4
Views: 892
Reputation: 6922
I came back to this problem now, and managed to solve it in a very flexible and generic way. The requirments have changed a bit since then, no need for binding, but it can be added easily.
Note that this is a circle, which is what I wanted. The question should really say circle rather than ellipse, even though a circle is an ellipse, but I digress...
Here's the UserControl I came up with:
StatusRing.xaml.cs
public partial class StatusRing
{
#region Dependency Property registrations
public static readonly DependencyProperty DashesProperty = DependencyProperty.Register("Dashes",
typeof(int), typeof(StatusRing), new PropertyMetadata(32, DashesChanged));
public static readonly DependencyProperty DiameterProperty = DependencyProperty.Register("Diameter",
typeof(double), typeof(StatusRing), new PropertyMetadata(150.00, DiameterChanged));
public static readonly DependencyProperty DashHeightProperty = DependencyProperty.Register("DashHeight",
typeof(double), typeof(StatusRing), new PropertyMetadata(20.00, DashHeightChanged));
public static readonly DependencyProperty DashWidthProperty = DependencyProperty.Register("DashWidth",
typeof(double), typeof(StatusRing), new PropertyMetadata(5.00, DashWidthChanged));
public static readonly DependencyProperty DashFillProperty = DependencyProperty.Register("DashFill",
typeof(SolidColorBrush), typeof(StatusRing), new PropertyMetadata(Brushes.DimGray, DashFillChanged));
public static readonly DependencyProperty DashAccentFillProperty = DependencyProperty.Register("DashAccentFill",
typeof(SolidColorBrush), typeof(StatusRing), new PropertyMetadata(Brushes.White, DashAnimationFillChanged));
public static readonly DependencyProperty TailSizeProperty = DependencyProperty.Register("TailSize",
typeof(int), typeof(StatusRing), new PropertyMetadata(10, TailSizeChanged));
public static readonly DependencyProperty AnimationSpeedProperty = DependencyProperty.Register("AnimationSpeed",
typeof(double), typeof(StatusRing), new PropertyMetadata(50.00, AnimationSpeedChanged));
public static readonly DependencyProperty IsPlayingProperty = DependencyProperty.Register("IsPlaying",
typeof(bool), typeof(StatusRing), new PropertyMetadata(false, IsPlayingChanged));
#endregion Dependency Property registrations
private readonly Storyboard glowAnimationStoryBoard = new Storyboard();
public StatusRing()
{
Loaded += OnLoaded;
InitializeComponent();
}
#region Dependency Properties
public int Dashes
{
get => (int)GetValue(DashesProperty);
set => SetValue(DashesProperty, value);
}
public double Diameter
{
get => (double)GetValue(DiameterProperty);
set => SetValue(DiameterProperty, value);
}
public double Radius => Diameter / 2;
public double DashHeight
{
get => (double)GetValue(DashHeightProperty);
set => SetValue(DashHeightProperty, value);
}
public double DashWidth
{
get => (double)GetValue(DashWidthProperty);
set => SetValue(DashWidthProperty, value);
}
public Brush DashFill
{
get => (SolidColorBrush)GetValue(DashFillProperty);
set => SetValue(DashFillProperty, value);
}
public Brush DashAccentFill
{
get => (SolidColorBrush)GetValue(DashAccentFillProperty);
set => SetValue(DashAccentFillProperty, value);
}
public int TailSize
{
get => (int)GetValue(TailSizeProperty);
set => SetValue(TailSizeProperty, value);
}
public double AnimationSpeed
{
get => (double)GetValue(AnimationSpeedProperty);
set => SetValue(AnimationSpeedProperty, value);
}
public bool IsPlaying
{
get => (bool)GetValue(IsPlayingProperty);
set => SetValue(IsPlayingProperty, value);
}
#endregion Dependency Properties
private void OnLoaded(object sender, RoutedEventArgs e)
{
var thisControl = sender as StatusRing;
Recreate(thisControl);
}
#region Dependency Property callbacks
private static void DashesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void DiameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void DashHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void DashWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void DashFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void DashAnimationFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void TailSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
Recreate(thisControl);
}
private static void AnimationSpeedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
if (thisControl.IsLoaded)
{
thisControl.glowAnimationStoryBoard.Stop();
thisControl.glowAnimationStoryBoard.Children.Clear();
ApplyAnimations(thisControl);
thisControl.glowAnimationStoryBoard.Begin();
}
}
private static void IsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var thisControl = d as StatusRing;
if (thisControl.IsLoaded)
{
var isPlaying = (bool)e.NewValue;
if (isPlaying)
{
thisControl.glowAnimationStoryBoard.Begin();
}
else
{
thisControl.glowAnimationStoryBoard.Stop();
}
}
}
#endregion Dependency Property callbacks
private static void Recreate(StatusRing thisControl)
{
if (thisControl.IsLoaded)
{
thisControl.glowAnimationStoryBoard.Stop();
thisControl.glowAnimationStoryBoard.Children.Clear();
thisControl.RootCanvas.Children.Clear();
Validate(thisControl);
BuildRing(thisControl);
ApplyAnimations(thisControl);
if (thisControl.IsPlaying)
{
thisControl.glowAnimationStoryBoard.Begin();
}
else
{
thisControl.glowAnimationStoryBoard.Stop();
}
}
}
private static void Validate(StatusRing thisControl)
{
if (thisControl.TailSize > thisControl.Dashes)
{
throw new Exception("TailSize cannot be larger than amount of dashes");
}
}
private static void BuildRing(StatusRing thisControl)
{
var angleStep = (double)360 / thisControl.Dashes;
for (double i = 0; i < 360; i = i + angleStep)
{
var rect = new Rectangle
{
Fill = thisControl.DashFill,
Height = thisControl.DashHeight,
Width = thisControl.DashWidth
};
//Rotate dash to follow circles circumference
var centerY = thisControl.Radius;
var centerX = thisControl.DashWidth / 2;
var rotateTransform = new RotateTransform(i, centerX, centerY);
rect.RenderTransform = rotateTransform;
var offset = thisControl.Radius - thisControl.DashWidth / 2;
rect.SetValue(Canvas.LeftProperty, offset);
thisControl.RootCanvas.Children.Add(rect);
}
thisControl.RootCanvas.Width = thisControl.Diameter;
thisControl.RootCanvas.Height = thisControl.Diameter;
}
private static void ApplyAnimations(StatusRing thisControl)
{
var baseColor = ((SolidColorBrush)thisControl.DashFill).Color;
var animatedColor = ((SolidColorBrush)thisControl.DashAccentFill).Color;
var dashes = thisControl.RootCanvas.Children.OfType<Rectangle>().ToList();
double animationPeriod = thisControl.AnimationSpeed;
double glowDuration = animationPeriod * thisControl.TailSize;
for (int i = 0; i < dashes.Count; i++)
{
var beginTime = TimeSpan.FromMilliseconds(animationPeriod * i);
var colorAnimation = new ColorAnimationUsingKeyFrames
{
BeginTime = beginTime,
RepeatBehavior = RepeatBehavior.Forever
};
var toFillColor = new LinearColorKeyFrame(animatedColor, TimeSpan.Zero);
colorAnimation.KeyFrames.Add(toFillColor);
var dimToBase = new LinearColorKeyFrame(baseColor, TimeSpan.FromMilliseconds(glowDuration));
colorAnimation.KeyFrames.Add(dimToBase);
var restingTime = animationPeriod * dashes.Count;
var delay = new LinearColorKeyFrame(baseColor, TimeSpan.FromMilliseconds(restingTime));
colorAnimation.KeyFrames.Add(delay);
Storyboard.SetTarget(colorAnimation, dashes[i]);
Storyboard.SetTargetProperty(colorAnimation, new PropertyPath("(Fill).(SolidColorBrush.Color)"));
thisControl.glowAnimationStoryBoard.Children.Add(colorAnimation);
}
}
}
StatusRing.xaml:
<UserControl x:Class="WpfPlayground.StatusRing"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Canvas x:Name="RootCanvas" />
Usage:
<local:StatusRing Diameter="250"
Dashes="32"
TailSize="16"
IsPlaying="True" />
Result:
The number of dashes, length and speed of animation, etc... are all configurable. The naming of the dependency properties could be better though...
Enjoy :-)
Upvotes: 2