本文将介绍一个好玩但实际作用可能不太大的动画效果:跳动的字符。为了提高动画效果的可重用性以及调用的灵活性,通过Behavior实现跳动的字符动画。先看下效果:
技术要点与实现
通过
TextEffect
的
PositionStart
和
PositionCount
属性控制应用动画效果的子字符串的起始位置以及长度,同时使用
TranslateTransform
设置字符纵坐标的移动变换,以实现跳动的效果。主要步骤如下:
-
在OnAttached方法中,注册
Loaded
事件,在Load
事件中为TextBlock
添加TextEffect
效果,其中PositionCount
设置为1,每次只跳动一个字符。 -
添加启动动画效果的
BeginEffect
方法,并创建控制子字符纵向移动变换的线性动画。然后根据字符串(剔除空字符)的长度n,创建n个关键帧,每个关键帧中把PositionStart
设置为要跳动的字符在字符串中的索引 -
在开启动画属性
IsEnabled=true
和TextBlock
内容变化时,启动动画效果
在创建关键帧设置跳动字符位置时剔除了空字符,是为了是动画效果显得连贯
public class DanceCharEffectBehavior : Behavior<TextBlock>
private TextEffect _textEffect;
private string _textEffectName;
private TranslateTransform _translateTransform = null;
private string _translateTransformName;
private Storyboard _storyboard;
protected override void OnAttached()
base.OnAttached();
this.AssociatedObject.Loaded += AssociatedObject_Loaded;
this.AssociatedObject.Unloaded += AssociatedObject_Unloaded;
this.AssociatedObject.IsVisibleChanged += AssociatedObject_IsVisibleChanged;
BindingOperations.SetBinding(this, DanceCharEffectBehavior.InternalTextProperty, new Binding("Text") { Source = this.AssociatedObject });
protected override void OnDetaching()
base.OnDetaching();
this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
this.AssociatedObject.Unloaded -= AssociatedObject_Unloaded;
this.AssociatedObject.IsVisibleChanged -= AssociatedObject_IsVisibleChanged;
this.ClearValue(DanceCharEffectBehavior.InternalTextProperty);
if (_storyboard != null)
_storyboard.Remove(this.AssociatedObject);
_storyboard.Children.Clear();
if (_textEffect != null)
this.AssociatedObject.TextEffects.Remove(_textEffect);
private void AssociatedObject_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
if ((bool)e.NewValue == false)
if (_storyboard != null)
_storyboard.Stop(this.AssociatedObject);
BeginEffect(this.AssociatedObject.Text);
private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
if (_textEffect == null)
this.AssociatedObject.TextEffects.Add(_textEffect = new TextEffect()
PositionCount = 1,
Transform = _translateTransform = new TranslateTransform(),
NameScope.SetNameScope(this.AssociatedObject, new NameScope());
this.AssociatedObject.RegisterName(_textEffectName = "n" + Guid.NewGuid().ToString("N"), _textEffect);
this.AssociatedObject.RegisterName(_translateTransformName = "n" + Guid.NewGuid().ToString("N"), _translateTransform);
if (IsEnabled)
BeginEffect(this.AssociatedObject.Text);
private void AssociatedObject_Unloaded(object sender, RoutedEventArgs e)
StopEffect();
private void SetEffect(string text)
if (string.IsNullOrEmpty(text) || this.AssociatedObject.IsLoaded == false)
StopEffect();
return;
BeginEffect(text);
private void StopEffect()
if (_storyboard != null)
_storyboard.Stop(this.AssociatedObject);
private void BeginEffect(string text)
StopEffect();
int textLength = text.Length;
if (textLength < 1 || _translateTransformName == null || IsEnabled == false) return;
if (_storyboard == null)
_storyboard = new Storyboard();
double duration = 0.5d;
DoubleAnimation da = new DoubleAnimation();
Storyboard.SetTargetName(da, _translateTransformName);
Storyboard.SetTargetProperty(da, new PropertyPath(TranslateTransform.YProperty));
da.From = 0d;
da.To = 10d;
da.Duration = TimeSpan.FromSeconds(duration / 2d);
da.RepeatBehavior = RepeatBehavior.Forever;
da.AutoReverse = true;
char emptyChar = ' ';
List<int> lsb = new List<int>();
for (int i = 0; i < textLength; ++i)
if (text[i] != emptyChar)
lsb.Add(i);
Int32AnimationUsingKeyFrames frames = new Int32AnimationUsingKeyFrames();
Storyboard.SetTargetName(frames, _textEffectName);
Storyboard.SetTargetProperty(frames, new PropertyPath(TextEffect.PositionStartProperty));
frames.Duration = TimeSpan.FromSeconds((lsb.Count) * duration);
frames.RepeatBehavior = RepeatBehavior.Forever;
frames.AutoReverse = true;
int ii = 0;
foreach (int index in lsb)
frames.KeyFrames.Add(new DiscreteInt32KeyFrame()
Value = index,
KeyTime = TimeSpan.FromSeconds(ii * duration),
++ii;
_storyboard.Children.Add(da);
_storyboard.Children.Add(frames);
_storyboard.Begin(this.AssociatedObject, true);
private string InternalText
get { return (string)GetValue(InternalTextProperty); }
set { SetValue(InternalTextProperty, value); }
private static readonly DependencyProperty InternalTextProperty =
DependencyProperty.Register("InternalText", typeof(string), typeof(DanceCharEffectBehavior),
new PropertyMetadata(OnInternalTextChanged));
private static void OnInternalTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
var source = d as DanceCharEffectBehavior;
if (source._storyboard != null)
source._storyboard.Stop(source.AssociatedObject);
source._storyboard.Children.Clear();
source.SetEffect(e.NewValue == null ? string.Empty : e.NewValue.ToString());
public bool IsEnabled
get { return (bool)GetValue(IsEnabledProperty); }
set { SetValue(IsEnabledProperty, value); }
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.Register("IsEnabled", typeof(bool), typeof(DanceCharEffectBehavior), new PropertyMetadata(true, (d, e) =>
bool b = (bool)e.NewValue;
var source = d as DanceCharEffectBehavior;
source.SetEffect(source.InternalText);