I'm developing a 'compass-like' control, which is able to rotate along a value ranging from 0-360 degrees; ie full-circle. By setting a property in a ViewModel, the Grid animates to the new angle. The animation however, does not use the shortest route to the new angle when it, for instance, rotates from 10 degrees to 350. It rotates clockwise for almost the entire circle.
But what I would like to achieve is that it animates along the shortest route: from 10 to 350 should animate counter-clockwise for only 20 degrees.
Below I've added a made up example of my code, but instead of binding to a property in a ViewModel it uses binding to a ListBox (copy-paste should get it running). Selecting an item in the ListBox rotates the Grid (which contains the 'needle').
If possible, I would prefer an "all XAML" solution. Is it at all doable, what I'm trying to achieve?
<Window x:Class="CompassRotation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="375" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<Grid>
<Ellipse Width="300" Height="300" Fill="DarkCyan" />
<Label Content="{Binding SelectedItem.Content, ElementName=Angle, UpdateSourceTrigger=Explicit}"
FontSize="36" />
<Grid Width="300" Height="300"
RenderTransformOrigin="0.5,0.5"
Tag="{Binding SelectedItem.Content, ElementName=Angle, UpdateSourceTrigger=PropertyChanged,NotifyOnTargetUpdated=True}">
<Grid.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Storyboard.TargetProperty="(Grid.RenderTransform).(RotateTransform.Angle)"
Duration="0:0:1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<Grid.RenderTransform>
<RotateTransform Angle="{Binding SelectedItem.Content, ElementName=Angle, FallbackValue=45}" />
</Grid.RenderTransform>
<Rectangle Width="15" Height="300">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStopCollection>
<GradientStop Color="Red" Offset="0" />
<GradientStop Color="Green" Offset="1" />
</GradientStopCollection>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
</Grid>
<ListBox Grid.Column="1" Name="Angle">
<ListBoxItem>0</ListBoxItem>
<ListBoxItem>45</ListBoxItem>
<ListBoxItem>90</ListBoxItem>
<ListBoxItem>135</ListBoxItem>
<ListBoxItem>180</ListBoxItem>
<ListBoxItem>225</ListBoxItem>
<ListBoxItem>270</ListBoxItem>
<ListBoxItem>305</ListBoxItem>
<ListBoxItem>360</ListBoxItem>
</ListBox>
</Grid>
</Window>
A DoubleAnimation
has a From
property that you can bind to to set the animation's starting value.
So before updating the ViewModel's Angle
value from 10 to 350 counterclockwise, set the ViewModel's From
value to 370 and the animation will scroll counterclockwise from the 370 (=10) degrees mark back to 350.
That leaves the question of how to determine the From
value to set if you're going to update the Angle
from value from
to to
.
One thing we know is that the From
property to set should be in the interval [to - 180, to + 180]
, to make sure that the rotation takes the shortest route to to
.
If we call the lower bound of this interval lowerbound = to - 180
, then
From = lowerbound + (from - lowerbound) % 360
Or actually, since %
behaves oddly for negative values, a safer formula that works for from
and to
angles in the range [-360,360]
is
From = lowerbound + (from - lowerbound + 720) % 360