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  Effects  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.

Custom Frame CornerRadius - Android

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.

Custom Frame CornerRadius - iOS

NOTE: This API is available starting from iOS SDK 12.

Code

The sample code is available in here: https://github.com/Progrunning/XamarinFormsPlayground