animationuwprotationuwp-xamlwindows-composition-api

How to make backside of Windows Composition UI ElementVisual different from front when rotated?


I have a uwp xaml application with series of buttons in the UI.

I am using the Windows Composition Visual Layer APIs to animate the RotationAngleInDegrees of one of the Button's visuals.

The content of the button is visible on the front side which I expected, but it is also visible (although reversed) when the backside of the visual rotates into view.

The desired result is for the button to look similar to a playing card being flipped over where the content is visible from the front but hidden when the card is turned over.

I have tried a number of things including:

However, the Visual element still appears to be see-thru and the content shows on both faces of the rotated visual.

Is this a limitation or bug in the Windows Composition api? Is there a simple workaround to make the backside of visuals display something different than the content displayed on the front?

Here are the basics of the rotation code that produces the animation:

var btnVisual = ElementCompositionPreview.GetElementVisual(btn);            
var compositor = btnVisual.Compositor;

ScalarKeyFrameAnimation flipAnimation = compositor.CreateScalarKeyFrameAnimation();
flipAnimation.InsertKeyFrame(0.0f, 0);
flipAnimation.InsertKeyFrame(0.0001f, 180);
flipAnimation.InsertKeyFrame(1f, 0);
flipAnimation.Duration = TimeSpan.FromMilliseconds(800);
flipAnimation.IterationBehavior = AnimationIterationBehavior.Count;
flipAnimation.IterationCount = 1;
btnVisual.CenterPoint = new Vector3((float)(0.5 * btn.ActualWidth),(float) (0.5f * btn.ActualHeight), (float)(btn.ActualWidth/4));
btnVisual.RotationAxis = new Vector3(0.0f, 1f, 0f);

btnVisual.StartAnimation(nameof(btnVisual.RotationAngleInDegrees), flipAnimation);

The button that is being flipped is a default xaml button that originally had very simple styling:

<Style x:Key="ButtonStyle" TargetType="Button">
    <Setter Property="HorizontalAlignment" Value="Stretch" />
    <Setter Property="VerticalAlignment" Value="Stretch" />
    <Setter Property="Margin" Value="10" />
    <Setter Property="FontSize" Value="80" />
    <Setter Property="FontFamily" Value="Webdings" />
</Style>

and one of my experiments involved modifying the default button template to nest the content presenter in a couple of border containers:

<Style x:Key="ButtonStyle" TargetType="Button">
        <Setter Property="HorizontalAlignment" Value="Stretch" />
        <Setter Property="VerticalAlignment" Value="Stretch" />
        <Setter Property="Margin" Value="10" />
        <Setter Property="FontSize" Value="80" />
        <Setter Property="FontFamily" Value="Webdings" />
        <Setter Property="Background" Value="Silver" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal">
                                    <Storyboard>
                                        <PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="PointerOver">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                               Storyboard.TargetProperty="BorderBrush">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightBaseMediumLowBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                               Storyboard.TargetProperty="Foreground">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightBaseHighBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Pressed">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
                                               Storyboard.TargetProperty="Background">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlBackgroundBaseMediumLowBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                               Storyboard.TargetProperty="BorderBrush">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightTransparentBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                               Storyboard.TargetProperty="Foreground">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightBaseHighBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <PointerDownThemeAnimation Storyboard.TargetName="RootGrid" />
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="RootGrid"
                                               Storyboard.TargetProperty="Background">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                               Storyboard.TargetProperty="Foreground">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseMediumLowBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
                                               Storyboard.TargetProperty="BorderBrush">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledTransparentBrush}" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Border Background="Red">
                            <Border Background="#FF62FB0A" Margin="10,10,10,10">
                                <ContentPresenter x:Name="ContentPresenter"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Content="{TemplateBinding Content}"
                                ContentTransitions="{TemplateBinding ContentTransitions}"
                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                Padding="{TemplateBinding Padding}"
                                HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                                AutomationProperties.AccessibilityView="Raw"/>
                            </Border>
                        </Border>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

    </Style>

specifically the part modified part was

<Border Background="Red">
    <Border Background="#FF62FB0A" Margin="10,10,10,10">
        <ContentPresenter x:Name="ContentPresenter"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Content="{TemplateBinding Content}"
                                ContentTransitions="{TemplateBinding ContentTransitions}"
                                ContentTemplate="{TemplateBinding ContentTemplate}"
                                Padding="{TemplateBinding Padding}"
                                HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                                AutomationProperties.AccessibilityView="Raw"/>
    </Border>
</Border>

which helped me confirm that putting the button content inside border containers did not prevent the Visual from being see thru when rotated.


Solution

  • I have found a work around, but would still welcome any solutions that make the composition visuals respect their containment when it comes to 3D visibility of items on the front not showing through the back of parents.

    The work around that I came up with was to reach in and grab the content visual and then animate it's opacity so that it only becomes visible at the half way mark of the rotation. I also added a linear easing which seems to help with the timing of child visual showing up at the correct time.

    //Get a visual for the content
    var btnContent = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(btn,0),0);
    var btnContentVisual = ElementCompositionPreview.GetElementVisual(btnContent as FrameworkElement);  
    
    var easing = compositor.CreateLinearEasingFunction();  
    
    ScalarKeyFrameAnimation appearAnimation = compositor.CreateScalarKeyFrameAnimation();
    appearAnimation.InsertKeyFrame(0.0f, 0);
    appearAnimation.InsertKeyFrame(0.499999f, 0);
    appearAnimation.InsertKeyFrame(0.5f, 1);
    appearAnimation.InsertKeyFrame(1f, 1);
    appearAnimation.Duration = TimeSpan.FromMilliseconds(800);
    
    btnContentVisual.StartAnimation(nameof(btnContentVisual.Opacity), appearAnimation);
    

    Note: I removed the custom template with the extra Borders which is why the above workaround only needs to dig two levels deep with the VisualTreeHelper to get the contentpresenter so that I can retrieve its visual.