Search code examples
c#wpfanimationstoryboardrotatetransform

Rotate a Grid using a Storyboard along shortest 'route'


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>

Solution

  • 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