所以,这是一个相当古老的问题,但我认为这是一个足够常见的场景,您需要将 Width 或 Height 从 0 动画化到 Auto(或类似)以证明额外答案的合理性.我不会在这里关注 Alex 的确切要求,以强调我提出的解决方案的一般性质。
即:编写您自己的 Clipper 控件,将其孩子的可见 Width 和 Height 剪辑到其中的一部分。然后我们可以为这些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 属性:它决定了子控件认为什么是“自动”尺寸。例如,如果您的控件是静态的(明确设置了Height 和Width),则应将Constraint 设置为Nothing 以剪切整个元素的一部分。如果您的控件是WrapPanel,而Orientation 设置为Horizontal,Constraint 应设置为Width,等等。如果您遇到错误的剪辑,请尝试不同的约束。另请注意Clipper 尊重您控件的对齐方式,这可能会在动画中被利用(例如,在将HeightFraction 从0 动画到1 时,VerticalAlignment.Bottom 将意味着控件“向下滑动”, VerticalAlignment.Center - “打开”)。