How to make ColorAnimationUsingKeyFrames on own control's Background work from code-behind?

243 views Asked by At

I'm trying to achieve a dynamic, keyframes-based color animation of my own custom ToggleButton's Background property (WPF custom control inheriting a ToggleButton). I need the animation to have its duration and colors (from/to) customizable at runtime, so I have exposed three dependency properties to alter these parameters using binding in XAML. When value of either of them changes, I update the animation to be fired when Blinking (bool DP) is set to true.

I tried following approach:

public class CommandAndActivationButton : ToggleButton
{
    public static readonly DependencyProperty BlinkingProperty = DependencyProperty.Register("Blinking", typeof(bool), typeof(CommandAndActivationButton), new PropertyMetadata(default(bool), OnBlinkingChanged));

    internal static readonly DependencyProperty BlinkingDurationProperty = DependencyProperty.Register("BlinkingDuration", typeof(Duration), typeof(CommandAndActivationButton), new PropertyMetadata(default(Duration), OnAnimationParametersChanged));
    internal static readonly DependencyProperty BlinkAnimationColorBrushProperty = DependencyProperty.Register("BlinkAnimationColorBrush", typeof(SolidColorBrush), typeof(CommandAndActivationButton), new PropertyMetadata(default(SolidColorBrush), OnAnimationParametersChanged));
    internal static readonly DependencyProperty InitialAnimationColorBrushProperty = DependencyProperty.Register("InitialAnimationColorBrush", typeof(SolidColorBrush), typeof(CommandAndActivationButton), new PropertyMetadata(default(SolidColorBrush), OnAnimationParametersChanged));

    private ColorAnimationUsingKeyFrames m_BackgroundAnimation;

    static CommandAndActivationButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CommandAndActivationButton),
            new FrameworkPropertyMetadata(typeof(CommandAndActivationButton)));
    }

    /// <summary>
    /// Whether the button should currently be blinking.
    /// </summary>
    public bool Blinking
    {
        get => (bool)GetValue(BlinkingProperty);
        set => SetValue(BlinkingProperty, value);
    }

    /// <summary>
    /// Duration (period) of the blinking animation.
    /// </summary>
    public Duration BlinkingDuration
    {
        get => (Duration)GetValue(BlinkingDurationProperty);
        set => SetValue(BlinkingProperty, value);
    }

    /// <summary>
    /// Color for use in blinking animation (i.e. the "to" color of the animation).
    /// </summary>
    public SolidColorBrush InitialAnimationColorBrush
    {
        get => (SolidColorBrush)GetValue(InitialAnimationColorBrushProperty);
        set => SetValue(InitialAnimationColorBrushProperty, value);
    }

    /// <summary>
    /// Color for use in blinking animation (i.e. the "to" color of the animation).
    /// </summary>
    public SolidColorBrush BlinkAnimationColorBrush
    {
        get => (SolidColorBrush)GetValue(BlinkAnimationColorBrushProperty);
        set => SetValue(BlinkAnimationColorBrushProperty, value);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        if (!Blinking) { return; }
        StartAnimation();
    }

    private static void OnBlinkingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is CommandAndActivationButton c)) { return; }

        if ((bool)e.NewValue)
        {
            c.StartAnimation();
        }
        else
        {
            c.StopAnimation();
        }
    }

    private static void OnAnimationParametersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is CommandAndActivationButton c)) { return; }

        // Make sure we have enough data to create animation
        if (c.BlinkAnimationColorBrush == null ||
            c.InitialAnimationColorBrush == null ||
            c.BlinkingDuration == default) { return; }

        c.m_BackgroundAnimation = new ColorAnimationUsingKeyFrames
        {
            Duration = c.BlinkingDuration,
            RepeatBehavior = RepeatBehavior.Forever,
            FillBehavior = FillBehavior.Stop,
            AutoReverse = true,
            KeyFrames = new ColorKeyFrameCollection
            {
                new DiscreteColorKeyFrame(c.InitialAnimationColorBrush.Color, KeyTime.FromPercent(0.0)),
                new DiscreteColorKeyFrame(c.BlinkAnimationColorBrush.Color, KeyTime.FromPercent(1.0))
            }
        };
    }

    private void StartAnimation()
    {
        Background.BeginAnimation(SolidColorBrush.ColorProperty, m_BackgroundAnimation);
    }

    private void StopAnimation()
    {
        Background.BeginAnimation(SolidColorBrush.ColorProperty, null);
    }
}

Other than this animation, the button also has a number of data triggers that change its Background when toggled (IsChecked = true), on mouse hover etc., but as far as I know those behaviors are overridden while an animation is running, so I should get the behavior I want... but I don't (although StartAnimation() is called correctly at runtime).

I have already tried to achieve the same using a single animation in pure-XAML but seems to be impossible as all the animations properties in XAML need to be static (otherwise I get a "Cannot freeze this Storyboard timeline tree for use across threads" error connected to animation being a Freezable).

Do you have an idea what am I doing wrong here?

Edit: It does not even run with hard-coded duration and colors, like below (minimum example: setting Blinking to true correctly causes a call to StartAnimation() on the button, but nothing happens with the button's background):

public class CommandAndActivationButton : ToggleButton
{
    public static readonly DependencyProperty BlinkingProperty = DependencyProperty.Register("Blinking", typeof(bool), typeof(CommandAndActivationButton), new PropertyMetadata(default(bool), OnBlinkingChanged));

    static CommandAndActivationButton()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(CommandAndActivationButton),
            new FrameworkPropertyMetadata(typeof(CommandAndActivationButton)));
    }

    /// <summary>
    /// Whether the button should currently be blinking.
    /// </summary>
    public bool Blinking
    {
        get => (bool)GetValue(BlinkingProperty);
        set => SetValue(BlinkingProperty, value);
    }

    private static void OnBlinkingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is CommandAndActivationButton c)) { return; }

        if ((bool)e.NewValue)
        {
            c.StartAnimation();
        }
    }

    private void StartAnimation()
    {
        var animation = new ColorAnimationUsingKeyFrames
        {
            Duration = new Duration(TimeSpan.FromSeconds(1)),
            RepeatBehavior = RepeatBehavior.Forever,
            FillBehavior = FillBehavior.Stop,
            AutoReverse = true,
            KeyFrames = new ColorKeyFrameCollection
            {
                new DiscreteColorKeyFrame(Colors.Blue, KeyTime.FromPercent(0.0)),
                new DiscreteColorKeyFrame(Colors.Yellow, KeyTime.FromPercent(0.5)),
                new DiscreteColorKeyFrame(Colors.Blue, KeyTime.FromPercent(1.0))
            }
        };
        Background.BeginAnimation(SolidColorBrush.ColorProperty, animation);
    }
}

The corresponding ControlTemplate of that button looks like this (simplified):

 <!-- Main button template -->
<Style TargetType="{x:Type local:CommandAndActivationButton}" 
       BasedOn="{StaticResource {x:Type ToggleButton}}">
    <Setter Property="Background">
        <Setter.Value>
            <SolidColorBrush Color="Gray"/>
        </Setter.Value>
    </Setter>
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:CommandAndActivationButton">
                <Border Background="{TemplateBinding Background}">
                    <!-- Stuff (with not set/transparent background) -->
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="BorderBrush" Value="Gray"/>
                        <Setter Property="Opacity" Value="0.7"/>
                    </Trigger>
                <ControlTemplate.Triggers>
            </ControlTemplate>
       <!-- Not important -->
0

There are 0 answers