6

I am trying to animate a StackPanel when its visibility changed to grow from a width of 0 to its automatic width, here is what I have at the moment:

<Trigger Property="Visibility" Value="Visible">
    <Setter Property="Width" Value="0"></Setter>
    <Trigger.EnterActions>
        <BeginStoryboard>
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Width" Duration="0:0:1">
                    <DiscreteObjectKeyFrame KeyTime="0">
                        <DiscreteObjectKeyFrame.Value>
                            <System:Double>NaN</System:Double>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </Trigger.EnterActions>
</Trigger>

Can someone explain how I might achieve this animation? Is it maybe not possible in the way I am trying to do it?

Thanks, alex.

daniele3004
  • 13,072
  • 12
  • 67
  • 75
Alex Hope O'Connor
  • 9,354
  • 22
  • 69
  • 112
  • 1
    I have produced some sample code by animating ScaleTransform from 0 to 1 as a double animation, however I don't seem to have control over which direction it animates from. Would you like me to post the sample code as an answer? – learningcs Dec 04 '14 at 23:10
  • 1
    @rshepp if you went to all that trouble, you may as well post even if it half-works. – McGarnagle Dec 04 '14 at 23:28
  • Yeah man, post it, the only other thing I found close to working was using a LayoutTransform but I didn't understand it well enough to make it suit my needs. – Alex Hope O'Connor Dec 04 '14 at 23:49
  • 1
    It wasn't really any trouble, I quite often try to emulate and solve problems because I find it's the best way to learn – learningcs Dec 04 '14 at 23:54

3 Answers3

11

Here is a quick mockup project I threw together.

In the Window's Loaded event, I simply set the stackpanel's visibility to Visible and it expands to fit its container width from left to right... Hopefully that's suits your needs.

Some things to note:

  • You must predefine the scale transform, else the animation will not play.
  • If you omit To in an animation, it will animate back to the default value.

And here is the code:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="600" Loaded="Window_Loaded">
    <Border HorizontalAlignment="Center" Width="300" Background="Gainsboro">
        <Border.Resources>
            <Style TargetType="StackPanel" x:Key="expand">
                <Setter Property="RenderTransform">
                    <Setter.Value>
                        <ScaleTransform ScaleX="1"/>
                    </Setter.Value>
                </Setter>
                <Style.Triggers>
                    <Trigger Property="Visibility" Value="Visible">
                        <Trigger.EnterActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="RenderTransform.ScaleX"
                                                     From="0"
                                                     Duration="0:00:01"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.EnterActions>
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Border.Resources>

        <StackPanel x:Name="stackpanel" Background="Gray" Visibility="Collapsed" Style="{StaticResource expand}"/>

    </Border>
</Window>
learningcs
  • 1,856
  • 16
  • 25
  • Works well enough! Would rather animate the width directly but the scaling animation does not look to shabby either. Thanks! – Alex Hope O'Connor Dec 05 '14 at 00:00
  • 1
    This works very well for animating when `Visibility` is changed to "Visible" - how would I add a "reverse" animation for when `Visibility` is "Collapsed" ? – Scott Baker Jun 10 '15 at 21:18
  • 1
    I saw in your comment above that you couldn't control the direction of the animation; all you have to do is set the `RenderTransformOrigin` to `1,0` to animate from the other side or `0.5,0` to animate from the center. – Scott Baker Jun 10 '15 at 21:20
  • 1
    @ScottSEA Good info regarding the origin, the less-experienced me didn't consider it at the time :P Using the visibility property probably isn't the best way to trigger this animation if you intend it to reverse. Collapsing the control hides it immediately, so you won't see any animation -- perhaps you could use the "Tag" property as the trigger for both directions. For example, setting the tag to 1 will immediately set the controls to visible and then play the animation. Then, setting the tag to 0 will play the reverse animation and then collapse the control after the animation has finished. – learningcs Jun 10 '15 at 23:06
  • This is cool but how could I rather slide it in from the left and slide it out again without the width compression/decompression effect ? – Duncan Groenewald Apr 11 '22 at 09:24
2

So, this is quite an old question, but I think it is a common enough scenario that you need to animate Width or Height from 0 to Auto (or similar) to justify an additional answer. I am not going to focus on the Alex's exact requirements here, so as to emphasize the general nature of my proposed solution.

Which is: writing your own Clipper control that would clip it's child's visible Width and Height to some fraction of them. Then we could animate those Fraction properties (0 -> 1) to achieve the desired effect. The code for Clipper is below, with all the helpers included.

