RoundEffect reutilizable de Xamarin.Forms

Download SampleDescargar el ejemplo

Importante

Ya no es necesario usar RoundEffect para representar un control como un círculo. El último enfoque recomendado es recortar el control mediante EllipseGeometry. Para obtener más información, vea Recorte con una geometría.

RoundEffect simplifica la representación de cualquier control que se derive de VisualElement como un círculo. Este efecto se puede usar para crear imágenes circulares, botones u otros controles:

RoundEffect screenshots on iOS and Android

Creación de un objeto RoutingEffect compartido

Para crear un efecto multiplataforma se debe crear una clase de efecto en el proyecto compartido. La aplicación de ejemplo crea una clase RoundEffect vacía que se deriva de la clase RoutingEffect:

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

Esta clase permite al proyecto compartido resolver las referencias al efecto en código o XAML, pero no proporciona ninguna funcionalidad. El efecto debe tener implementaciones para cada plataforma.

Implementación del efecto de Android

El proyecto de la plataforma Android define una clase RoundEffect que se deriva de PlatformEffect. Esta clase se etiqueta con atributos assembly que permiten que Xamarin.Forms resuelva la clase de efecto:

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

En la plataforma Android se usa el concepto de OutlineProvider para definir los bordes de un control. En el proyecto de ejemplo se incluye una clase CornerRadiusProvider que se deriva de la clase ViewOutlineProvider:

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);
    }
}

Esta clase usa las propiedades Width y Height de la instancia Xamarin.FormsElement para calcular un radio que es la mitad de la dimensión más corta.

Una vez que se ha definido un proveedor de esquemas, la clase RoundEffect puede consumirlo para implementar el efecto:

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;
        }
    }
}

Se llama al método OnAttached cuando el efecto se adjunta a un elemento. El objeto OutlineProvider existente se guarda para que se pueda restaurar cuando se desasocie el efecto. Se usa una nueva instancia de CornerRadiusOutlineProvider como OutlineProvider y ClipToOutline se establece en true para recortar los elementos que se desbordan en los bordes del contorno.

Se llama al método OnDetatched cuando el efecto se quita de un elemento y restaura el valor de OutlineProvider original.

Nota:

En función del tipo de elemento, la propiedad Control puede ser NULL o no. Si la propiedad Control no es NULL, las esquinas redondeadas se pueden aplicar directamente al control. Pero si es NULL, las esquinas redondeadas se deben aplicar al objeto Container. El campo effectTarget permite aplicar el efecto al objeto adecuado.

Implementación del efecto de iOS

El proyecto de la plataforma iOS define una clase RoundEffect que se deriva de PlatformEffect. Esta clase se etiqueta con atributos assembly que permiten que Xamarin.Forms resuelva la clase de efecto:

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

En iOS, los controles tienen una propiedad Layer, que tiene una propiedad CornerRadius. La implementación de la clase RoundEffect en iOS calcula el radio de redondeo adecuado y actualiza la propiedad CornerRadius de la capa:

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;
    }
}

El método CalculateRadius calcula un radio en función en la dimensión mínima de Xamarin.FormsElement. Se llama al método OnAttached cuando el efecto se adjunta a un control y actualiza la propiedad CornerRadius de la capa. Establece la propiedad ClipToBounds en true de modo que el desbordamiento de los elementos se recorta a los bordes del control. Se llama al método OnDetatched cuando el efecto se quita de un control e invierte estos cambios, lo que restaura el radio de esquina original.

Nota:

En función del tipo de elemento, la propiedad Control puede ser NULL o no. Si la propiedad Control no es NULL, las esquinas redondeadas se pueden aplicar directamente al control. Pero si es NULL, las esquinas redondeadas se deben aplicar al objeto Container. El campo effectTarget permite aplicar el efecto al objeto adecuado.

Consumo del efecto

Una vez que el efecto se ha implementado entre plataformas, los controles de Xamarin.Forms pueden usarlo. Una aplicación común de RoundEffect consiste en convertir un objeto Image en circular. En el código XAML siguiente se muestra el efecto aplicado a una instancia de Image:

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

El efecto también se puede aplicar en el código:

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

La clase RoundEffect se puede aplicar a cualquier control que se derive de VisualElement.

Nota:

Para que el efecto calcule el radio correcto, el control al que se aplica debe tener un tamaño explícito. Por tanto, se deben definir las propiedades HeightRequest y WidthRequest. Si el control afectado aparece en un elemento StackLayout, su propiedad HorizontalOptions no debe usar uno de los valores Expand como LayoutOptions.CenterAndExpand, o no tendrá dimensiones precisas.