Shadows for Xamarin.Forms components creators
Get it from Github and Nuget:
https://github.com/roubachof/Sharpnado.Shadows | |
Sharpnado.Shadows
has been architectured with modularity in mind.
The goal is to make it easy to integrate into others Xamarin.Forms
components.
In Shadows
, each platform renderers relies on Controllers
(iOS and UWP) or View
(Android) that are doing all the job. There is very little code in each renderer. Basically they are just collecting property values updates and passing them to the controllers.
On the pure Xamarin.Forms
side, Shades
are decoupled from the Shadows
component and then can be reused by others components.
Here is a basic pseudo-code of how you could reuse Shades
in your own Foo
component:
-- Xamarin.Forms Side
class FooView
{
IEnumerable<Shade> Shades;
void HandleShadesBindingContext()
{
Inherit from FooView BindingContext;
}
}
-- Renderer Side
class FooViewRenderer : VisualElementRenderer<FooView>
{
void CreateShadowController()
{
_shadowsController = new ShadowController(shadowSource);
}
override void Dispose()
{
_shadowsController?.Dispose();
_shadowsController = null;
}
override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Element.Shades):
_shadowsController?.UpdateShades(Element.Shades);
break;
}
}
override void Layout()
{
_shadowsController.Layout();
}
}
Shades UI layout
Shadows
layout is basic. You need to put the Shades
view, behind your shadow source.
This is why Shadows
renderers are based on simple containers allowing view stacking:
FrameLayout
on AndroidUIView
on iOSGrid
on UWP
The shadows are rendered thanks to a simple View
on Android
.
We use CALayer
on iOS
, and SpriteVisual
on UWP
.
But let's dig a little bit deeper.
Xamarin.Forms Shades
If you look at Shadows
implementation, you will realize that it is really just in fact a simple container of Shades with a CornerRadius
property. The only dull thing to take care of is the BindingContext
inheritance:
public class Shadows : ContentView
{
public static readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(
nameof(CornerRadius),
typeof(int),
typeof(Shadows),
DefaultCornerRadius);
public static readonly BindableProperty ShadesProperty = BindableProperty.Create(
nameof(Shades),
typeof(IEnumerable<Shade>),
typeof(Shadows),
defaultValueCreator: (bo) => new ObservableCollection<Shade> { new Shade() },
validateValue: (bo, v) =>
{
var shades = (IEnumerable<Shade>)v;
return shades != null;
});
private const int DefaultCornerRadius = 0;
public int CornerRadius
{
get => (int)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public IEnumerable<Shade> Shades
{
get => (IEnumerable<Shade>)GetValue(ShadesProperty);
set => SetValue(ShadesProperty, value);
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
foreach (var shade in Shades)
{
SetInheritedBindingContext(shade, BindingContext);
}
}
private static void ShadesPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
var shadows = (Shadows)bindable;
if (oldvalue != null)
{
if (oldvalue is INotifyCollectionChanged oldCollection)
{
oldCollection.CollectionChanged -= shadows.OnShadeCollectionChanged;
}
foreach (var shade in (IEnumerable<Shade>)oldvalue)
{
shade.Parent = null;
shade.BindingContext = null;
}
}
foreach (var shade in (IEnumerable<Shade>)newvalue)
{
shade.Parent = shadows;
SetInheritedBindingContext(shade, shadows.BindingContext);
}
if (newvalue is INotifyCollectionChanged newCollection)
{
newCollection.CollectionChanged += shadows.OnShadeCollectionChanged;
}
}
private void OnShadeCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Shade newShade in e.NewItems)
{
newShade.Parent = this;
SetInheritedBindingContext(newShade, BindingContext);
}
break;
case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (Shade oldShade in e.OldItems ?? new Shade[0])
{
oldShade.Parent = null;
oldShade.BindingContext = null;
}
break;
}
}
}
The good news is that you can just copy-paste the BindingContext
management code it should work as is.
Platform renderers
In your component platform renderers, all the work will be done by the ShadowView
(Android) or the Controllers
(iOS and UWP).
Your renderer is in fact just responsible of 4 things:
- Creating the
Controller
- Calling
Layout
method on yourController
- Dispatching
Shades
updates to yourController
- Disposing your
Controller
Android ShadowView
If we look at the Shadows
renderer we can see clearly those 3 steps.
1. Creating the ShadowView
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
switch (e.PropertyName)
{
case "Renderer":
var content = GetChildAt(0);
if (content == null)
{
return;
}
if (_shadowView == null)
{
_shadowView = new ShadowView(Context, content, Context.ToPixels(Element.CornerRadius));
_shadowView.UpdateShades(Element.Shades);
AddView(_shadowView, 0);
}
break;
...
}
}
Here we need to wait that our children view is created and added to our Android
view, then we can create our ShadowView
.
2. Calling Layout
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
var children = GetChildAt(1);
if (children == null)
{
return;
}
_shadowView?.Layout(MeasuredWidth, MeasuredHeight);
}
3. Dispatching Shades
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
switch (e.PropertyName)
{
...
case nameof(Element.CornerRadius):
_shadowView.UpdateCornerRadius(Context.ToPixels(Element.CornerRadius));
break;
case nameof(Element.Shades):
_shadowView.UpdateShades(Element.Shades);
break;
}
}
4. Disposing
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_shadowView?.Dispose();
}
}
And that's about it.
All the Bitmap
creation, caching, all the Shade
collection changed events, each Shade
properties changes, are handled by the ShadoView
iOSShadowsController
On iOS
, a controller is doing all the job. The shadows are rendered thanks to CALayers
. There is a 1 to 1 relationship between a Shade
and a CALayer
. Consistency is the iOSShadowsController
job. It needs the shadow source and the layer that we be the parent of all our shadows.
1. CreateShadowController
protected override void OnElementChanged(ElementChangedEventArgs<Shadows> e)
{
base.OnElementChanged(e);
...
if (_shadowsController == null && Subviews.Length > 0)
{
CreateShadowController(Subviews[0], e.NewElement);
}
}
private void CreateShadowController(UIView shadowSource, Shadows formsElement)
{
Layer.BackgroundColor = new CGColor(0, 0, 0, 0);
Layer.MasksToBounds = false;
_shadowsLayer = new CALayer { MasksToBounds = false };
Layer.InsertSublayer(_shadowsLayer, 0);
_shadowsController = new iOSShadowsController(shadowSource, _shadowsLayer, formsElement.CornerRadius);
_shadowsController.UpdateShades(formsElement.Shades);
}
2. Calling Layout
public override void LayoutSublayersOfLayer(CALayer layer)
{
base.LayoutSublayersOfLayer(layer);
_shadowsController?.OnLayoutSubLayers();
}
3. Dispatching Shades
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
switch (e.PropertyName)
{
case nameof(Element.CornerRadius):
_shadowsController?.UpdateCornerRadius(Element.CornerRadius);
break;
case nameof(Element.Shades):
_shadowsController?.UpdateShades(Element.Shades);
break;
}
}
4. Disposing
protected override void OnElementChanged(ElementChangedEventArgs<Shadows> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
{
_shadowsController?.Dispose();
_shadowsController = null;
_shadowsLayer.Dispose();
_shadowsLayer = null;
return;
}
...
}
UWPShadowsRenderer
UWPShadowsController
is the simplest to integrate cause the UWP
platform handles the layout of your shadows.
First you need to disable AutoPackage
or it will create the layout for you.
We use a Canvas
as the host of our shadows.
1. UWPShadowsController
protected override void OnElementChanged(ElementChangedEventArgs<Shadows> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
{
return;
}
if (Control == null)
{
SetNativeControl(new Grid());
}
PackChild();
}
private void PackChild()
{
if (Element.Content == null)
{
return;
}
IVisualElementRenderer renderer = Element.Content.GetOrCreateRenderer();
FrameworkElement frameworkElement = renderer.ContainerElement;
_shadowsCanvas = new Canvas();
Control.Children.Add(_shadowsCanvas);
Control.Children.Add(frameworkElement);
_shadowsController = new UWPShadowsController(_shadowsCanvas, frameworkElement, Element.CornerRadius);
_shadowsController.UpdateShades(Element.Shades);
}
2. Dispatching Shades
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(Element.CornerRadius):
_shadowsController?.UpdateCornerRadius(Element.CornerRadius);
break;
case nameof(Element.Shades):
_shadowsController?.UpdateShades(Element.Shades);
break;
}
}
3. Disposing
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_shadowsController?.Dispose();
_shadowsController = null;
}
}
Installing
Just add Sharpnado.Shadows
to all your projects and you will have access to all the Controllers
and Views
.