UWP grid rowheight not updating after scale transform of content

602 Views Asked by At

I have a control which contains 2 rows (header and content). The content has a ScaleY transform on it when the pointer enters or exists, but it seems the row which has height Auto does not collapse the height when the content is scaled to 0.

<Page
x:Class="ScaleTest.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ScaleTest"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <local:TestControl VerticalAlignment="Bottom"/>

</Grid>

    <Style TargetType="local:TestControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:TestControl">
                <Grid VerticalAlignment="{TemplateBinding VerticalAlignment}">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="DisplayStates">
                            <VisualState x:Name="Minimized">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="contentTransForm" 
                                                     Storyboard.TargetProperty="ScaleY" 
                                                     Duration="0:0:0.2" 
                                                     To="0"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Maximized">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="contentTransForm" 
                                                     Storyboard.TargetProperty="ScaleY" 
                                                     Duration="0:0:0.2" 
                                                     To="1"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid Background="Red">
                        <TextBlock Text="Header"/>
                    </Grid>
                    <Grid Grid.Row="1" Background="Orange">
                        <Grid.RenderTransform>
                            <CompositeTransform x:Name="contentTransForm" ScaleY="0" ScaleX="1"/>
                        </Grid.RenderTransform>
                        <TextBlock Text="Content"/>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

I made a small sample app to show the problem. sample

Why the grid is not updating.

3

There are 3 best solutions below

0
On BEST ANSWER

Thanks guys,

I actually solved it by implementing a LayoutTansformer from the wpf toolkit and using a mediator it and the transformer. It gives a nice smooth animation without staying up and then suddenly collapsing.

the LayoutTransformer:

    [TemplatePart(Name = "Presenter", Type = typeof(ContentPresenter))]
[TemplatePart(Name = "TransformRoot", Type = typeof(Grid))]
public sealed class LayoutTransformer : ContentControl
{
    public static readonly DependencyProperty LayoutTransformProperty = DependencyProperty.Register("LayoutTransform", typeof(Transform), typeof(LayoutTransformer), new PropertyMetadata(new PropertyChangedCallback(LayoutTransformer.LayoutTransformChanged)));

    private Size _childActualSize = Size.Empty;

    private const string TransformRootName = "TransformRoot";

    private const string PresenterName = "Presenter";

    private const double AcceptableDelta = 0.0001;

    private const int DecimalsAfterRound = 4;

    private Panel _transformRoot;

    private ContentPresenter _contentPresenter;

    private MatrixTransform _matrixTransform;

    private Matrix _transformation;

    public Transform LayoutTransform
    {
        get => (Transform)GetValue(LayoutTransformer.LayoutTransformProperty);
        set => SetValue(LayoutTransformer.LayoutTransformProperty, (object)value);
    }

    private FrameworkElement Child => _contentPresenter?.Content as FrameworkElement ?? _contentPresenter;

