How to animate an Image in a button to shake every

2020-03-31 07:21发布

问题:

I not good when it comes to dealing with anything with styles and animations.

I was hoping to be able to get some help on making an Image that is the only content of a Button shake every 30 seconds when ever the buttons Visibility is set to Visibility.Visible.

It is to get the users attention to encourage them to click the button.

I would prefer to do this as an attached behavior on Image, or if possible even UIControl, to make it easily reusable instead of messing with the style, as I am already using a style from my control vendor, and I don't want to edit it.

Here is the solution I used derived from the marked answer

This is the Attached Behavior that can be applied to any System.Windows.Controls.Image.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;

namespace SampleShakeBehavior
{
    public class ShakeBehavior : Behavior<Image>
    {
        private const double DefaultRepeatInterval = 10.0;
        private const double DefaultSpeedRatio = 1.0;

        private const string RepeatIntervalName = "RepeatInterval";
        private const string SpeedRatioName = "SpeedRatio";

        public static readonly DependencyProperty RepeatIntervalProperty =
            DependencyProperty.Register(RepeatIntervalName,
                                        typeof(double),
                                        typeof(ShakeBehavior),
                                        new PropertyMetadata(DefaultRepeatInterval));

        public static readonly DependencyProperty SpeedRatioProperty =
            DependencyProperty.Register(SpeedRatioName,
                                        typeof(double),
                                        typeof(ShakeBehavior),
                                        new PropertyMetadata(DefaultSpeedRatio));

        /// <summary>
        /// Gets or sets the time interval in in seconds between each shake.
        /// </summary>
        /// <value>
        /// The time interval in in seconds between each shake.
        /// </value>
        /// <remarks>
        /// If interval is less than total shake time, then it will shake
        /// constantly without pause. If this is your intention, simply set
        /// interval to 0.
        /// </remarks>
        public double RepeatInterval
        {
            get { return (double)GetValue(RepeatIntervalProperty); }
            set { SetValue(RepeatIntervalProperty, value); }
        }

        /// <summary>
        /// Gets or sets the ratio at which time progresses on the Shakes
        /// Timeline, relative to its parent. 
        /// </summary>
        /// <value> 
        /// The ratio at which time progresses on the Shakes Timeline, relative 
        /// to its parent.
        /// </value>
        /// <remarks> 
        /// If Acceleration or Deceleration are specified, this ratio is the
        /// average ratio over the natural length of the Shake's Timeline. This 
        /// property has a default value of 1.0. If set to zero or less it
        /// will be reset back to th default value.
        /// </remarks>
        public double SpeedRatio
        {
            get { return (double)GetValue(SpeedRatioProperty); }
            set { SetValue(SpeedRatioProperty, value); }
        }

        private Style _orignalStyle;
        protected override void OnAttached()
        {
            _orignalStyle = AssociatedObject.Style;
            AssociatedObject.Style = CreateShakeStyle();
        }

        protected override void  OnDetaching()
        {
            AssociatedObject.Style = _orignalStyle;
        }

        private Style CreateShakeStyle()
        {
            Style newStyle = new Style(AssociatedObject.GetType(), AssociatedObject.Style);
            /**
             * The following will replace/override any existing RenderTransform
             * and RenderTransformOrigin properties on the FrameworkElement
             * once the the new Style is applied to it.
             */
            newStyle.Setters.Add(new Setter(UIElement.RenderTransformProperty, new RotateTransform(0)));
            newStyle.Setters.Add(new Setter(UIElement.RenderTransformOriginProperty, new Point(0.5, 0.5)));

            newStyle.Triggers.Add(CreateTrigger());

            return newStyle;
        }

        private DataTrigger CreateTrigger()
        {
            DataTrigger trigger = new DataTrigger
            {
                Binding = new Binding
                {
                    RelativeSource = new RelativeSource
                    {
                        Mode = RelativeSourceMode.FindAncestor,
                        AncestorType = typeof(UIElement)
                    },
                    Path = new PropertyPath(UIElement.IsVisibleProperty)
                },
                Value = true,
            };

            trigger.EnterActions.Add(new BeginStoryboard { Storyboard = CreateStoryboard() });

            return trigger;
        }

