Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF animating a StackPanel's width from 0 to Auto?

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.

like image 478
Alex Hope O'Connor Avatar asked Dec 04 '14 22:12

Alex Hope O'Connor


2 Answers

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>
like image 62
learningcs Avatar answered Sep 28 '22 07:09

learningcs


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").

like image 28
TripleAccretion Avatar answered Sep 28 '22 07:09

TripleAccretion