    public LayoutTransformer()
    {
        DefaultStyleKey = (object)typeof(LayoutTransformer);
        IsTabStop = false;
        UseLayoutRounding = false;
    }

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _transformRoot = (Panel)(GetTemplateChild(TransformRootName) as Grid);
        _contentPresenter = GetTemplateChild(PresenterName) as ContentPresenter;
        _matrixTransform = new MatrixTransform();
        if (null != _transformRoot)
            _transformRoot.RenderTransform = (Transform)_matrixTransform;
        ApplyLayoutTransform();
    }

    private static void LayoutTransformChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        ((LayoutTransformer)o).ProcessTransform((Transform)e.NewValue);
    }

    public void ApplyLayoutTransform()
    {
        ProcessTransform(LayoutTransform);
    }

    private void ProcessTransform(Transform transform)
    {
        _transformation = LayoutTransformer.RoundMatrix(GetTransformMatrix(transform), DecimalsAfterRound);
        if (null != _matrixTransform)
            _matrixTransform.Matrix = _transformation;
        InvalidateMeasure();
    }

    private Matrix GetTransformMatrix(Transform transform)
    {
        if (null == transform) return Matrix.Identity;
        if (transform is TransformGroup transformGroup)
        {
            var matrix1 = Matrix.Identity;
            foreach (var transform1 in (TransformCollection)transformGroup.Children)
                matrix1 = LayoutTransformer.MatrixMultiply(matrix1, GetTransformMatrix(transform1));
            return matrix1;
        }
        if (transform is RotateTransform rotateTransform)
        {
            var num1 = 2.0 * Math.PI * rotateTransform.Angle / 360.0;
            var m12 = Math.Sin(num1);
            var num2 = Math.Cos(num1);
            return new Matrix(num2, m12, -m12, num2, 0.0, 0.0);
        }
        if (transform is ScaleTransform scaleTransform)
            return new Matrix(scaleTransform.ScaleX, 0.0, 0.0, scaleTransform.ScaleY, 0.0, 0.0);
        if (transform is SkewTransform skewTransform)
        {
            var angleX = skewTransform.AngleX;
            return new Matrix(1.0, 2.0 * Math.PI * skewTransform.AngleY / 360.0, 2.0 * Math.PI * angleX / 360.0, 1.0, 0.0, 0.0);
        }
        if (transform is MatrixTransform matrixTransform)
            return matrixTransform.Matrix;
        return Matrix.Identity;
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        if (_transformRoot == null || null == Child)
            return Size.Empty;
        _transformRoot.Measure(!(_childActualSize == Size.Empty) ? _childActualSize : ComputeLargestTransformedSize(availableSize));
        var x = 0.0;
        var y = 0.0;
        var desiredSize = _transformRoot.DesiredSize;
        var width = desiredSize.Width;
        desiredSize = _transformRoot.DesiredSize;
        var height = desiredSize.Height;
        var rect = LayoutTransformer.RectTransform(new Rect(x, y, width, height), _transformation);
        return new Size(rect.Width, rect.Height);
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        var child = Child;
        if (_transformRoot == null || null == child)
            return finalSize;
        var a = ComputeLargestTransformedSize(finalSize);
        if (LayoutTransformer.IsSizeSmaller(a, _transformRoot.DesiredSize))
            a = _transformRoot.DesiredSize;
        var rect = LayoutTransformer.RectTransform(new Rect(0.0, 0.0, a.Width, a.Height), _transformation);
        _transformRoot.Arrange(new Rect(-rect.Left + (finalSize.Width - rect.Width) / 2.0, -rect.Top + (finalSize.Height - rect.Height) / 2.0, a.Width, a.Height));
        if (LayoutTransformer.IsSizeSmaller(a, child.RenderSize) && Size.Empty == _childActualSize)
        {
            _childActualSize = new Size(child.ActualWidth, child.ActualHeight);
            InvalidateMeasure();
        }
        else
            _childActualSize = Size.Empty;
        return finalSize;
    }

    private Size ComputeLargestTransformedSize(Size arrangeBounds)
    {
        var size = Size.Empty;
        var flag1 = double.IsInfinity(arrangeBounds.Width);
        if (flag1)
            arrangeBounds.Width = arrangeBounds.Height;
        var flag2 = double.IsInfinity(arrangeBounds.Height);
        if (flag2)
            arrangeBounds.Height = arrangeBounds.Width;
        var m11 = _transformation.M11;
        var m12 = _transformation.M12;
        var m21 = _transformation.M21;
        var m22 = _transformation.M22;
        var num1 = Math.Abs(arrangeBounds.Width / m11);
        var num2 = Math.Abs(arrangeBounds.Width / m21);
        var num3 = Math.Abs(arrangeBounds.Height / m12);
        var num4 = Math.Abs(arrangeBounds.Height / m22);
        var num5 = num1 / 2.0;
        var num6 = num2 / 2.0;
        var num7 = num3 / 2.0;
        var num8 = num4 / 2.0;
        var num9 = -(num2 / num1);
        var num10 = -(num4 / num3);
        if (0.0 == arrangeBounds.Width || 0.0 == arrangeBounds.Height)
            size = new Size(arrangeBounds.Width, arrangeBounds.Height);
        else if (flag1 && flag2)
            size = new Size(double.PositiveInfinity, double.PositiveInfinity);
        else if (!LayoutTransformer.MatrixHasInverse(_transformation))
            size = new Size(0.0, 0.0);
        else if (0.0 == m12 || 0.0 == m21)
        {
            var num11 = flag2 ? double.PositiveInfinity : num4;
            var num12 = flag1 ? double.PositiveInfinity : num1;
            if (0.0 == m12 && 0.0 == m21)
                size = new Size(num12, num11);
            else if (0.0 == m12)
            {
                var height = Math.Min(num6, num11);
                size = new Size(num12 - Math.Abs(m21 * height / m11), height);
            }
            else if (0.0 == m21)
            {
                var width = Math.Min(num7, num12);
                size = new Size(width, num11 - Math.Abs(m12 * width / m22));
            }
        }
        else if (0.0 == m11 || 0.0 == m22)
        {
            var num11 = flag2 ? double.PositiveInfinity : num3;
            var num12 = flag1 ? double.PositiveInfinity : num2;
            if (0.0 == m11 && 0.0 == m22)
                size = new Size(num11, num12);
            else if (0.0 == m11)
            {
                var height = Math.Min(num8, num12);
                size = new Size(num11 - Math.Abs(m22 * height / m12), height);
            }
            else if (0.0 == m22)
            {
                var width = Math.Min(num5, num11);
                size = new Size(width, num12 - Math.Abs(m11 * width / m21));
            }
        }
        else if (num6 <= num10 * num5 + num4)
            size = new Size(num5, num6);
        else if (num8 <= num9 * num7 + num2)
        {
            size = new Size(num7, num8);
        }
        else
        {
            var width = (num4 - num2) / (num9 - num10);
            size = new Size(width, num9 * width + num2);
        }
        return size;
    }

    private static bool IsSizeSmaller(Size a, Size b)
    {
        return a.Width + AcceptableDelta < b.Width || a.Height + AcceptableDelta < b.Height;
    }

    private static Matrix RoundMatrix(Matrix matrix, int decimals)
    {
        return new Matrix(Math.Round(matrix.M11, decimals), Math.Round(matrix.M12, decimals), Math.Round(matrix.M21, decimals), Math.Round(matrix.M22, decimals), matrix.OffsetX, matrix.OffsetY);
    }

    private static Rect RectTransform(Rect rect, Matrix matrix)
    {
        var point1 = matrix.Transform(new Point(rect.Left, rect.Top));
        var point2 = matrix.Transform(new Point(rect.Right, rect.Top));
        var point3 = matrix.Transform(new Point(rect.Left, rect.Bottom));
        var point4 = matrix.Transform(new Point(rect.Right, rect.Bottom));
        var x = Math.Min(Math.Min(point1.X, point2.X), Math.Min(point3.X, point4.X));
        var y = Math.Min(Math.Min(point1.Y, point2.Y), Math.Min(point3.Y, point4.Y));
        var num1 = Math.Max(Math.Max(point1.X, point2.X), Math.Max(point3.X, point4.X));
        var num2 = Math.Max(Math.Max(point1.Y, point2.Y), Math.Max(point3.Y, point4.Y));
        return new Rect(x, y, num1 - x, num2 - y);
    }

    private static Matrix MatrixMultiply(Matrix matrix1, Matrix matrix2)
    {
        return new Matrix(matrix1.M11 * matrix2.M11 + matrix1.M12 * matrix2.M21, matrix1.M11 * matrix2.M12 + matrix1.M12 * matrix2.M22, matrix1.M21 * matrix2.M11 + matrix1.M22 * matrix2.M21, matrix1.M21 * matrix2.M12 + matrix1.M22 * matrix2.M22, matrix1.OffsetX * matrix2.M11 + matrix1.OffsetY * matrix2.M21 + matrix2.OffsetX, matrix1.OffsetX * matrix2.M12 + matrix1.OffsetY * matrix2.M22 + matrix2.OffsetY);
    }

    private static bool MatrixHasInverse(Matrix matrix)
    {
        return 0.0 != matrix.M11 * matrix.M22 - matrix.M12 * matrix.M21;
    }
}

