Merge pull request #4404 from Ryken100/ryken100/feature-AttachedNineGridCardShadow

Clip inner content from AttachedCardShadow using CompositionMaskBrush
This commit is contained in:
Michael Hawker MSFT (XAML Llama) 2022-05-11 10:14:45 -07:00 коммит произвёл GitHub
Родитель 00e9790337 65e350b8ca
Коммит cca764fafb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
4 изменённых файлов: 213 добавлений и 14 удалений

Просмотреть файл

@ -37,7 +37,8 @@
CornerRadius="32" CornerRadius="32"
Color="@[Color:Brush:Black]" Color="@[Color:Brush:Black]"
Offset="@[Offset:Vector3:4,4]" Offset="@[Offset:Vector3:4,4]"
Opacity="@[Opacity:DoubleSlider:1.0:0.0-1.0]"/> Opacity="@[Opacity:DoubleSlider:1.0:0.0-1.0]"
InnerContentClipMode="@[InnerContentClipMode:Enum:InnerContentClipMode.CompositionGeometricClip]"/>
</ui:Effects.Shadow> </ui:Effects.Shadow>
</Rectangle> </Rectangle>
<!-- If you want to apply a shadow directly in your visual tree to an untemplated element <!-- If you want to apply a shadow directly in your visual tree to an untemplated element

Просмотреть файл

@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.Toolkit.Uwp.UI.Media
{
/// <summary>
/// The method that each instance of <see cref="AttachedCardShadow"/> uses when clipping its inner content.
/// </summary>
public enum InnerContentClipMode
{
/// <summary>
/// Do not clip inner content.
/// </summary>
None,
/// <summary>
/// Use <see cref="Windows.UI.Composition.CompositionMaskBrush"/> to clip inner content.
/// </summary>
/// <remarks>
/// This mode has better performance than <see cref="CompositionGeometricClip"/>.
/// </remarks>
CompositionMaskBrush,
/// <summary>
/// Use <see cref="Windows.UI.Composition.CompositionGeometricClip"/> to clip inner content.
/// </summary>
/// <remarks>
/// Content clipped in this mode will have smoother corners than when using <see cref="CompositionMaskBrush"/>.
/// </remarks>
CompositionGeometricClip
}
}

Просмотреть файл

