Customizing Frame corner radius with Xamarin.Forms Android and iOS
I've recently needed to customize Frame
corner radius to have just the top or bottom corners rounded, whereas the rest corners should stay rectangular. The problem is that CornerRadius
property on a Frame
element takes a float
as the argument, meaning that the value will apply to all of the corners. Fortunately, this can be quite easily changed with an Effect
or a CustomRenderer
.
NOTE: We will need to use CustomRenderer
because currently there's an issue with Effect
s for a Frame
element on Android - more details in this Xamarin.Forms github issue.
Shared code
We will start by creating a CustomFrame
control in our shared code
public class CustomFrame : Frame
{
public static new readonly BindableProperty CornerRadiusProperty = BindableProperty.Create(nameof(CustomFrame), typeof(CornerRadius), typeof(CustomDatePicker));
public CustomFrame()
{
// MK Clearing default values (e.g. on iOS it's 5)
base.CornerRadius = 0;
}
public new CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
}
In this example we're overloading the CornerRadius
property of the derived Frame
type (note the new
keyword) to make sure that we don't end up with two ways of setting the CornerRadius
- two different properties doing pretty much the same thing. Another thing to note in the above code is the fact that we're resetting the original CornerRadiu
property to avoid situations where the default value (e.g. iOS default value is 5) is set without our knowledge.
Having done that, we can write some XAML to utilize the above custom control.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:XamarinFormsPlayground.Controls;assembly=XamarinFormsPlayground"
x:Class="XamarinFormsPlayground.MainPage">
<controls:CustomFrame CornerRadius="0,0,30,30"
BackgroundColor="Red"
HorizontalOptions="Center"
VerticalOptions="Center"
HeightRequest="100"
WidthRequest="100" />
</ContentPage>
Android
Let's get to the platform specific code.
using FrameRenderer = Xamarin.Forms.Platform.Android.AppCompat.FrameRenderer;
[assembly: ExportRenderer(typeof(CustomFrame), typeof(CustomFrameRenderer))]
namespace XamarinFormsPlayground.Droid.Renderers
{
public class CustomFrameRenderer : FrameRenderer
{
public CustomFrameRenderer(Context context)
: base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
if (e.NewElement != null && Control != null)
{
UpdateCornerRadius();
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(CustomFrame.CornerRadius) ||
e.PropertyName == nameof(CustomFrame))
{
UpdateCornerRadius();
}
}
private void UpdateCornerRadius()
{
if (Control.Background is GradientDrawable backgroundGradient)
{
var cornerRadius = (Element as CustomFrame)?.CornerRadius;
if (!cornerRadius.HasValue)
{
return;
}
var topLeftCorner = Context.ToPixels(cornerRadius.Value.TopLeft);
var topRightCorner = Context.ToPixels(cornerRadius.Value.TopRight);
var bottomLeftCorner = Context.ToPixels(cornerRadius.Value.BottomLeft);
var bottomRightCorner = Context.ToPixels(cornerRadius.Value.BottomRight);
var cornerRadii = new[]
{
topLeftCorner,
topLeftCorner,
topRightCorner,
topRightCorner,
bottomRightCorner,
bottomRightCorner,
bottomLeftCorner,
bottomLeftCorner,
};
backgroundGradient.SetCornerRadii(cornerRadii);
}
}
}
}
The above code is pretty straight forward. We need to override the usual OnElementChanged
event handler, where we invoke the UpdateCornerRadius
method. We also want to make sure that we respond to the CornerRadius
value changes, therefore we need to override the OnElementPropertyChanged
event handler.
The UpdateCornerRadius
method retrieves the background of our native Control
and casts it to GradientDrawable
. We then retrieve CornerRadius
property value that has been set in our XAML code, we transform it values into pixels and apply it to the elements background using SetCornerRadii()
method.
iOS
The iOS code is going to be slightly more limiting in terms of customization of the corner radius. It won't allow to set different radius values across different corners (i.e. these that gets set will have the same radius).
[assembly: ExportRenderer(typeof(CustomFrame), typeof(CustomFrameRenderer))]
namespace XamarinFormsPlayground.iOS.Renderers
{
public class CustomFrameRenderer : FrameRenderer
{
public override void LayoutSubviews()
{
base.LayoutSubviews();
UpdateCornerRadius();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(CustomFrame.CornerRadius) ||
e.PropertyName == nameof(CustomFrame))
{
UpdateCornerRadius();
}
}
// A very basic way of retrieving same one value for all of the corners
private double RetrieveCommonCornerRadius(CornerRadius cornerRadius)
{
var commonCornerRadius = cornerRadius.TopLeft;
if (commonCornerRadius <= 0)
{
commonCornerRadius = cornerRadius.TopRight;
if (commonCornerRadius <= 0)
{
commonCornerRadius = cornerRadius.BottomLeft;
if (commonCornerRadius <= 0)
{
commonCornerRadius = cornerRadius.BottomRight;
}
}
}
return commonCornerRadius;
}
private UIRectCorner RetrieveRoundedCorners(CornerRadius cornerRadius)
{
var roundedCorners = default(UIRectCorner);
if (cornerRadius.TopLeft > 0)
{
roundedCorners |= UIRectCorner.TopLeft;
}
if (cornerRadius.TopRight > 0)
{
roundedCorners |= UIRectCorner.TopRight;
}
if (cornerRadius.BottomLeft > 0)
{
roundedCorners |= UIRectCorner.BottomLeft;
}
if (cornerRadius.BottomRight > 0)
{
roundedCorners |= UIRectCorner.BottomRight;
}
return roundedCorners;
}
private void UpdateCornerRadius()
{
var cornerRadius = (Element as CustomFrame)?.CornerRadius;
if (!cornerRadius.HasValue)
{
return;
}
var roundedCornerRadius = RetrieveCommonCornerRadius(cornerRadius.Value);
if (roundedCornerRadius <= 0)
{
return;
}
var roundedCorners = RetrieveRoundedCorners(cornerRadius.Value);
var path = UIBezierPath.FromRoundedRect(Bounds, roundedCorners, new CGSize(roundedCornerRadius, roundedCornerRadius));
var mask = new CAShapeLayer { Path = path.CGPath };
NativeView.Layer.Mask = mask;
}
}
}
In the above code we do the same thing as per Android code, where we override OnElementPropertyChanged
event handler to update frame's corner radius.
In the RetrieveRoundedCorners
method we set which corners (e.g. TopLeft, BottomRight) will have some radius and which won't. That's done using the bitwise OR (|
) operator with enum UIRectCorner
, which is decorated with FlagsAttribute, therefore such operations are possible.
The code responsible for the rounded corners is the UIBezirePath.FromRoundedRect()
method (iOS docs), that takes values for corner radius as parameters and returns a UIBezierPath
. That path then ends up as a Mask
to our NativeView
.
NOTE: This API is available starting from iOS SDK 12.
Code
The sample code is available in here: https://github.com/Progrunning/XamarinFormsPlayground