diff --git a/samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml b/samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml
new file mode 100644
index 000000000..608cc9875
--- /dev/null
+++ b/samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml.cs b/samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml.cs
new file mode 100644
index 000000000..c3f683297
--- /dev/null
+++ b/samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml.cs
@@ -0,0 +1,11 @@
+
+namespace Xamarin.CommunityToolkit.Sample.Pages.Effects
+{
+ public partial class ShadowEffectPage
+ {
+ public ShadowEffectPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs
index e073a6087..8cd073b5b 100644
--- a/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs
+++ b/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs
@@ -33,6 +33,11 @@ public class EffectsGalleryViewModel : BaseGalleryViewModel
typeof(TouchEffectPage),
nameof(TouchEffect),
"The TouchEffect is an effect that allows changing the view's appearance depending on the touch state (normal, pressed, hovered). Also, it allows to handle long presses."),
+
+ new SectionModel(
+ typeof(ShadowEffectPage),
+ nameof(ShadowEffect),
+ "The ShadowEffect allows all views to display shadow."),
};
}
}
\ No newline at end of file
diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs
index 9b88e2b10..ab6728440 100644
--- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs
+++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs
@@ -40,5 +40,10 @@ sealed class EffectIds
/// Effect Id for
///
public static string TouchEffect => $"{effectResolutionGroupName}.{nameof(TouchEffect)}";
+
+ ///
+ /// Effect Id for
+ ///
+ public static string ShadowEffect => $"{effectResolutionGroupName}.{nameof(ShadowEffect)}";
}
}
\ No newline at end of file
diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs
new file mode 100644
index 000000000..385572c2e
--- /dev/null
+++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs
@@ -0,0 +1,90 @@
+using Xamarin.Forms.Platform.Android;
+using Xamarin.Forms;
+using Android.Views;
+using AView = Android.Views.View;
+using Android.OS;
+using System.ComponentModel;
+using Xamarin.CommunityToolkit.Effects;
+using Xamarin.CommunityToolkit.Android.Effects;
+using Android.Widget;
+
+[assembly: ExportEffect(typeof(PlatformShadowEffect), nameof(ShadowEffect))]
+
+namespace Xamarin.CommunityToolkit.Android.Effects
+{
+ public class PlatformShadowEffect : PlatformEffect
+ {
+ const float defaultRadius = 10f;
+
+ const float defaultOpacity = 1f;
+
+ AView View => Control ?? Container;
+
+ protected override void OnAttached()
+ => Update();
+
+ protected override void OnDetached()
+ {
+ if (View == null)
+ return;
+
+ View.Elevation = 0;
+ }
+
+ protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
+ {
+ base.OnElementPropertyChanged(args);
+
+ if (View == null)
+ return;
+
+ switch (args.PropertyName)
+ {
+ case nameof(ShadowEffect.ColorPropertyName):
+ case nameof(ShadowEffect.OpacityPropertyName):
+ case nameof(ShadowEffect.RadiusPropertyName):
+ case nameof(ShadowEffect.OffsetXPropertyName):
+ case nameof(ShadowEffect.OffsetYPropertyName):
+ View.Invalidate();
+ Update();
+ break;
+ }
+ }
+
+ void Update()
+ {
+ if (View == null || Build.VERSION.SdkInt < BuildVersionCodes.Lollipop)
+ return;
+
+ var radius = (float)ShadowEffect.GetRadius(Element);
+ if (radius < 0)
+ radius = defaultRadius;
+
+ var opacity = ShadowEffect.GetOpacity(Element);
+ if (opacity < 0)
+ opacity = defaultOpacity;
+
+ var androidColor = ShadowEffect.GetColor(Element).MultiplyAlpha(opacity).ToAndroid();
+
+ if (View is TextView textView)
+ {
+ var offsetX = (float)ShadowEffect.GetOffsetX(Element);
+ var offsetY = (float)ShadowEffect.GetOffsetY(Element);
+ textView.SetShadowLayer(radius, offsetX, offsetY, androidColor);
+ return;
+ }
+
+ View.OutlineProvider = (Element as VisualElement)?.BackgroundColor.A > 0
+ ? ViewOutlineProvider.PaddedBounds
+ : ViewOutlineProvider.Bounds;
+
+ View.Elevation = View.Context.ToPixels(radius);
+
+ if (Build.VERSION.SdkInt < BuildVersionCodes.P)
+ return;
+
+ View.SetOutlineAmbientShadowColor(androidColor);
+ View.SetOutlineSpotShadowColor(androidColor);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs
new file mode 100644
index 000000000..0f10cc7b1
--- /dev/null
+++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs
@@ -0,0 +1,99 @@
+using System;
+using System.ComponentModel;
+using CoreGraphics;
+using Xamarin.CommunityToolkit.Effects;
+using Xamarin.Forms;
+
+#if __IOS__
+using NativeView = UIKit.UIView;
+using Xamarin.Forms.Platform.iOS;
+using Xamarin.CommunityToolkit.iOS.Effects;
+#elif __MACOS__
+using NativeView = AppKit.NSView;
+using Xamarin.Forms.Platform.MacOS;
+using Xamarin.CommunityToolkit.macOS.Effects;
+#endif
+
+[assembly: ExportEffect(typeof(PlatformShadowEffect), nameof(ShadowEffect))]
+
+#if __IOS__
+namespace Xamarin.CommunityToolkit.iOS.Effects
+#elif __MACOS__
+namespace Xamarin.CommunityToolkit.macOS.Effects
+#endif
+{
+ public class PlatformShadowEffect : PlatformEffect
+ {
+ const float defaultRadius = 10f;
+
+ const float defaultOpacity = .5f;
+
+ NativeView View => Control ?? Container;
+
+ protected override void OnAttached()
+ {
+ if (View == null)
+ return;
+
+ UpdateColor();
+ UpdateOpacity();
+ UpdateRadius();
+ UpdateOffset();
+ }
+
+ protected override void OnDetached()
+ {
+ if (View?.Layer == null)
+ return;
+
+ View.Layer.ShadowOpacity = 0;
+ }
+
+ protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
+ {
+ base.OnElementPropertyChanged(args);
+
+ if (View == null)
+ return;
+
+ switch (args.PropertyName)
+ {
+ case nameof(ShadowEffect.ColorPropertyName):
+ UpdateColor();
+ break;
+ case nameof(ShadowEffect.OpacityPropertyName):
+ UpdateOpacity();
+ break;
+ case nameof(ShadowEffect.RadiusPropertyName):
+ UpdateRadius();
+ break;
+ case nameof(ShadowEffect.OffsetXPropertyName):
+ case nameof(ShadowEffect.OffsetYPropertyName):
+ UpdateOffset();
+ break;
+ }
+ }
+
+ void UpdateColor()
+ => View.Layer.ShadowColor = ShadowEffect.GetColor(Element).ToCGColor();
+
+ void UpdateOpacity()
+ {
+ var opacity = (float)ShadowEffect.GetOpacity(Element);
+ View.Layer.ShadowOpacity = opacity < 0
+ ? defaultOpacity
+ : opacity;
+ }
+
+ void UpdateRadius()
+ {
+ var radius = (nfloat)ShadowEffect.GetRadius(Element);
+ View.Layer.ShadowRadius = radius < 0
+ ? defaultRadius
+ : radius;
+ }
+
+ void UpdateOffset()
+ => View.Layer.ShadowOffset = new CGSize((double)ShadowEffect.GetOffsetX(Element), (double)ShadowEffect.GetOffsetY(Element));
+ }
+}
diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs
new file mode 100644
index 000000000..0d5761122
--- /dev/null
+++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs
@@ -0,0 +1,117 @@
+using System.Linq;
+using Xamarin.Forms;
+
+namespace Xamarin.CommunityToolkit.Effects
+{
+ public class ShadowEffect : RoutingEffect
+ {
+ internal const string ColorPropertyName = "Color";
+
+ internal const string OpacityPropertyName = "Opacity";
+
+ internal const string RadiusPropertyName = "Radius";
+
+ internal const string OffsetXPropertyName = "OffsetX";
+
+ internal const string OffsetYPropertyName = "OffsetY";
+
+ public static readonly BindableProperty ColorProperty = BindableProperty.CreateAttached(
+ ColorPropertyName,
+ typeof(Color),
+ typeof(ShadowEffect),
+ Color.Default,
+ propertyChanged: TryGenerateEffect);
+
+ public static readonly BindableProperty OpacityProperty = BindableProperty.CreateAttached(
+ OpacityPropertyName,
+ typeof(double),
+ typeof(ShadowEffect),
+ -1.0,
+ propertyChanged: TryGenerateEffect);
+
+ public static readonly BindableProperty RadiusProperty = BindableProperty.CreateAttached(
+ RadiusPropertyName,
+ typeof(double),
+ typeof(ShadowEffect),
+ -1.0,
+ propertyChanged: TryGenerateEffect);
+
+ public static readonly BindableProperty OffsetXProperty = BindableProperty.CreateAttached(
+ OffsetXPropertyName,
+ typeof(double),
+ typeof(ShadowEffect),
+ .0,
+ propertyChanged: TryGenerateEffect);
+
+ public static readonly BindableProperty OffsetYProperty = BindableProperty.CreateAttached(
+ OffsetYPropertyName,
+ typeof(double),
+ typeof(ShadowEffect),
+ .0,
+ propertyChanged: TryGenerateEffect);
+
+ public ShadowEffect()
+ : base(EffectIds.ShadowEffect)
+ {
+#if __ANDROID__
+ if (System.DateTime.Now.Ticks < 0)
+ _ = new Xamarin.CommunityToolkit.Android.Effects.PlatformShadowEffect();
+#elif __IOS__
+ if (System.DateTime.Now.Ticks < 0)
+ _ = new Xamarin.CommunityToolkit.iOS.Effects.PlatformShadowEffect();
+#elif __MACOS__
+ if (System.DateTime.Now.Ticks < 0)
+ _ = new Xamarin.CommunityToolkit.macOS.Effects.PlatformShadowEffect();
+#endif
+ }
+
+ public static Color GetColor(BindableObject bindable)
+ => (Color)bindable.GetValue(ColorProperty);
+
+ public static void SetColor(BindableObject bindable, Color value)
+ => bindable.SetValue(ColorProperty, value);
+
+ public static double GetOpacity(BindableObject bindable)
+ => (double)bindable.GetValue(OpacityProperty);
+
+ public static void SetOpacity(BindableObject bindable, double value)
+ => bindable.SetValue(OpacityProperty, value);
+
+ public static double GetRadius(BindableObject bindable)
+ => (double)bindable.GetValue(RadiusProperty);
+
+ public static void SetRadius(BindableObject bindable, double value)
+ => bindable.SetValue(RadiusProperty, value);
+
+ public static double GetOffsetX(BindableObject bindable)
+ => (double)bindable.GetValue(OffsetXProperty);
+
+ public static void SetOffsetX(BindableObject bindable, double value)
+ => bindable.SetValue(OffsetXProperty, value);
+
+ public static double GetOffsetY(BindableObject bindable)
+ => (double)bindable.GetValue(OffsetYProperty);
+
+ public static void SetOffsetY(BindableObject bindable, double value)
+ => bindable.SetValue(OffsetYProperty, value);
+
+ static void TryGenerateEffect(BindableObject bindable, object oldValue, object newValue)
+ {
+ if (!(bindable is VisualElement view))
+ return;
+
+ var shadowEffects = view.Effects.OfType();
+
+ if (GetColor(view) == Color.Default)
+ {
+ foreach (var effect in shadowEffects.ToArray())
+ view.Effects.Remove(effect);
+
+ return;
+ }
+
+ if (!shadowEffects.Any())
+ view.Effects.Add(new ShadowEffect());
+ }
+ }
+}
\ No newline at end of file