        private Storyboard CreateStoryboard()
        {
            double speedRatio = SpeedRatio;

            // Must be greater than zero
            if (speedRatio <= 0.0)
                SpeedRatio = DefaultSpeedRatio;

            Storyboard storyboard = new Storyboard 
            {
                RepeatBehavior = RepeatBehavior.Forever,
                SpeedRatio = speedRatio
            };

            storyboard.Children.Add(CreateAnimationTimeline());

            return storyboard;
        }

        private Timeline CreateAnimationTimeline()
        {
            DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();

            animation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("(0).(1)", UIElement.RenderTransformProperty, RotateTransform.AngleProperty));

            int keyFrameCount = 8;
            double timeOffsetInSeconds = 0.25;
            double totalAnimationLength = keyFrameCount * timeOffsetInSeconds;
            double repeatInterval = RepeatInterval;

            // Can't be less than zero and pointless to be less than total length
            if (repeatInterval < totalAnimationLength)
                repeatInterval = totalAnimationLength;

            animation.Duration = new Duration(TimeSpan.FromSeconds(repeatInterval));

            int targetValue = 12;
            for (int i = 0; i < keyFrameCount; i++)
                animation.KeyFrames.Add(new LinearDoubleKeyFrame(i % 2 == 0 ? targetValue : -targetValue, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(i * timeOffsetInSeconds))));

            animation.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(totalAnimationLength))));
            return animation;
        }
    }
}

Here is how to use it in Xaml.

<Button>
    <Image Source="myImage.png">
        <i:Interaction.Behaviors>
            <local:ShakeBehavior RepeatInterval="30" SpeedRatio="3.0"/>
        </i:Interaction.Behaviors>
    </Image>
</Button>

For a clear definition of an Attached Behavior you can look at the System.Windows.Interactivity.Behavior class remarks. Behaviors can optionally have Attached Properties with them as well make them very useful.

For a clear definition of an Attached Property you can read the Attached Properties Overview from MSDN. Attached properties can do anything, and they can be thought of as attached behaviors because they can trigger an action to occur causing an effective behavior, however technically they are still just an attached property.

Since an Attached Property can act like a behavior people have come to also call those types of Attached Properties an Attached Behavior, when in fact it is not really an Attached Behavior unless you derive from Behavior and at it to the attached property Interaction.Behaviors collection.

Blend is not required for any Attached Behavior or Attached Property, as with most things in WPF/Silverlight.

回答1:

Here it is an attached behaviour. Just be careful as it may be destructive if your control has existing render transforms

public class Wibble
{
    public static bool GetWobble(DependencyObject obj)
    {
        return (bool)obj.GetValue(WobbleProperty);
    }

    public static void SetWobble(DependencyObject obj, bool value)
    {
        obj.SetValue(WobbleProperty, value);
    }

    public static readonly DependencyProperty WobbleProperty = DependencyProperty.RegisterAttached("Wobble", typeof(bool), typeof(Wibble), new UIPropertyMetadata(false, new PropertyChangedCallback(OnWobbleChanged)));

