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