Skip to content

Commit ab486ec

Browse files
chris-blackmanmichael-hawker
authored andcommitted
Initial attached shadow code
1 parent cc562b3 commit ab486ec

File tree

6 files changed

+923
-0
lines changed

6 files changed

+923
-0
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// ------------------------------------------------------
2+
// Copyright (C) Microsoft. All rights reserved.
3+
// ------------------------------------------------------
4+
5+
using Microsoft.Graphics.Canvas.Geometry;
6+
using System.Numerics;
7+
using Windows.Foundation;
8+
using Windows.Foundation.Metadata;
9+
using Windows.UI;
10+
using Windows.UI.Composition;
11+
using Windows.UI.Xaml;
12+
13+
namespace Microsoft.Toolkit.Uwp.UI.Media.Shadows
14+
{
15+
public class AttachedCardShadow : AttachedShadowBase
16+
{
17+
private const float MaxBlurRadius = 72;
18+
private static readonly TypedResourceKey<CompositionGeometricClip> ClipResourceKey = "Clip";
19+
private static readonly bool SupportsCompositionVisualSurface;
20+
private static readonly bool SupportsCompositionGeometricClip;
21+
22+
private static readonly TypedResourceKey<CompositionPathGeometry> PathGeometryResourceKey = "PathGeometry";
23+
private static readonly TypedResourceKey<CompositionRoundedRectangleGeometry> RoundedRectangleGeometryResourceKey = "RoundedGeometry";
24+
private static readonly TypedResourceKey<CompositionSpriteShape> ShapeResourceKey = "Shape";
25+
private static readonly TypedResourceKey<ShapeVisual> ShapeVisualResourceKey = "ShapeVisual";
26+
private static readonly TypedResourceKey<CompositionSurfaceBrush> SurfaceBrushResourceKey = "SurfaceBrush";
27+
private static readonly TypedResourceKey<CompositionVisualSurface> VisualSurfaceResourceKey = "VisualSurface";
28+
29+
public static readonly DependencyProperty CornerRadiusProperty =
30+
DependencyProperty.Register(
31+
nameof(CornerRadius),
32+
typeof(double),
33+
typeof(AttachedCardShadow),
34+
new PropertyMetadata(8d, OnDependencyPropertyChanged));
35+
36+
static AttachedCardShadow()
37+
{
38+
SupportsCompositionGeometricClip = ApiInformation.IsTypePresent(typeof(CompositionGeometricClip).FullName);
39+
SupportsCompositionVisualSurface = ApiInformation.IsTypePresent(typeof(CompositionVisualSurface).FullName); ;
40+
}
41+
42+
/// <summary>
43+
/// Gets or sets the roundness of the shadow's corners
44+
/// </summary>
45+
public double CornerRadius
46+
{
47+
get => (double)GetValue(CornerRadiusProperty);
48+
set => SetValue(CornerRadiusProperty, value);
49+
}
50+
51+
/// <summary>
52+
/// <inheritdoc/>
53+
/// </summary>
54+
public override bool IsSupported => SupportsCompositionVisualSurface;
55+
56+
/// <summary>
57+
/// <inheritdoc/>
58+
/// </summary>
59+
public override bool SupportsOnSizeChangedEvent => true;
60+
61+
protected override void OnElementContextUninitialized(AttachedShadowElementContext context)
62+
{
63+
context.ClearAndDisposeResources();
64+
base.OnElementContextUninitialized(context);
65+
}
66+
67+
protected override void OnPropertyChanged(AttachedShadowElementContext context, DependencyProperty property, object oldValue, object newValue)
68+
{
69+
if (property == CornerRadiusProperty)
70+
{
71+
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
72+
if (geometry != null)
73+
{
74+
geometry.CornerRadius = new Vector2((float)(double)newValue);
75+
}
76+
77+
UpdateShadowClip(context);
78+
}
79+
else
80+
{
81+
base.OnPropertyChanged(context, property, oldValue, newValue);
82+
}
83+
}
84+
85+
protected override CompositionBrush GetShadowMask(AttachedShadowElementContext context)
86+
{
87+
if (!SupportsCompositionVisualSurface)
88+
{
89+
return null;
90+
}
91+
92+
// Create rounded rectangle geometry and add it to a shape
93+
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey) ?? context.AddResource(
94+
RoundedRectangleGeometryResourceKey,
95+
context.Compositor.CreateRoundedRectangleGeometry());
96+
geometry.CornerRadius = new Vector2((float)CornerRadius);
97+
98+
var shape = context.GetResource(ShapeResourceKey) ?? context.AddResource(ShapeResourceKey, context.Compositor.CreateSpriteShape(geometry));
99+
shape.FillBrush = context.Compositor.CreateColorBrush(Colors.Black);
100+
101+
// Create a ShapeVisual so that our geometry can be rendered to a visual
102+
var shapeVisual = context.GetResource(ShapeVisualResourceKey) ??
103+
context.AddResource(ShapeVisualResourceKey, context.Compositor.CreateShapeVisual());
104+
shapeVisual.Shapes.Add(shape);
105+
106+
// Create a CompositionVisualSurface, which renders our ShapeVisual to a texture
107+
var visualSurface = context.GetResource(VisualSurfaceResourceKey) ??
108+
context.AddResource(VisualSurfaceResourceKey, context.Compositor.CreateVisualSurface());
109+
visualSurface.SourceVisual = shapeVisual;
110+
111+
// Create a CompositionSurfaceBrush to render our CompositionVisualSurface to a brush.
112+
// Now we have a rounded rectangle brush that can be used on as the mask for our shadow.
113+
var surfaceBrush = context.GetResource(SurfaceBrushResourceKey) ?? context.AddResource(
114+
SurfaceBrushResourceKey,
115+
context.Compositor.CreateSurfaceBrush(visualSurface));
116+
117+
geometry.Size = visualSurface.SourceSize = shapeVisual.Size = context.Element.RenderSize.ToVector2();
118+
119+
return surfaceBrush;
120+
}
121+
122+
protected override CompositionClip GetShadowClip(AttachedShadowElementContext context)
123+
{
124+
var pathGeom = context.GetResource(PathGeometryResourceKey) ??
125+
context.AddResource(PathGeometryResourceKey, context.Compositor.CreatePathGeometry());
126+
var clip = context.GetResource(ClipResourceKey) ?? context.AddResource(ClipResourceKey, context.Compositor.CreateGeometricClip(pathGeom));
127+
128+
// Create rounded rectangle geometry at a larger size that compensates for the size of the stroke,
129+
// as we want the inside edge of the stroke to match the edges of the element.
130+
// Additionally, the inside edge of the stroke will have a smaller radius than the radius we specified.
131+
// Using "(StrokeThickness / 2) + Radius" as our rectangle's radius will give us an inside stroke radius that matches the radius we want.
132+
var canvasRectangle = CanvasGeometry.CreateRoundedRectangle(
133+
null,
134+
-MaxBlurRadius / 2,
135+
-MaxBlurRadius / 2,
136+
(float)context.Element.ActualWidth + MaxBlurRadius,
137+
(float)context.Element.ActualHeight + MaxBlurRadius,
138+
(MaxBlurRadius / 2) + (float)CornerRadius,
139+
(MaxBlurRadius / 2) + (float)CornerRadius);
140+
141+
var canvasStroke = canvasRectangle.Stroke(MaxBlurRadius);
142+
143+
pathGeom.Path = new CompositionPath(canvasStroke);
144+
145+
return clip;
146+
}
147+
148+
protected override void OnSizeChanged(AttachedShadowElementContext context, Size newSize, Size previousSize)
149+
{
150+
var sizeAsVec2 = newSize.ToVector2();
151+
152+
var geometry = context.GetResource(RoundedRectangleGeometryResourceKey);
153+
if (geometry != null)
154+
{
155+
geometry.Size = sizeAsVec2;
156+
}
157+
158+
var visualSurface = context.GetResource(VisualSurfaceResourceKey);
159+
if (geometry != null)
160+
{
161+
visualSurface.SourceSize = sizeAsVec2;
162+
}
163+
164+
var shapeVisual = context.GetResource(ShapeVisualResourceKey);
165+
if (geometry != null)
166+
{
167+
shapeVisual.Size = sizeAsVec2;
168+
}
169+
170+
UpdateShadowClip(context);
171+
172+
base.OnSizeChanged(context, newSize, previousSize);
173+
}
174+
}
175+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using Microsoft.Graphics.Canvas.Effects;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Numerics;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Windows.UI.Composition;
9+
using Windows.UI.Xaml.Hosting;
10+
11+
namespace Microsoft.Toolkit.Uwp.UI.Media.Shadows
12+
{
13+
/// <summary>
14+
/// A base class for attached shadows that use an opacity mask to clip content from the shadow.
15+
/// </summary>
16+
public abstract class AttachedShadowBaseWithOpacityMask : AttachedShadowBase
17+
{
18+
private const string AlphaMaskSourceKey = "AttachedShadowAlphaMask";
19+
private const string SpriteVisualSourceKey = "AttachedShadowSpriteVisual";
20+
21+
private static readonly TypedResourceKey<CompositionEffectBrush> OpacityMaskEffectBrushResourceKey = "AttachedShadowSpriteVisualEffectBrush";
22+
private static readonly TypedResourceKey<CompositionBrush> OpacityMaskResourceKey = "AttachedShadowSpriteVisualOpacityMask";
23+
private static readonly TypedResourceKey<SpriteVisual> OpacityMaskVisualResourceKey = "AttachedShadowSpriteVisualOpacityMaskVisual";
24+
private static readonly TypedResourceKey<CompositionVisualSurface> OpacityMaskVisualSurfaceResourceKey = "AttachedShadowSpriteVisualOpacityMaskSurface";
25+
private static readonly TypedResourceKey<CompositionSurfaceBrush> OpacityMaskSurfaceBrushResourceKey =
26+
"AttachedShadowSpriteVisualOpacityMaskSurfaceBrush";
27+
private static readonly TypedResourceKey<AlphaMaskEffect> AlphaMaskEffectResourceKey = "AttachedShadowSpriteVisualAlphaMaskEffect";
28+
29+
/// <summary>
30+
/// Update the opacity mask for the shadow's <see cref="SpriteVisual"/>.
31+
/// </summary>
32+
/// <param name="context">The <see cref="AttachedShadowElementContext"/> this operation will be performed on.</param>
33+
protected void UpdateVisualOpacityMask(AttachedShadowElementContext context)
34+
{
35+
if (!context.IsInitialized)
36+
{
37+
return;
38+
}
39+
40+
var brush = GetisualOpacityMask(context);
41+
if (brush != null)
42+
{
43+
context.AddResource(OpacityMaskResourceKey, brush);
44+
}
45+
else
46+
{
47+
context.RemoveResource(OpacityMaskResourceKey)?.Dispose();
48+
}
49+
}
50+
51+
/// <summary>
52+
/// Override and return a <see cref="CompositionBrush"/> that serves as an opacity mask for the shadow's <see cref="SpriteVisual"/>
53+
/// </summary>
54+
protected abstract CompositionBrush GetisualOpacityMask(AttachedShadowElementContext context);
55+
56+
protected override void OnElementContextInitialized(AttachedShadowElementContext context)
57+
{
58+
UpdateVisualOpacityMask(context);
59+
base.OnElementContextInitialized(context);
60+
}
61+
62+
protected override void OnElementContextUninitialized(AttachedShadowElementContext context)
63+
{
64+
context.RemoveAndDisposeResource(OpacityMaskResourceKey);
65+
context.RemoveAndDisposeResource(OpacityMaskVisualResourceKey);
66+
context.RemoveAndDisposeResource(OpacityMaskVisualSurfaceResourceKey);
67+
context.RemoveAndDisposeResource(OpacityMaskSurfaceBrushResourceKey);
68+
context.RemoveAndDisposeResource(OpacityMaskEffectBrushResourceKey);
69+
context.RemoveAndDisposeResource(AlphaMaskEffectResourceKey);
70+
base.OnElementContextUninitialized(context);
71+
}
72+
73+
protected override void SetElementChildVisual(AttachedShadowElementContext context)
74+
{
75+
if (context.TryGetResource(OpacityMaskResourceKey, out var opacityMask))
76+
{
77+
var visualSurface = context.GetResource(OpacityMaskVisualSurfaceResourceKey) ?? context.AddResource(
78+
OpacityMaskVisualSurfaceResourceKey,
79+
context.Compositor.CreateVisualSurface());
80+
visualSurface.SourceVisual = context.SpriteVisual;
81+
visualSurface.StartAnimation(nameof(visualSurface.SourceSize), context.Compositor.CreateExpressionAnimation("this.SourceVisual.Size"));
82+
83+
var surfaceBrush = context.GetResource(OpacityMaskSurfaceBrushResourceKey) ?? context.AddResource(
84+
OpacityMaskSurfaceBrushResourceKey,
85+
context.Compositor.CreateSurfaceBrush(visualSurface));
86+
var alphaMask = context.GetResource(AlphaMaskEffectResourceKey) ?? context.AddResource(AlphaMaskEffectResourceKey, new AlphaMaskEffect());
87+
alphaMask.Source = new CompositionEffectSourceParameter(SpriteVisualSourceKey);
88+
alphaMask.AlphaMask = new CompositionEffectSourceParameter(AlphaMaskSourceKey);
89+
90+
using (var factory = context.Compositor.CreateEffectFactory(alphaMask))
91+
{
92+
context.RemoveResource(OpacityMaskEffectBrushResourceKey)?.Dispose();
93+
var brush = context.AddResource(OpacityMaskEffectBrushResourceKey, factory.CreateBrush());
94+
brush.SetSourceParameter(SpriteVisualSourceKey, surfaceBrush);
95+
brush.SetSourceParameter(AlphaMaskSourceKey, opacityMask);
96+
97+
var visual = context.GetResource(OpacityMaskVisualResourceKey) ?? context.AddResource(
98+
OpacityMaskVisualResourceKey,
99+
context.Compositor.CreateSpriteVisual());
100+
visual.RelativeSizeAdjustment = Vector2.One;
101+
visual.Brush = brush;
102+
ElementCompositionPreview.SetElementChildVisual(context.Element, visual);
103+
}
104+
}
105+
else
106+
{
107+
base.SetElementChildVisual(context);
108+
}
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)