public sealed class Clipper : Decorator
{
    public static readonly DependencyProperty WidthFractionProperty = DependencyProperty.RegisterAttached("WidthFraction", typeof(double), typeof(Clipper), new PropertyMetadata(1d, OnClippingInvalidated), IsFraction);
    public static readonly DependencyProperty HeightFractionProperty = DependencyProperty.RegisterAttached("HeightFraction", typeof(double), typeof(Clipper), new PropertyMetadata(1d, OnClippingInvalidated), IsFraction);
    public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register("Background", typeof(Brush), typeof(Clipper), new FrameworkPropertyMetadata(Brushes.Transparent, FrameworkPropertyMetadataOptions.AffectsRender));
    public static readonly DependencyProperty ConstraintProperty = DependencyProperty.Register("Constraint", typeof(ConstraintSource), typeof(Clipper), new PropertyMetadata(ConstraintSource.WidthAndHeight, OnClippingInvalidated), IsValidConstraintSource);

    private Size _childSize;
    private DependencyPropertySubscriber _childVerticalAlignmentSubcriber;
    private DependencyPropertySubscriber _childHorizontalAlignmentSubscriber;

    public Clipper()
    {
        ClipToBounds = true;
    }

    public Brush Background
    {
        get { return (Brush)GetValue(BackgroundProperty); }
        set { SetValue(BackgroundProperty, value); }
    }

    public ConstraintSource Constraint
    {
        get { return (ConstraintSource)GetValue(ConstraintProperty); }
        set { SetValue(ConstraintProperty, value); }
    }

    [AttachedPropertyBrowsableForChildren]
    public static double GetWidthFraction(DependencyObject obj)
    {
        return (double)obj.GetValue(WidthFractionProperty);
    }

    public static void SetWidthFraction(DependencyObject obj, double value)
    {
        obj.SetValue(WidthFractionProperty, value);
    }

    [AttachedPropertyBrowsableForChildren]
    public static double GetHeightFraction(DependencyObject obj)
    {
        return (double)obj.GetValue(HeightFractionProperty);
    }

    public static void SetHeightFraction(DependencyObject obj, double value)
    {
        obj.SetValue(HeightFractionProperty, value);
    }

    protected override Size MeasureOverride(Size constraint)
    {
        if (Child is null)
        {
            return Size.Empty;
        }

        switch (Constraint)
        {
            case ConstraintSource.WidthAndHeight:
                Child.Measure(constraint);
                break;

            case ConstraintSource.Width:
                Child.Measure(new Size(constraint.Width, double.PositiveInfinity));
                break;

            case ConstraintSource.Height:
                Child.Measure(new Size(double.PositiveInfinity, constraint.Height));
                break;

            case ConstraintSource.Nothing:
                Child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
                break;
        }

        var finalSize = Child.DesiredSize;
        if (Child is FrameworkElement childElement)
        {
            if (childElement.HorizontalAlignment == HorizontalAlignment.Stretch && constraint.Width > finalSize.Width && !double.IsInfinity(constraint.Width))
            {
                finalSize.Width = constraint.Width;
            }

            if (childElement.VerticalAlignment == VerticalAlignment.Stretch && constraint.Height > finalSize.Height && !double.IsInfinity(constraint.Height))
            {
                finalSize.Height = constraint.Height;
            }
        }

        _childSize = finalSize;

        finalSize.Width *= GetWidthFraction(Child);
        finalSize.Height *= GetHeightFraction(Child);

        return finalSize;
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        if (Child is null)
        {
            return Size.Empty;
        }

        var childSize = _childSize;
        var clipperSize = new Size(Math.Min(arrangeSize.Width, childSize.Width * GetWidthFraction(Child)),
                                   Math.Min(arrangeSize.Height, childSize.Height * GetHeightFraction(Child)));
        var offsetX = 0d;
        var offsetY = 0d;

        if (Child is FrameworkElement childElement)
        {
            if (childSize.Width > clipperSize.Width)
            {
                switch (childElement.HorizontalAlignment)
                {
                    case HorizontalAlignment.Right:
                        offsetX = -(childSize.Width - clipperSize.Width);
                        break;

                    case HorizontalAlignment.Center:
                        offsetX = -(childSize.Width - clipperSize.Width) / 2;
                        break;
                }
            }

            if (childSize.Height > clipperSize.Height)
            {
                switch (childElement.VerticalAlignment)
                {
                    case VerticalAlignment.Bottom:
                        offsetY = -(childSize.Height - clipperSize.Height);
                        break;

                    case VerticalAlignment.Center:
                        offsetY = -(childSize.Height - clipperSize.Height) / 2;
                        break;
                }
            }
        }

        Child.Arrange(new Rect(new Point(offsetX, offsetY), childSize));

        return clipperSize;
    }

    protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
    {
        void UpdateLayout(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue.Equals(HorizontalAlignment.Stretch) || e.NewValue.Equals(VerticalAlignment.Stretch))
            {
                InvalidateMeasure();
            }
            else
            {
                InvalidateArrange();
            }
        }

        _childHorizontalAlignmentSubscriber?.Unsubscribe();
        _childVerticalAlignmentSubcriber?.Unsubscribe();

        if (visualAdded is FrameworkElement childElement)
        {
            _childHorizontalAlignmentSubscriber = new DependencyPropertySubscriber(childElement, HorizontalAlignmentProperty, UpdateLayout);
            _childVerticalAlignmentSubcriber = new DependencyPropertySubscriber(childElement, VerticalAlignmentProperty, UpdateLayout);
        }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        drawingContext.DrawRectangle(Background, null, new Rect(RenderSize));
    }

    private static bool IsFraction(object value)
    {
        var numericValue = (double)value;
        return numericValue >= 0d && numericValue <= 1d;
    }

    private static void OnClippingInvalidated(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is UIElement element && VisualTreeHelper.GetParent(element) is Clipper translator)
        {
            translator.InvalidateMeasure();
        }
    }

    private static bool IsValidConstraintSource(object value)
    {
        return Enum.IsDefined(typeof(ConstraintSource), value);
    }
}

public enum ConstraintSource
{
    WidthAndHeight,
    Width,
    Height,
    Nothing
}

public class DependencyPropertySubscriber : DependencyObject
{    
    private static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(DependencyPropertySubscriber), new PropertyMetadata(null, ValueChanged));

    private readonly PropertyChangedCallback _handler;

    public DependencyPropertySubscriber(DependencyObject dependencyObject, DependencyProperty dependencyProperty, PropertyChangedCallback handler)
    {
        if (dependencyObject is null)
        {
            throw new ArgumentNullException(nameof(dependencyObject));
        }

        if (dependencyProperty is null)
        {
            throw new ArgumentNullException(nameof(dependencyProperty));
        }

        _handler = handler ?? throw new ArgumentNullException(nameof(handler));

        var binding = new Binding() { Path = new PropertyPath(dependencyProperty), Source = dependencyObject, Mode = BindingMode.OneWay };
        BindingOperations.SetBinding(this, ValueProperty, binding);
    }

    public void Unsubscribe()
    {
        BindingOperations.ClearBinding(this, ValueProperty);
    }

    private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((DependencyPropertySubscriber)d)._handler(d, e);
    }
}

The usage is as follows:

<Clipper Constraint="WidthAndHeight">
    <Control Clipper.HeightFraction="0.5"
             Clipper.WidthFraction="0.5" />
</Clipper>

Note the Constraint property: it determines what the child control considers "Auto" dimensions. For example, if your control is static (has Height and Width set explicitly), you should set Constraint to Nothing to clip the fraction of the entire element. If your control is WrapPanel with Orientation set to Horizontal, Constraint should be set to Width, etc. If you are getting wrong clipping, try out out different constraints. Note also that Clipper respects you control's alignment, which can potentially be exploited in an animation (for example, while animating HeightFraction from 0 to 1, VerticalAlignment.Bottom will mean that the control "slides down", VerticalAlignment.Center - "opens up").

TripleAccretion
  • 312
  • 3
  • 10
-1
<Style TargetType="{x:Type YourType}">
        <Setter Property="Height" Value="{Binding ActualHeight, RelativeSource={RelativeSource Mode=Self}}" />
        <Style.Triggers>
            <Trigger Property="Visibility" Value="Visible">
                <Trigger.EnterActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="Height" From="0" Duration="0:0:0.2" />
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.EnterActions>
                <Trigger.ExitActions>
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="Height" To="0" Duration="0:0:0.2" />
                        </Storyboard>
                    </BeginStoryboard>
                </Trigger.ExitActions>
            </Trigger>
        </Style.Triggers>
    </Style>

This works perfect!

Tequipo
  • 1
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 01 '22 at 11:20