Shadows for Xamarin.Forms components creators

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:

  1. FrameLayout on Android
  2. UIView on iOS
  3. Grid 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:

  1. Creating the Controller
  2. Calling Layout method on your Controller
  3. Dispatching Shades updates to your Controller
  4. 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.