    private static void OnWobbleChanged(object sender, DependencyPropertyChangedEventArgs args)
    {
        var image = sender as Image;

        if (image == null)
            throw new InvalidOperationException("only images can wobble!");

        // don't really need this check (the find ancestor binding would still find the button), but the spec said the image should be the only child of the button
        var button = LogicalTreeHelper.GetParent(image) as Button;
        if (button == null)
            throw new InvalidOperationException("only images that are the only child of a button can wobble!");

        var previousStyle = image.Style;

        var newStyle = new Style(image.GetType(), previousStyle);

        // this will override any existing render transform + origin on the button, hope they didn't already have one (and I'm too lazy to check)
        newStyle.Setters.Add(new Setter(Image.RenderTransformProperty, new RotateTransform(0)));
        newStyle.Setters.Add(new Setter(Image.RenderTransformOriginProperty, new Point(0.5, 0.5)));

        var trigger = new DataTrigger();

        var binding = new Binding();

        var relativeSource = new RelativeSource();
        relativeSource.Mode = RelativeSourceMode.FindAncestor;
        relativeSource.AncestorType = typeof(Button);

        binding.RelativeSource = relativeSource;
        binding.Path = new PropertyPath(Button.VisibilityProperty);

        trigger.Binding = binding;
        trigger.Value = Visibility.Visible;

        var storyboard = new Storyboard();

        var animation = new DoubleAnimationUsingKeyFrames();
        animation.SetValue(Storyboard.TargetPropertyProperty, new PropertyPath("(0).(1)", Image.RenderTransformProperty, RotateTransform.AngleProperty));
        animation.Duration = new Duration(TimeSpan.FromSeconds(5)); // spec said 30, but i wanted to actually see it happen!
        animation.KeyFrames.Add(new LinearDoubleKeyFrame(-12, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.2))));
        animation.KeyFrames.Add(new LinearDoubleKeyFrame(12, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.4))));
        animation.KeyFrames.Add(new LinearDoubleKeyFrame(0, KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0.5))));

        storyboard.Children.Add(animation);
        storyboard.RepeatBehavior = RepeatBehavior.Forever;

        var beginStoryboard = new BeginStoryboard();
        beginStoryboard.Storyboard = storyboard;
        beginStoryboard.Name = "its_wobble_time"; // it is

        trigger.EnterActions.Add(beginStoryboard);

        var removeStoryboard = new RemoveStoryboard();
        removeStoryboard.BeginStoryboardName = beginStoryboard.Name;

        trigger.ExitActions.Add(removeStoryboard);

        newStyle.Triggers.Add(trigger);

        image.Style = newStyle;
    }
}

here is how it would be used:

<Button Width="100" Height="25" >
        <Image Source="Untitled.png" xmlns:local="clr-namespace:WpfApplication17" local:Wibble.Wobble="True" />
    </Button>


回答2:

Create a WPF custom control by adding a new item in VS and then navigating to the WPF templates. This will allow you to select "Custom Control (WPF)". Name it "ShakyImageControl". This will create a Themes folder with a generic.xaml in it and a "ShakyImageControl.cs" class file. In the generic.xaml replace the existing style with the following:

<Style TargetType="{x:Type local:ShakyImageControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ShakyImageControl}">
                <Image x:Name="image" Source="{TemplateBinding ImageSource}" RenderTransformOrigin="0.5,0.5">
                    <Image.RenderTransform>
                        <TransformGroup>
                            <RotateTransform x:Name="Rotaty"/>
                        </TransformGroup>
                    </Image.RenderTransform>       
                </Image>
                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}, Path=Visibility}" Value="Visible">
                        <DataTrigger.EnterActions>
                            <BeginStoryboard Name="fred">
                                <Storyboard AutoReverse="False" RepeatBehavior="Forever" Duration="0:0:30" Storyboard.TargetName="Rotaty" Storyboard.TargetProperty="Angle">
                                    <DoubleAnimationUsingKeyFrames>
                                        <EasingDoubleKeyFrame KeyTime="0:0:0.2" Value="-12.0"/>
                                        <EasingDoubleKeyFrame KeyTime="0:0:0.4" Value="12.0"/>
                                        <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="0"/>
                                    </DoubleAnimationUsingKeyFrames>
                                </Storyboard>
                            </BeginStoryboard>
                        </DataTrigger.EnterActions>
                        <DataTrigger.ExitActions>
                            <StopStoryboard BeginStoryboardName="fred"/>
                        </DataTrigger.ExitActions>
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

In the ShakyImageControl class add a dependency property as follows:

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

    public ImageSource ImageSource
    {
        get { return (ImageSource)GetValue(ImageSourceProperty); }
        set { SetValue(ImageSourceProperty, value); }
    }

    // Using a DependencyProperty as the backing store for ImageSource.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ImageSourceProperty =
        DependencyProperty.Register("ImageSource", typeof(ImageSource), typeof(ShakyImageControl), new UIPropertyMetadata(null));

To use the shakyImage in a button just do:

<Button Height="50" Width="500" Name="showy" Visibility="Collapsed"> 
        <local:ShakyImageControl ImageSource="\Expand.png"/>
    </Button>

local is an xml namespace like "xmlns:local="clr-namespace:WpfApplication6"

NB: your custom control can be in a seperate assembly if you want