Iris Classon
Iris Classon - In Love with Code

Xamarin Android and Shadows for Irregular Transparent Images

I had barely even touched Xamarin before I started at Plejd, and it’s been an interesting journey. It’s like a flash from the past, when I was working with Windows Phone and Windows Store Apps, albeit with other challenges.

Today’s challenge was shadows. On Android.

When displaying a device, we want to have a slight drop shadow. Easy, right? Nope.

There are a few ways you can create shadows on with Xamarin. Elevation and SetShadowLayer. While they seem to do the same thing, there are some significant differences between the two.

Elevation This property is available on Android API level 21 (Android 5.0 Lollipop) and higher. I believe it’s a part of the Material Design. It creates a guidelines and is used to create shadows and give a visual indication of the Z-axis depth of a view. https://m2.material.io/design/environment/elevation.html

Elevation works with the Android framework, which automatically generates the shadow based on the elevation value. It’s usually used with views that inherit from View and supports dynamic shadows.

Pros: Easy to use and automatically handles shadow drawing. More consistent with Material Design guidelines. Works with hardware acceleration.

Cons: Only available on Android API level 21 and higher. Limited customization for shadow appearance.

SetShadowLayer

SetShadowLayer is a method available in the Paint class. It’s used to draw custom shadows by providing parameters such as radius, dx, dy, and color. This gives us more control, but more work. You must handle the drawing yourself. Unfortunately this method requires disabling hardware acceleration by setting the layer type to software (using SetLayerType(Android.Views.LayerType.Software, null)).

Pros: Customizable shadow appearance. Works on lower API levels (below API level 21).

Cons: Requires handling the shadow drawing yourself. Requires disabling hardware acceleration.

Great. I just have to pick one, or switch between the two depending on the API level by using: Build.VERSION.SdkInt

And Elevate would be preferred for performance reasons.

It’s important to note that Elevate requires a background. And that background cannot be transparent. This means that I ran into a problem with our images, which cannot have an opaque background and the devices pictured in the images have irregular outlines. We want the shadow to follow the outline of the devices shown in our images, and Elevate won’t do that out of the box.

The first solution I tried was to traverse the bitmap pixels, find those that were opaque (not transparent) and create a Path based on those. The path was then used as an outline in the custom renderer, before setting Elevate.

The code, summarized, looked like this: A custom renderer that sets ouline and elevates And a class that does the outlining

public class ShadowEffect : PlatformEffect
{
    /// <inheritdoc/>
    protected override void OnAttached() => SetElevation();

    /// <inheritdoc/>
    protected override void OnDetached() { }

    /// <inheritdoc/>
    protected override void OnElementPropertyChanged(PropertyChangedEventArgs args)
    {
        if (this.Control == null)
            return;
        SetElevation();
    }
    
    /// <summary>
    /// Sets the correct elevation of the view.
    /// </summary>
    private void SetElevation()
    {
        try
        {
            if (Control is ImageView imageView)
            {
                var visualElement = Element as VisualElement;
                var elevation = visualElement?.IsEnabled == true ? 30f : 0.0f;
                imageView.OutlineProvider = new CustomImageViewOutlineProvider();
                imageView.SetElevation(elevation);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Cannot set property on attached control. Error: ", ex.Message);
        }
    }
}

Don’t use this code as is, this is a very simplistic version!

And the outline:

public class CustomImageViewOutlineProvider : ViewOutlineProvider
{
    public override void GetOutline(Android.Views.View view, Outline outline)
    {
        if (!(view is ImageView imageView)|| imageView.Height == 0 || imageView.Width == 0 || imageView.Drawable is null) return;
        
        var drawable = imageView.Drawable;
        
        var bitmap = ((BitmapDrawable)drawable).Bitmap;

        if (bitmap is null) return;

        var path = new Path();

        for (var y = 0; y < bitmap.Height; y++)
        {
            for (var x = 0; x < bitmap.Width; x++)
            {
                var pixel = bitmap.GetPixel(x, y);
                if (pixel == 0) continue;
                if (Path.Direction.Ccw != null) path.AddRect(x, y, x + 1, y + 1, Path.Direction.Ccw);
            }
        }
        if (Build.VERSION.SdkInt >= BuildVersionCodes.Q)
        {
            // Use SetPath for API level 29 and above
            outline.SetPath(path);
        }
        else
        {
            // Use SetConvexPath for API level 28 and below
#pragma warning disable CS0618
            outline.SetConvexPath(path);
#pragma warning restore CS0618
        }

    }
}

But oh boy was this slow. More than a minute.

I’ll be back with either a better solution, or a performance refactor.

Comments

Leave a comment below, or by email.

Last modified on 2023-03-16

comments powered by Disqus