Merge pull request #4404 from Ryken100/ryken100/feature-AttachedNineGridCardShadow
Clip inner content from AttachedCardShadow using CompositionMaskBrush
This commit is contained in:
Коммит
cca764fafb
|
@ -37,7 +37,8 @@
|
|||
CornerRadius="32"
|
||||
Color="@[Color:Brush:Black]"
|
||||
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>
|
||||
</Rectangle>
|
||||
<!-- 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.Composition;
|
||||
using Windows.UI.Xaml;
|
||||
using Windows.UI.Xaml.Hosting;
|
||||
|
||||
namespace Microsoft.Toolkit.Uwp.UI.Media
|
||||
{
|
||||
|
@ -20,9 +21,18 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
|
|||
public sealed class AttachedCardShadow : AttachedShadowBase
|
||||
{
|
||||
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<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<CompositionSpriteShape> ShapeResourceKey = "Shape";
|
||||
private static readonly TypedResourceKey<ShapeVisual> ShapeVisualResourceKey = "ShapeVisual";
|
||||
|
@ -39,6 +49,16 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
|
|||
typeof(AttachedCardShadow),
|
||||
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>
|
||||
/// Gets or sets the roundness of the shadow's corners.
|
||||
/// </summary>
|
||||
|
@ -48,24 +68,47 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
|
|||
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/>
|
||||
public override bool IsSupported => SupportsCompositionVisualSurface;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override bool SupportsOnSizeChangedEvent => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected internal override void OnElementContextInitialized(AttachedShadowElementContext context)
|
||||
{
|
||||
UpdateVisualOpacityMask(context);
|
||||
base.OnElementContextInitialized(context);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue)
|
||||
{
|
||||
if (property == CornerRadiusProperty)
|
||||
{
|
||||
UpdateShadowClip(context);
|
||||
UpdateVisualOpacityMask(context);
|
||||
|
||||
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
|
||||
if (geometry != null)
|
||||
{
|
||||
geometry.CornerRadius = new Vector2((float)(double)newValue);
|
||||
}
|
||||
|
||||
}
|
||||
else if (property == InnerContentClipModeProperty)
|
||||
{
|
||||
UpdateShadowClip(context);
|
||||
UpdateVisualOpacityMask(context);
|
||||
SetElementChildVisual(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -114,6 +157,13 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
|
|||
/// <inheritdoc/>
|
||||
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
|
||||
// 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
|
||||
|
@ -144,24 +194,130 @@ namespace Microsoft.Toolkit.Uwp.UI.Media
|
|||
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/>
|
||||
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)
|
||||
{
|
||||
var sizeAsVec2 = newSize.ToVector2();
|
||||
Vector2 sizeAsVec2 = newSize.ToVector2();
|
||||
|
||||
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
|
||||
CompositionRoundedRectangleGeometry geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
|
||||
if (geometry != null)
|
||||
{
|
||||
geometry.Size = sizeAsVec2;
|
||||
}
|
||||
|
||||
var visualSurface = context.GetResource(VisualSurfaceResourceKey);
|
||||
CompositionVisualSurface visualSurface = context.GetResource(VisualSurfaceResourceKey);
|
||||
if (geometry != null)
|
||||
{
|
||||
visualSurface.SourceSize = sizeAsVec2;
|
||||
}
|
||||
|
||||
var shapeVisual = context.GetResource(ShapeVisualResourceKey);
|
||||
ShapeVisual shapeVisual = context.GetResource(ShapeVisualResourceKey);
|
||||
if (geometry != null)
|
||||
{
|
||||
shapeVisual.Size = sizeAsVec2;
|
||||
|
|
|
@ -137,10 +137,13 @@ namespace Microsoft.Toolkit.Uwp.UI
|
|||
|
||||
Parent.OnElementContextUninitialized(this);
|
||||
|
||||
SpriteVisual.Shadow = null;
|
||||
SpriteVisual.Dispose();
|
||||
if (SpriteVisual != null)
|
||||
{
|
||||
SpriteVisual.Shadow = null;
|
||||
SpriteVisual.Dispose();
|
||||
}
|
||||
|
||||
Shadow.Dispose();
|
||||
Shadow?.Dispose();
|
||||
|
||||
ElementCompositionPreview.SetElementChildVisual(Element, null);
|
||||
|
||||
|
@ -197,7 +200,7 @@ namespace Microsoft.Toolkit.Uwp.UI
|
|||
/// <returns>True if the resource exists, false otherwise</returns>
|
||||
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;
|
||||
return true;
|
||||
|
@ -231,7 +234,7 @@ namespace Microsoft.Toolkit.Uwp.UI
|
|||
/// <returns>The resource that was removed, if any</returns>
|
||||
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);
|
||||
if (objResource is T resource)
|
||||
|
@ -252,7 +255,7 @@ namespace Microsoft.Toolkit.Uwp.UI
|
|||
public T RemoveAndDisposeResource<T>(string key)
|
||||
where T : IDisposable
|
||||
{
|
||||
if (_resources.TryGetValue(key, out var objResource))
|
||||
if (_resources is not null && _resources.TryGetValue(key, out var objResource))
|
||||
{
|
||||
_resources.Remove(key);
|
||||
if (objResource is T resource)
|
||||
|
@ -306,7 +309,7 @@ namespace Microsoft.Toolkit.Uwp.UI
|
|||
/// </summary>
|
||||
public void ClearAndDisposeResources()
|
||||
{
|
||||
if (_resources != null)
|
||||
if (_resources is not null)
|
||||
{
|
||||
foreach (var kvp in _resources)
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче