marko devcic

  • github:
    deva666
  • email:
    madevcic {at} gmail.com

Animating WPF ContentControl Part 1

Posted on 17 June 2014

I'm a big fan of rich UIs, and WPF has excellent support for almost any kind of animation you can think of. So I'm going to show you how easy is it is to add a little style to your WPF app.

We're going to build upon the project from the MVVM navigation series. Now every time the view changes it will be animated, old view is going to slide and fade out and new one is going to slide and fade in. Also we'll have to option of choosing between vertical and horizontal sliding.

Let's get going, we'll add a new class to our old project and call it FadingContentControl and make it extend the WPF ContentControl class.

    class FadingContentControl : ContentControl
    {
        Storyboard fadeOut;
        Storyboard fadeIn;
        Shape paintArea;
        ContentPresenter mainArea;

Next we'll add 4 storyboards to our resource dictionary and change the template of our FadingContentControl.

       <Storyboard x:Key="fadeIn">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <SplineDoubleKeyFrame KeyTime="00:00:01"
                                      Value="1" />
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="150" />
                <EasingDoubleKeyFrame KeyTime="00:00:01"
                                      Value="0">
                    <EasingDoubleKeyFrame.EasingFunction>
                        <CubicEase EasingMode="EaseOut" />
                    </EasingDoubleKeyFrame.EasingFunction>
                </EasingDoubleKeyFrame>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

        <Storyboard x:Key="fadeOut">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="1" />
                <SplineDoubleKeyFrame KeyTime="00:00:00.3"
                                      Value="0" />
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <SplineDoubleKeyFrame KeyTime="00:00:00.3"
                                      Value="300" />
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>


        <Storyboard x:Key="fadeInVertical">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <SplineDoubleKeyFrame KeyTime="00:00:00.7"
                                      Value="1" />
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="100" />
                <EasingDoubleKeyFrame KeyTime="00:00:00.7"
                                      Value="0">
                    <EasingDoubleKeyFrame.EasingFunction>
                        <CubicEase EasingMode="EaseOut" />
                    </EasingDoubleKeyFrame.EasingFunction>
                </EasingDoubleKeyFrame>
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

        <Storyboard x:Key="fadeOutVertical">
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.Opacity)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="1" />
                <SplineDoubleKeyFrame KeyTime="00:00:00.5"
                                      Value="0" />
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
                <SplineDoubleKeyFrame KeyTime="00:00:00"
                                      Value="0" />
                <SplineDoubleKeyFrame KeyTime="00:00:00.5"
                                      Value="200" />
            </DoubleAnimationUsingKeyFrames>
        </Storyboard>

        <Style TargetType="controls:FadingContentControl">
            <Setter Property="HorizontalContentAlignment"
                    Value="Stretch" />
            <Setter Property="VerticalContentAlignment"
                    Value="Stretch" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="controls:FadingContentControl">
                        <Grid x:Name="root"
                              RenderTransformOrigin="0.5,0.5">                        
                            <ContentPresenter Cursor="{TemplateBinding Cursor}"
                                              ContentTemplate="{TemplateBinding ContentTemplate}"
                                              Content="{TemplateBinding Content}"
                                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              Margin="{TemplateBinding Padding}"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                              x:Name="mainArea" />
                            <Rectangle x:Name="paintArea" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

We've added a storyboard for horizontal and vertical sliding in and sliding out, and we've defined our control template for the FadingContentControl.

Note that the template has a standard ContentPresenter that displays the controls content but we've added a Rectangle that will act as buffer for holding the image of old content just before a new one slides in. Otherwise the content changes inside one of the animations and that looks pretty bad. This way can animation length to whatever we like and the user will always see old content go out and new content go in.

Now, let's see the code for doing all the magic.

Find those storyboards we defined as resources ...

        public FadingContentControl()
        {
            InitStoryBoards(FadeVertically);
            this.RenderTransform = new TranslateTransform();
        }

        private void InitStoryBoards(bool fadeVertical)
        {
            Contract.Ensures(_fadeIn != null, "Failed to init fade in storyboard");
            Contract.Ensures(_fadeOut != null, "Failed to init fade out storyboard");
            _fadeIn = fadeVertical ? (Storyboard)FindResource("fadeInVertical") : (Storyboard)FindResource("fadeIn");
            _fadeOut = fadeVertical ? (Storyboard)FindResource("fadeOutVertical") : (Storyboard)FindResource("fadeOut");
        }

... and override the ContentControl's OnContentChanged method ...

        protected override void OnContentChanged(object oldContent, object newContent)
        {
            if (oldContent != null && newContent != null)
            {
                _oldContent.Fill = GetVisualBrush(_mainArea);
                _oldContent.Visibility = System.Windows.Visibility.Visible;
                _mainArea.Visibility = System.Windows.Visibility.Collapsed;
                Storyboard fadeOutClone = _fadeOut.Clone();
                Storyboard fadeInClone = _fadeIn.Clone();
                fadeOutClone.Completed += (s, e) =>
                {
                    fadeInClone.Begin(_mainArea);
                    _oldContent.Visibility = System.Windows.Visibility.Collapsed;
                    _mainArea.Visibility = System.Windows.Visibility.Visible;
                };
                fadeOutClone.Begin(_oldContent);
            }
            base.OnContentChanged(oldContent, newContent);
        }

... and before the content changes we fill the Rectangle defined in the template with the content of the our control, make it visible, hide the main content and start animating the control. After it slides out, the new content has been put to the main area, all we have to do is change the visibility to visible again and hide the rectangle. The idea and part of the code for this smooth transition was adopted from this article, so proper credit to the author.

        private Brush GetVisualBrush(Visual visual)
        {
            Contract.Requires(visual != null, "Visual is null");
            var target = new RenderTargetBitmap((int)this.ActualWidth, (int)this.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            target.Render(visual);
            var brush = new ImageBrush(target);
            brush.Freeze();
            return brush;
        }

And that's it, whenever your content changes, you're going to have a nice slide out/slide in animation. If you want to change it to vertical sliding just change the FadeVertically property to true.

Download the complete Visual studio project here.

Browse the full source code on Bitbucket.

Next part of this series will add some more usability to our custom ContentControl.