TransitionControl

|

Douglas Stockwell has blogged about how he implements transitions in WPF, actually, I really like the animation effects he creates, but I think his implementation is bit cumbersome, so I finally wind up creating my own transition by defining a custom control aka TransitionControl,  here comes the code for this control:

[TemplatePartAttribute(Name = "PART_ContentHost", Type = typeof(ContentPresenter))]
[TemplatePartAttribute(Name = "PART_StaleContentHost", Type = typeof(ContentPresenter))]
public class TransitionControl : ContentControl
{
    private static readonly DependencyPropertyKey IsContentChangedPropertyKey;
    public static readonly DependencyProperty StaleContentProperty;
    public static readonly DependencyProperty IsContentChangedProperty;
    public static readonly DependencyProperty TransitionEffectProperty;

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

        TransitionEffectProperty = DependencyProperty.Register(
           "TransitionEffect",
           typeof(TransitionEffects),
           typeof(TransitionControl),
           new FrameworkPropertyMetadata(TransitionEffects.None));

        StaleContentProperty = DependencyProperty.Register(
            "StaleContent",
            typeof(Object),
            typeof(TransitionControl),
            new FrameworkPropertyMetadata(null));

        ContentControl.ContentProperty.OverrideMetadata(
            typeof(TransitionControl),
            new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnContentChanged)));

        IsContentChangedPropertyKey = DependencyProperty.RegisterReadOnly(
           "IsContentChanged",
           typeof(Boolean),
           typeof(TransitionControl),
           new FrameworkPropertyMetadata(BooleanBoxes.FalseBox));

        IsContentChangedProperty = IsContentChangedPropertyKey.DependencyProperty;
    }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public Boolean IsContentChanged
    {
        get { return (Boolean) GetValue(IsContentChangedProperty); }
    }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public Object StaleContent
    {
        get { return base.GetValue(StaleContentProperty); }
        set { base.SetValue(StaleContentProperty, value); }
    }

    public TransitionEffects TransitionEffect
    {
        get { return (TransitionEffects) base.GetValue(TransitionEffectProperty); }
        set { base.SetValue(TransitionEffectProperty, value); }
    }

    private static void OnContentChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
    {
        TransitionControl tc = element as TransitionControl;
        tc.SetValue(IsContentChangedPropertyKey, BooleanBoxes.FalseBox);
        tc.StaleContent = e.OldValue;
        if (e.OldValue != null && e.NewValue != null)
        {
            if (!tc.IsLoaded)
            {
                tc.Loaded += delegate(Object sender, RoutedEventArgs args)
                {
                    tc.SetValue(IsContentChangedPropertyKey, BooleanBoxes.TrueBox);
                };
            }
            else
            {
                tc.SetValue(IsContentChangedPropertyKey, BooleanBoxes.TrueBox);
            }
        }
    }
}

The basic idea is to override the ContentProperty's default metadata by adding a PropertyChangedCallback to track the change of ContentProperty, inside the callback, I assign the old value of the ContentProperty to a custom DP I create (I've no idea of how to come up with a sexy name for this DP, so I just call it StaleContent), and I also toggle the value of IsContentChanged read-only DP to notify that the content of this control is changed, so this is the plumbing I create for my TransitionControl, and the tedious part is over, next question is how to apply transition effects, that's the job of control template, I define different control templates for different transition effects, take waving transition for example, the definition of the control template is as follows:

<!--Waving Transition Template-->
<
ControlTemplate TargetType="{x:Type cc:TransitionControl}" x:Key="WavingTemplate">
    <
Grid>
      <
ContentPresenter
         Name="PART_ContentHost"
         HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
         VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
        
ContentTemplate="{TemplateBinding ContentTemplate}"
         Content="{TemplateBinding Content}" />
      <
ContentPresenter
         Name="PART_StaleContentHost"
        
IsHitTestVisible="False"
         HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
        
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
         ContentTemplate="{TemplateBinding ContentTemplate}"
         Content="{TemplateBinding StaleContent}">
        <
ContentPresenter.OpacityMask>
          <
RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="1" RadiusY="1">
            <
RadialGradientBrush.GradientStops>
              <
GradientStop Offset="0" Color="#00000000"/>
              <
GradientStop Offset="0" Color="#FF000000"/>
            </
RadialGradientBrush.GradientStops>
          </
RadialGradientBrush>
        </
ContentPresenter.OpacityMask>
      </
ContentPresenter>
    </
Grid>
    <
ControlTemplate.Triggers>
      <
Trigger Property="IsContentChanged" Value="True">
        <
Trigger.EnterActions>
          <
BeginStoryboard>
            <
Storyboard>
              <
DoubleAnimation
                
From="0"
                
To="1"
                
Duration="00:00:0.7"
                
Storyboard.TargetProperty="OpacityMask.(GradientBrush.GradientStops)[0].Offset"
                
Storyboard.TargetName="PART_StaleContentHost"/>
              <
DoubleAnimation
                
From="0"
                
To="1"
                
Duration="00:00:0.7"
                
Storyboard.TargetProperty="OpacityMask.(GradientBrush.GradientStops)[1].Offset"
                
Storyboard.TargetName="PART_StaleContentHost"/>
            </
Storyboard>
          </
BeginStoryboard>
        </
Trigger.EnterActions>
      </
Trigger>
    </
ControlTemplate.Triggers>
</
ControlTemplate>

You can see that I create two ContentPresenters, one for the old content(StaleContentProperty), and one for the new content(ContentProperty), the PART_StaleContentHost ContentPresenter overlapps the PART_ContentHost ContentPresenter, so to achieve transition effects here, I simply "erase" the PART_StaleContentHost to let the PART_ContentHost show through, so here I use Trigger the track the IsContentChanged property, and apply appropriate animation to the PART_StaleContentHost accordingly.

You can see my implementation of transitions is pretty streightforward, and much more clear, and a bit more performant than Douglas Stockwell's approach.

I've upload the control to Channel9's Sandbox, so you can download the source from here.

1 comments:

Michael H said...

Thank you very much for this... very well written article -- clear and helpful. Now if there was a way to simply play a storyboard to animate from an old value to the new value (for instance, make an textblock and its ContentTemplate fade out uniformly when its Text field goes to null).