【问题标题】:WPF animating a StackPanel's width from 0 to Auto?WPF将StackPanel的宽度从0设置为Auto?
【发布时间】:2014-12-04 22:44:45
【问题描述】:

当 StackPanel 的可见性从 0 的宽度变为其自动宽度时,我正在尝试对其进行动画处理,这就是我目前所拥有的:

<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>

谁能解释我如何实现这个动画?以我尝试的方式可能是不可能的吗?

谢谢, 亚历克斯。

【问题讨论】:

  • 我通过将 ScaleTransform 从 0 设置为 1 作为双重动画制作了一些示例代码,但是我似乎无法控制它的动画方向。您希望我发布示例代码作为答案吗?
  • @rshepp 如果您遇到了所有麻烦,即使它成功了,您也可以发布。
  • 是的,发布吧,我发现的唯一一件接近工作的事情是使用 LayoutTransform,但我对它的理解不够好,无法满足我的需求。
  • 其实没什么问题,我经常尝试模仿和解决问题,因为我发现这是最好的学习方式

标签: .net wpf xaml wpf-controls wpf-4.0


【解决方案1】:

这是我拼凑的一个快速模型项目。

在 Window 的 Loaded 事件中,我只是将 stackpanel 的可见性设置为 Visible,它会从左到右扩展以适应其容器宽度...希望这能满足您的需求。

注意事项:

  • 您必须预先定义缩放变换,否则动画将无法播放。
  • 如果您在动画中省略 To,则动画会恢复为默认值。

这里是代码:

<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>

【讨论】:

  • 效果很好!宁愿直接为宽度设置动画,但缩放动画也不显得破旧。谢谢!
  • Visibility 更改为“可见”时,这非常适用于动画 - 当Visibility 为“折叠”时,我将如何添加“反向”动画?
  • 我在您上面的评论中看到您无法控制动画的方向;您所要做的就是将RenderTransformOrigin 设置为1,0 以从另一侧设置动画,或将0.5,0 设置为从中心设置动画。
  • @ScottSEA 关于起源的好信息,经验不足的我当时没有考虑过:P 如果您打算使用可见性属性可能不是触发此动画的最佳方式扭转。折叠控件会立即隐藏它,因此您不会看到任何动画——也许您可以使用“Tag”属性作为两个方向的触发器。例如,将标签设置为 1 将立即将控件设置为可见,然后播放动画。然后,将标签设置为 0 将播放反向动画,然后在动画完成后折叠控件。
【解决方案2】:

所以,这是一个相当古老的问题,但我认为这是一个足够常见的场景,您需要将 WidthHeight0 动画化到 Auto(或类似)以证明额外答案的合理性.我不会在这里关注 Alex 的确切要求,以强调我提出的解决方案的一般性质。

即:编写您自己的 Clipper 控件,将其孩子的可见 WidthHeight 剪辑到其中的一部分。然后我们可以为这些Fraction 属性(0 -> 1)设置动画以达到预期的效果。 Clipper 的代码如下,包括所有的助手。

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);
    }
}

用法如下:

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

注意Constraint 属性:它决定了子控件认为什么是“自动”尺寸。例如,如果您的控件是静态的(明确设置了HeightWidth),则应将Constraint 设置为Nothing 以剪切整个元素的一部分。如果您的控件是WrapPanel,而Orientation 设置为HorizontalConstraint 应设置为Width,等等。如果您遇到错误的剪辑,请尝试不同的约束。另请注意Clipper 尊重您控件的对齐方式,这可能会在动画中被利用(例如,在将HeightFraction0 动画到1 时,VerticalAlignment.Bottom 将意味着控件“向下滑动”, VerticalAlignment.Center - “打开”)。

【讨论】:

    【解决方案3】:
    <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>
    

    这很完美!

    【讨论】:

    • 您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center
    猜你喜欢
    • 2011-09-27
    • 2019-11-10
    • 2015-11-30
    • 1970-01-01
    • 2020-03-30
    • 1970-01-01
    • 2017-02-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多