<Style TargetType="local:LayoutTransformer">
    <Setter Property="Foreground" Value="#FF000000" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:LayoutTransformer">
                <Grid x:Name="TransformRoot" Background="{TemplateBinding Background}">
                    <ContentPresenter x:Name="Presenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

the Mediator:

    public class DoubleAnimationMediator : FrameworkElement
{
    private string _layoutTransformerName;

    public LayoutTransformer LayoutTransformer { get; set; }

    public string LayoutTransformerName
    {
        get => _layoutTransformerName;
        set
        {
            _layoutTransformerName = value;
            LayoutTransformer = null;
        }
    }

    public static readonly DependencyProperty AnimationValueProperty =
        DependencyProperty.Register(
            "AnimationValue",
            typeof(double),
            typeof(DoubleAnimationMediator),
            new PropertyMetadata(0, AnimationValuePropertyChanged));

    private static void AnimationValuePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        ((DoubleAnimationMediator)o).AnimationValuePropertyChanged();
    }

    public double AnimationValue
    {
        get => (double)GetValue(AnimationValueProperty);
        set => SetValue(AnimationValueProperty, value);
    }

    private void AnimationValuePropertyChanged()
    {
        if (null == LayoutTransformer)
        {
            LayoutTransformer = FindName(LayoutTransformerName) as LayoutTransformer;
            if (null == LayoutTransformer)
            {
                throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                    "AnimationMediator was unable to find a LayoutTransformer named \"{0}\".",
                    LayoutTransformerName));
            }
        }
        CoreApplication.MainView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => LayoutTransformer.ApplyLayoutTransform()).AsTask().ConfigureAwait(true);
    }
}

