Xamarin.Forms Reusable RoundEffect

Important

It's no longer necessary to use a RoundEffect to render a control as a circle. The latest recommended approach is to clip the control using an EllipseGeometry. For more information, see Clip with a Geometry.

The RoundEffect simplifies rendering any control that derives from VisualElement as a circle. This effect can be used to create circular images, buttons, and other controls:

RoundEffect screenshots on iOS and Android

Create a shared RoutingEffect

An effect class must be created in the shared project to create a cross-platform effect. The sample application creates an empty RoundEffect class that derives from the RoutingEffect class:

public class RoundEffect : RoutingEffect
{
    public RoundEffect() : base($"Xamarin.{nameof(RoundEffect)}")
    {
    }
}

This class allows the shared project to resolve the references to the effect in code or XAML but does not provide any functionality. The effect must have implementations for each platform.

Implement the Android effect

The Android platform project defines a RoundEffect class that derives from PlatformEffect. This class is tagged with assembly attributes that allow Xamarin.Forms to resolve the effect class:

[assembly: ResolutionGroupName("Xamarin")]
[assembly: ExportEffect(typeof(RoundEffectDemo.Droid.RoundEffect), nameof(RoundEffectDemo.Droid.RoundEffect))]
namespace RoundEffectDemo.Droid
{
    public class RoundEffect : PlatformEffect
    {
        // ...
    }
}

The Android platform uses the concept of an OutlineProvider to define the edges of a control. The sample project includes a CornerRadiusProvider class that derives from the ViewOutlineProvider class:

class CornerRadiusOutlineProvider : ViewOutlineProvider
{
    Element element;

    public CornerRadiusOutlineProvider(Element formsElement)
    {
        element = formsElement;
    }

    public override void GetOutline(Android.Views.View view, Outline outline)
    {
        float scale = view.Resources.DisplayMetrics.Density;
        double width = (double)element.GetValue(VisualElement.WidthProperty) * scale;
        double height = (double)element.GetValue(VisualElement.HeightProperty) * scale;
        float minDimension = (float)Math.Min(height, width);
        float radius = minDimension / 2f;
        Rect rect = new Rect(0, 0, (int)width, (int)height);
        outline.SetRoundRect(rect, radius);
    }
}

This class uses the Width and Height properties of the Xamarin.Forms Element instance to calculate a radius that is half of the shortest dimension.

Once an outline provider is defined the RoundEffect class can consume it to implement the effect:

public class RoundEffect : PlatformEffect
{
    ViewOutlineProvider originalProvider;
    Android.Views.View effectTarget;

    protected override void OnAttached()
    {
        try
        {
            effectTarget = Control ?? Container;
            originalProvider = effectTarget.OutlineProvider;
            effectTarget.OutlineProvider = new CornerRadiusOutlineProvider(Element);
            effectTarget.ClipToOutline = true;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to set corner radius: {ex.Message}");
        }
    }

    protected override void OnDetached()
    {
        if(effectTarget != null)
        {
            effectTarget.OutlineProvider = originalProvider;
            effectTarget.ClipToOutline = false;
        }
    }
}

The OnAttached method is called when the effect is attached to an element. The existing OutlineProvider object is saved so it can be restored when the effect is detached. A new instance of the CornerRadiusOutlineProvider is used as the OutlineProvider and ClipToOutline is set to true to clip overflowing elements to the outline borders.

The OnDetatched method is called when the effect is removed from an element and restores the original OutlineProvider value.

Note

Depending on the element type, the Control property may or may not be null. If the Control property is not null, the rounded corners can be applied directly to the control. However, if it is null the rounded corners must be applied to the Container object. The effectTarget field allows the effect to be applied to the appropriate object.

Implement the iOS effect

The iOS platform project defines a RoundEffect class that derives from PlatformEffect. This class is tagged with assembly attributes that allow Xamarin.Forms to resolve the effect class:

[assembly: ResolutionGroupName("Xamarin")]
[assembly: ExportEffect(typeof(RoundEffectDemo.iOS.RoundEffect), nameof(RoundEffectDemo.iOS.RoundEffect))]
namespace RoundEffectDemo.iOS
{
    public class RoundEffect : PlatformEffect
    {
        // ...
    }

On iOS, controls have a Layer property, which has a CornerRadius property. The RoundEffect class implementation on iOS calculates the appropriate corner radius and updates the layer's CornerRadius property:

public class RoundEffect : PlatformEffect
{
    nfloat originalRadius;
    UIKit.UIView effectTarget;

    protected override void OnAttached()
    {
        try
        {
            effectTarget = Control ?? Container;
            originalRadius = effectTarget.Layer.CornerRadius;
            effectTarget.ClipsToBounds = true;
            effectTarget.Layer.CornerRadius = CalculateRadius();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to set corner radius: {ex.Message}");
        }
    }

    protected override void OnDetached()
    {
        if (effectTarget != null)
        {
            effectTarget.ClipsToBounds = false;
            if (effectTarget.Layer != null)
            {
                effectTarget.Layer.CornerRadius = originalRadius;
            }
        }
    }

    float CalculateRadius()
    {
        double width = (double)Element.GetValue(VisualElement.WidthRequestProperty);
        double height = (double)Element.GetValue(VisualElement.HeightRequestProperty);
        float minDimension = (float)Math.Min(height, width);
        float radius = minDimension / 2f;

        return radius;
    }
}

The CalculateRadius method calculates a radius based on the minimum dimension of the Xamarin.Forms Element. The OnAttached method is called when the effect is attached to a control, and updates the layer's CornerRadius property. It sets the ClipToBounds property to true so overflowing elements are clipped to the borders of the control. The OnDetatched method is called when the effect is removed from a control and reverses these changes, restoring the original corner radius.

Note

Depending on the element type, the Control property may or may not be null. If the Control property is not null, the rounded corners can be applied directly to the control. However, if it is null the rounded corners must be applied to the Container object. The effectTarget field allows the effect to be applied to the appropriate object.

Consume the effect

Once the effect is implemented across platforms, it can be consumed by Xamarin.Forms controls. A common application of the RoundEffect is making an Image object circular. The following XAML shows the effect being applied to an Image instance:

<Image Source=outdoors"
       HeightRequest="100"
       WidthRequest="100">
    <Image.Effects>
        <local:RoundEffect />
    </Image.Effects>
</Image>

The effect can also be applied in code:

var image = new Image
{
    Source = ImageSource.FromFile("outdoors"),
    HeightRequest = 100,
    WidthRequest = 100
};
image.Effects.Add(new RoundEffect());

The RoundEffect class can be applied to any control that derives from VisualElement.

Note

For the effect to calculate the correct radius, the control it's applied to must have explicit sizing. Therefore, the HeightRequest and WidthRequest properties should be defined. If the affected control appears in a StackLayout, its HorizontalOptions property should not use one of the Expand values such as LayoutOptions.CenterAndExpand or it will not have accurate dimensions.