@ -8,6 +8,7 @@ using Windows.Foundation;
using Windows.UI; using Windows.UI;
using Windows.UI.Composition; using Windows.UI.Composition;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Hosting;
namespace Microsoft.Toolkit.Uwp.UI.Media namespace Microsoft.Toolkit.Uwp.UI.Media
{ {
@ -20,9 +21,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
public sealed class AttachedCardShadow : AttachedShadowBase public sealed class AttachedCardShadow : AttachedShadowBase
{ {
private const float MaxBlurRadius = 72; private const float MaxBlurRadius = 72;
private static readonly TypedResourceKey<CompositionGeometricClip> ClipResourceKey = "Clip";
private static readonly TypedResourceKey<CompositionGeometricClip> ClipResourceKey = "Clip";
private static readonly TypedResourceKey<CompositionPathGeometry> PathGeometryResourceKey = "PathGeometry"; private static readonly TypedResourceKey<CompositionPathGeometry> PathGeometryResourceKey = "PathGeometry";
private static readonly TypedResourceKey<CompositionMaskBrush> OpacityMaskBrushResourceKey = "OpacityMask";
private static readonly TypedResourceKey<ShapeVisual> OpacityMaskShapeVisualResourceKey = "OpacityMaskShapeVisual";
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> OpacityMaskGeometryResourceKey = "OpacityMaskGeometry";
private static readonly TypedResourceKey<CompositionSpriteShape> OpacityMaskSpriteShapeResourceKey = "OpacityMaskSpriteShape";
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskShapeVisualSurfaceResourceKey = "OpacityMaskShapeVisualSurface";
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskShapeVisualSurfaceBrushResourceKey = "OpacityMaskShapeVisualSurfaceBrush";
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskVisualSurfaceResourceKey = "OpacityMaskVisualSurface";
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskSurfaceBrushResourceKey = "OpacityMaskSurfaceBrush";
private static readonly TypedResourceKey<SpriteVisual> OpacityMaskVisualResourceKey = "OpacityMaskVisual";
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> RoundedRectangleGeometryResourceKey = "RoundedGeometry"; private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> RoundedRectangleGeometryResourceKey = "RoundedGeometry";
private static readonly TypedResourceKey<CompositionSpriteShape> ShapeResourceKey = "Shape"; private static readonly TypedResourceKey<CompositionSpriteShape> ShapeResourceKey = "Shape";
private static readonly TypedResourceKey<ShapeVisual> ShapeVisualResourceKey = "ShapeVisual"; private static readonly TypedResourceKey<ShapeVisual> ShapeVisualResourceKey = "ShapeVisual";
@ -39,6 +49,16 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
typeof(AttachedCardShadow), typeof(AttachedCardShadow),
new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4 new PropertyMetadata(4d, OnDependencyPropertyChanged)); // Default WinUI ControlCornerRadius is 4
/// <summary>
/// The <see cref="DependencyProperty"/> for <see cref="InnerContentClipMode"/>.
/// </summary>
public static readonly DependencyProperty InnerContentClipModeProperty =
DependencyProperty.Register(
nameof(InnerContentClipMode),
typeof(InnerContentClipMode),
typeof(AttachedCardShadow),
new PropertyMetadata(InnerContentClipMode.CompositionGeometricClip, OnDependencyPropertyChanged));
/// <summary> /// <summary>
/// Gets or sets the roundness of the shadow's corners. /// Gets or sets the roundness of the shadow's corners.
/// </summary> /// </summary>
@ -48,24 +68,47 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
set => SetValue(CornerRadiusProperty, value); set => SetValue(CornerRadiusProperty, value);
} }
/// <summary>
/// Gets or sets the mode use to clip inner content from the shadow.
/// </summary>
public InnerContentClipMode InnerContentClipMode
{
get => (InnerContentClipMode)GetValue(InnerContentClipModeProperty);
set => SetValue(InnerContentClipModeProperty, value);
}
/// <inheritdoc/> /// <inheritdoc/>
public override bool IsSupported => SupportsCompositionVisualSurface; public override bool IsSupported => SupportsCompositionVisualSurface;
/// <inheritdoc/> /// <inheritdoc/>
protected internal override bool SupportsOnSizeChangedEvent => true; protected internal override bool SupportsOnSizeChangedEvent => true;
/// <inheritdoc/>
protected internal override void OnElementContextInitialized(AttachedShadowElementContext context)
{
UpdateVisualOpacityMask(context);
base.OnElementContextInitialized(context);
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue) protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue)
{ {
if (property == CornerRadiusProperty) if (property == CornerRadiusProperty)
{ {
UpdateShadowClip(context);
UpdateVisualOpacityMask(context);
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey); var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
if (geometry != null) if (geometry != null)
{ {
geometry.CornerRadius = new Vector2((float)(double)newValue); geometry.CornerRadius = new Vector2((float)(double)newValue);
} }
}
else if (property == InnerContentClipModeProperty)
{
UpdateShadowClip(context); UpdateShadowClip(context);
UpdateVisualOpacityMask(context);
SetElementChildVisual(context);
} }
else else
{ {
@ -114,6 +157,13 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
/// <inheritdoc/> /// <inheritdoc/>
protected override CompositionClip GetShadowClip(AttachedShadowElementContext context) protected override CompositionClip GetShadowClip(AttachedShadowElementContext context)
{ {
if (InnerContentClipMode != InnerContentClipMode.CompositionGeometricClip)
{
context.RemoveAndDisposeResource(PathGeometryResourceKey);
context.RemoveAndDisposeResource(ClipResourceKey);
return null;
}
// The way this shadow works without the need to project on another element is because // The way this shadow works without the need to project on another element is because
// we're clipping the inner part of the shadow which would be cast on the element // we're clipping the inner part of the shadow which would be cast on the element
// itself away. This method is creating an outline so that we are only showing the // itself away. This method is creating an outline so that we are only showing the
@ -144,24 +194,130 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
return clip; return clip;
} }
/// <summary>
/// Updates the <see cref="CompositionBrush"/> used to mask <paramref name="context"/>.<see cref="AttachedShadowElementContext.SpriteVisual">SpriteVisual</see>.
/// </summary>
/// <param name="context">The <see cref="AttachedShadowElementContext"/> whose <see cref="SpriteVisual"/> will be masked.</param>
private void UpdateVisualOpacityMask(AttachedShadowElementContext context)
{
if (InnerContentClipMode != InnerContentClipMode.CompositionMaskBrush)
{
context.RemoveAndDisposeResource(OpacityMaskShapeVisualResourceKey);
context.RemoveAndDisposeResource(OpacityMaskGeometryResourceKey);
context.RemoveAndDisposeResource(OpacityMaskSpriteShapeResourceKey);
context.RemoveAndDisposeResource(OpacityMaskShapeVisualSurfaceResourceKey);
context.RemoveAndDisposeResource(OpacityMaskShapeVisualSurfaceBrushResourceKey);
return;
}
// Create ShapeVisual, and CompositionSpriteShape with geometry, these will provide the visuals for the opacity mask.
ShapeVisual shapeVisual = context.GetResource(OpacityMaskShapeVisualResourceKey) ??
context.AddResource(OpacityMaskShapeVisualResourceKey, context.Compositor.CreateShapeVisual());
CompositionRoundedRectangleGeometry geometry = context.GetResource(OpacityMaskGeometryResourceKey) ??
context.AddResource(OpacityMaskGeometryResourceKey, context.Compositor.CreateRoundedRectangleGeometry());
CompositionSpriteShape shape = context.GetResource(OpacityMaskSpriteShapeResourceKey) ??
context.AddResource(OpacityMaskSpriteShapeResourceKey, context.Compositor.CreateSpriteShape(geometry));
// Set the attributes of the geometry, and add the CompositionSpriteShape to the ShapeVisual.
// The geometry will have a thick outline and no fill, meaning that when used as a mask,
// the shadow will only be rendered on the outer area covered by the outline, clipping out its inner portion.
geometry.Offset = new Vector2(MaxBlurRadius / 2);
geometry.CornerRadius = new Vector2((MaxBlurRadius / 2) + (float)CornerRadius);
shape.StrokeThickness = MaxBlurRadius;
shape.StrokeBrush = shape.StrokeBrush ?? context.Compositor.CreateColorBrush(Colors.Black);
if (!shapeVisual.Shapes.Contains(shape))
{
shapeVisual.Shapes.Add(shape);
}
// Create CompositionVisualSurface using the ShapeVisual as the source visual.
CompositionVisualSurface visualSurface = context.GetResource(OpacityMaskShapeVisualSurfaceResourceKey) ??
context.AddResource(OpacityMaskShapeVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
visualSurface.SourceVisual = shapeVisual;
geometry.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius);
shapeVisual.Size = visualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);
// Create a CompositionSurfaceBrush using the CompositionVisualSurface as the source, this essentially converts the ShapeVisual into a brush.
// This brush can then be used as a mask.
CompositionSurfaceBrush opacityMask = context.GetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey) ??
context.AddResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
opacityMask.Surface = visualSurface;
}
/// <inheritdoc/> /// <inheritdoc/>
protected override void SetElementChildVisual(AttachedShadowElementContext context)
{
if (context.TryGetResource(OpacityMaskShapeVisualSurfaceBrushResourceKey, out CompositionSurfaceBrush opacityMask))
{
// If the resource for OpacityMaskShapeVisualSurfaceBrushResourceKey exists it means this.InnerContentClipMode == CompositionVisualSurface,
// which means we need to take some steps to set up an opacity mask.
// Create a CompositionVisualSurface, and use the SpriteVisual containing the shadow as the source.
CompositionVisualSurface shadowVisualSurface = context.GetResource(OpacityMaskVisualSurfaceResourceKey) ??
context.AddResource(OpacityMaskVisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
shadowVisualSurface.SourceVisual = context.SpriteVisual;
context.SpriteVisual.RelativeSizeAdjustment = Vector2.Zero;
context.SpriteVisual.Size = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight);
// Adjust the offset and size of the CompositionVisualSurface to accommodate the thick outline of the shape created in UpdateVisualOpacityMask().
shadowVisualSurface.SourceOffset = new Vector2(-MaxBlurRadius);
shadowVisualSurface.SourceSize = new Vector2((float)context.Element.ActualWidth, (float)context.Element.ActualHeight) + new Vector2(MaxBlurRadius * 2);
// Create a CompositionSurfaceBrush from the CompositionVisualSurface. This allows us to render the shadow in a brush.
CompositionSurfaceBrush shadowSurfaceBrush = context.GetResource(OpacityMaskSurfaceBrushResourceKey) ??
context.AddResource(OpacityMaskSurfaceBrushResourceKey, context.Compositor.CreateSurfaceBrush());
shadowSurfaceBrush.Surface = shadowVisualSurface;
shadowSurfaceBrush.Stretch = CompositionStretch.None;
// Create a CompositionMaskBrush, using the CompositionSurfaceBrush of the shadow as the source,
// and the CompositionSurfaceBrush created in UpdateVisualOpacityMask() as the mask.
// This creates a brush that renders the shadow with its inner portion clipped out.
CompositionMaskBrush maskBrush = context.GetResource(OpacityMaskBrushResourceKey) ??
context.AddResource(OpacityMaskBrushResourceKey, context.Compositor.CreateMaskBrush());
maskBrush.Source = shadowSurfaceBrush;
maskBrush.Mask = opacityMask;
// Create a SpriteVisual and set its brush to the CompositionMaskBrush created in the previous step,
// then set it as the child of the element in the context.
SpriteVisual visual = context.GetResource(OpacityMaskVisualResourceKey) ??
context.AddResource(OpacityMaskVisualResourceKey, context.Compositor.CreateSpriteVisual());
visual.RelativeSizeAdjustment = Vector2.One;
visual.Offset = new Vector3(-MaxBlurRadius, -MaxBlurRadius, 0);
visual.Size = new Vector2(MaxBlurRadius * 2);
visual.Brush = maskBrush;
ElementCompositionPreview.SetElementChildVisual(context.Element, visual);
}
else
{
base.SetElementChildVisual(context);
context.RemoveAndDisposeResource(OpacityMaskVisualSurfaceResourceKey);
context.RemoveAndDisposeResource(OpacityMaskSurfaceBrushResourceKey);
context.RemoveAndDisposeResource(OpacityMaskVisualResourceKey);
context.RemoveAndDisposeResource(OpacityMaskBrushResourceKey);
}
}
/// <inheritdoc />
protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize) protected internal override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize)
{ {
var sizeAsVec2 = newSize.ToVector2(); Vector2 sizeAsVec2 = newSize.ToVector2();
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey); CompositionRoundedRectangleGeometry geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
if (geometry != null) if (geometry != null)
{ {
geometry.Size = sizeAsVec2; geometry.Size = sizeAsVec2;
} }
var visualSurface = context.GetResource(VisualSurfaceResourceKey); CompositionVisualSurface visualSurface = context.GetResource(VisualSurfaceResourceKey);
if (geometry != null) if (geometry != null)
{ {
visualSurface.SourceSize = sizeAsVec2; visualSurface.SourceSize = sizeAsVec2;
} }
var shapeVisual = context.GetResource(ShapeVisualResourceKey); ShapeVisual shapeVisual = context.GetResource(ShapeVisualResourceKey);
if (geometry != null) if (geometry != null)
{ {
shapeVisual.Size = sizeAsVec2; shapeVisual.Size = sizeAsVec2;

Просмотреть файл

@ -137,10 +137,13 @@ namespace Microsoft.Toolkit.Uwp.UI
Parent.OnElementContextUninitialized(this); Parent.OnElementContextUninitialized(this);
SpriteVisual.Shadow = null; if (SpriteVisual != null)
SpriteVisual.Dispose(); {
SpriteVisual.Shadow = null;
SpriteVisual.Dispose();
}
Shadow.Dispose(); Shadow?.Dispose();
ElementCompositionPreview.SetElementChildVisual(Element, null); ElementCompositionPreview.SetElementChildVisual(Element, null);
@ -197,7 +200,7 @@ namespace Microsoft.Toolkit.Uwp.UI
/// <returns>True if the resource exists, false otherwise</returns> /// <returns>True if the resource exists, false otherwise</returns>
public bool TryGetResource<T>(string key, out T resource) public bool TryGetResource<T>(string key, out T resource)
{ {
if (_resources != null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource) if (_resources is not null && _resources.TryGetValue(key, out var objResource) && objResource is T tResource)
{ {
resource = tResource; resource = tResource;
return true; return true;
@ -231,7 +234,7 @@ namespace Microsoft.Toolkit.Uwp.UI
/// <returns>The resource that was removed, if any</returns> /// <returns>The resource that was removed, if any</returns>
public T RemoveResource<T>(string key) public T RemoveResource<T>(string key)
{ {
if (_resources.TryGetValue(key, out var objResource)) if (_resources is not null && _resources.TryGetValue(key, out var objResource))
{ {
_resources.Remove(key); _resources.Remove(key);
if (objResource is T resource) if (objResource is T resource)
@ -252,7 +255,7 @@ namespace Microsoft.Toolkit.Uwp.UI
public T RemoveAndDisposeResource<T>(string key) public T RemoveAndDisposeResource<T>(string key)
where T : IDisposable where T : IDisposable
{ {
if (_resources.TryGetValue(key, out var objResource)) if (_resources is not null && _resources.TryGetValue(key, out var objResource))
{ {
_resources.Remove(key); _resources.Remove(key);
if (objResource is T resource) if (objResource is T resource)
@ -306,7 +309,7 @@ namespace Microsoft.Toolkit.Uwp.UI
/// </summary> /// </summary>
public void ClearAndDisposeResources() public void ClearAndDisposeResources()
{ {
if (_resources != null) if (_resources is not null)
{ {
foreach (var kvp in _resources) foreach (var kvp in _resources)
{ {