The TestControl:

    public class TestControl : Control
{
    public TestControl()
    {
        DefaultStyleKey = typeof(TestControl);

        PointerEntered += OnPointerEntered;
        PointerExited += OnPointerExited;
    }

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        VisualStateManager.GoToState(this, "Minimized", false);
    }

    private void OnPointerExited(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
    {
        VisualStateManager.GoToState(this, "Minimized", false);
    }

    private void OnPointerEntered(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
    {
        VisualStateManager.GoToState(this, "Maximized", false);
    }
}

    <Style TargetType="local:TestControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:TestControl">
                <Grid VerticalAlignment="{TemplateBinding VerticalAlignment}">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="DisplayStates">
                            <VisualState x:Name="Minimized">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="scaleYMediator" 
                                                     Storyboard.TargetProperty="AnimationValue" 
                                                     EnableDependentAnimation="True"
                                                     Duration="0:0:0.2" 
                                                     To="0"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Maximized">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetName="scaleYMediator" 
                                                     Storyboard.TargetProperty="AnimationValue" 
                                                     EnableDependentAnimation="True"
                                                     Duration="0:0:0.2" 
                                                     To="1"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>
                    <local:DoubleAnimationMediator x:Name="scaleYMediator"
                                                   LayoutTransformerName="layoutTransform"
                                                   AnimationValue="{Binding ScaleY, ElementName=scaleTransform, Mode=TwoWay}"/>
                    <Grid Background="Red"
                          Grid.Row="0">
                        <TextBlock Text="Header"/>
                    </Grid>
                    <local:LayoutTransformer x:Name="layoutTransform" Grid.Row="1"
                                             Background="Orange">
                        <local:LayoutTransformer.LayoutTransform>
                            <TransformGroup>
                                <ScaleTransform x:Name="scaleTransform" ScaleY="0"/>
                            </TransformGroup>
                        </local:LayoutTransformer.LayoutTransform>
                        <local:LayoutTransformer.Content>
                            <Grid>
                                <TextBlock Text="Content"/>
                            </Grid>
                        </local:LayoutTransformer.Content>
                    </local:LayoutTransformer>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
0
On

That is indeed the case. RenderTransform is actually just for rendering and does not influence the layout of the page. To make the animation appear the way you want, you would need to animate the height of the orange Grid instead of ScaleY. The problem with that is the fact that animating such property causes layout changes so you have to add EnableDependentAnimation="True" to your DoubleAnimations. Beware however, that dependent animations are much less performant as they require a layout cycle with each frame. It is always better to avoid them if you find a better solution.

One solution could be to animate the header position (TranslateTransform) when scaling down the content and then set Visibility to actually update the layout and make sure everything is in place. This would be again without layout updates and could work well.

3
On

You need to add setters for your visual state at your control template.

<ControlTemplate TargetType="local:TestControl">
    <Grid VerticalAlignment="{TemplateBinding VerticalAlignment}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid Background="Red">
            <TextBlock Text="Header"/>
        </Grid>
        <Grid x:Name="ContentGrid" Grid.Row="1" Background="Orange" MinHeight="0" Height="auto">
            <Grid.RenderTransform>
                <CompositeTransform x:Name="contentTransForm" ScaleY="0" ScaleX="1"/>
            </Grid.RenderTransform>
            <TextBlock Text="Content">
                <!--<TextBlock.RenderTransform>
                    <CompositeTransform x:Name="contentTransForm" ScaleY="0" ScaleX="1"/>
                </TextBlock.RenderTransform>-->
            </TextBlock>
        </Grid>


        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="DisplayStates">
                <VisualState x:Name="Minimized">
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="contentTransForm" 
                                         Storyboard.TargetProperty="ScaleY" 
                                         Duration="0:0:0.2" 
                                         To="0"/>
                    </Storyboard>
                    <VisualState.Setters>
                        <Setter Target="ContentGrid.Visibility" Value="Collapsed" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState x:Name="Maximized">
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="contentTransForm" 
                                         Storyboard.TargetProperty="ScaleY" 
                                         Duration="0:0:0.2" 
                                         To="1"/>
                    </Storyboard>
                    <VisualState.Setters>
                        <Setter Target="ContentGrid.Visibility" Value="Visible" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Grid>
</ControlTemplate>

Here is the modified source from your application