Overriding frame to allow custom radius and shadow

I'm having immense difficulty figuring out an effective way to override Frame to allow for a custom corner radius and shadow. Does anyone have a good solution? It seems this should be functionality within the cross-platform layout, but isn't yet.

Thanks in advance!

Best Answer

Answers

  • adamkempadamkemp USInsider, Developer Group Leader mod

    What are you trying? Are you using a custom renderer?

  • MichaelRobichaudMichaelRobichaud USMember ✭✭

    I'm trying to use a custom renderer. Here is the override of Draw in my ios renderer:
    `

        public override void Draw (RectangleF rect)
        {
            var elem = (BorderViewContainer)this.Element;
            if (elem != null) {
                using (var context = UIGraphics.GetCurrentContext ()) {
                    // Border
                    context.SetFillColor (elem.BackgroundColor.ToCGColor ());
                    context.SetStrokeColor (elem.BorderColor.ToCGColor ());
                    context.SetLineWidth ((float)elem.BorderThickness);
    
                    this.Layer.CornerRadius = (float)elem.CornerRadius;
                    this.Layer.Bounds.Inset ((int)elem.BorderThickness, (int)elem.BorderThickness);
    
                    // Shadow
                    this.Layer.ShadowColor = UIColor.DarkGray.CGColor;
                    this.Layer.ShadowOpacity = 0.4f;
                    this.Layer.ShadowRadius = 2.0f;
                    this.Layer.ShadowOffset = new SizeF (0, 0); //(2.0f, 2.0f);
                    this.Layer.MasksToBounds = false;
    
                    var rc = this.Bounds.Inset ((int)elem.BorderThickness, (int)elem.BorderThickness);
                    float radius = (float)elem.CornerRadius;
                    radius = Math.Max (0, Math.Min (radius, Math.Max (rc.Height / 2, rc.Width / 2)));
    
                    var path = CGPath.FromRoundedRect (rc, radius, radius);
                    context.AddPath (path);
                    context.DrawPath (CGPathDrawingMode.FillStroke);
    
                    // Here is where I'm stuck... of course, uncommenting the line below gives the nicely
                    // rounded corners, but hides the shadow. I have read up on some solutions in
                    // objective-c that use a subview/superview model, where a shadow view is added to
                    // the view. Can't quite get it to work here.
    
                    //elem.IsClippedToBounds = true;
                }
            }
        } 
    

    `

  • MichaelRobichaudMichaelRobichaud USMember ✭✭

    Thanks for the guidance! The following is working (for the most part):

    `

        protected override void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.Frame> e)
        {
            base.OnElementChanged (e);
            var elem = (BorderViewContainer)this.Element;
            if (elem != null) {
    
                // Border
                this.Layer.CornerRadius = (float)elem.CornerRadius;
                this.Layer.Bounds.Inset ((int)elem.BorderThickness, (int)elem.BorderThickness);
                Layer.BorderColor = elem.BorderColor.ToCGColor();
                Layer.BorderWidth = (float)elem.BorderThickness;
    
                // Shadow
                this.Layer.ShadowColor = UIColor.DarkGray.CGColor;
                this.Layer.ShadowOpacity = 0.6f;
                this.Layer.ShadowRadius = 2.0f;
                this.Layer.ShadowOffset = new SizeF (0, 0);
                //this.Layer.MasksToBounds = true;
    
            }
        }
    

    `

    The one issue is that with no padding, the child element (in my case a stacklayout) extends beyond the corner radius. I think a good solution will be to create another custom renderer: roundedstacklayout, that sets MasksToBounds = true; (Attached screenshots demonstrate with padding and without padding.

  • adamkempadamkemp USInsider, Developer Group Leader mod

    You also need to handle when the properties change. You can use OnElementPropertyChanged for that, and then see if the property that changed is one of the ones you're using.

    I think a good solution will be to create another custom renderer: roundedstacklayout, that sets MasksToBounds = true;

    You could also try making the stack layout have a clear background.

  • MichaelRobichaudMichaelRobichaud USMember ✭✭

    I ended up adding the following:

    // Child Element Layer if (this.Layer.Sublayers.Length > 0) { var subLayer = this.Layer.Sublayers [0]; subLayer.CornerRadius = (float)elem.CornerRadius; subLayer.MasksToBounds = true; }

    This seemed to work!

  • Sweet_LouSweet_Lou USMember

    Hey guys, thanks for this thread...it helped a lot. I was able to implement the code for creating a custom shadow, but not the border radius. 'CornerRadius', 'BorderThickness' and others are not defined, which probably means I'm missing an Assembly Reference with the definitions. Could you include what all references you needed for this to work? I know the Shadow took CoreGraphics, but what about the Border? I looked through API but no luck finding it. Thanks!

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Those properties are on the CALayer class, not UIView. You need to set them on the view's Layer. Example:

    Layer.BorderWidth = 2;
    
  • LoneLyLoneLy USMember ✭✭

    Hello,
    @MichaelRobichaud

    I am following the example provided in this thread. It seems that I am missing an assembly because I cannot find BorderViewContainer. Would you please tell me what assemblies/namespaces are you referencing to?

    Thanks.

  • TonyDTonyD USMember ✭✭✭

    @LoneLy Since this comes up a lot, here's the full code for mine for iOS:

    using MyApp;
    using Foundation;
    using CoreGraphics;
    using System;
    
    [assembly: ExportRenderer (typeof (ExtendedFrame), typeof (ExtendedFrameRenderer))]
    
    namespace MyApp
    {
        public class ExtendedFrameRenderer : FrameRenderer
        {
            protected override void OnElementChanged (ElementChangedEventArgs<Frame> e)
            {
                base.OnElementChanged (e);
                var elem = (ExtendedFrame)this.Element;
                if (elem != null) {
    
                    // Border
                    this.Layer.CornerRadius = (float)elem.Radius;
                    this.Layer.Bounds.Inset ((int)elem.BorderWidth, (int)elem.BorderWidth);
                    Layer.BorderColor = elem.BorderColor.ToCGColor();
                    Layer.BorderWidth = (float)elem.BorderWidth;
                }
            }
    
        }
    }
    

    I occasionally have to set IsClippedToBounds = true as well.

  • LoneLyLoneLy USMember ✭✭

    Thanks @TonyD. After I copied and pasted your code in mine, I cannot resolve Radius, BorderWidth, and BorderColor in my elem object. Any ideas what I may have missed?

    Btw, my ExtendedFrame is an empty class that inherits from Frame.

  • TonyDTonyD USMember ✭✭✭
    edited June 2016

    Yep just add those as double properties to ExtendedFrame in your PCL project. Not in front of my work computer but something like:

    class ExtendedFrame : Frame
    {
    public double Radius {get;set;}
    public double BorderWidth {get;set;}
    public Color BorderColor {get;set;}
    

    (...)
    }

  • LoneLyLoneLy USMember ✭✭

    Now it works perfectly! Thanks a lot @TonyD ! :)

  • BrightLeeBrightLee KRMember ✭✭✭

    Do you have code for android too? @TonyD

  • TonyDTonyD USMember ✭✭✭

    @BBright yes but it's like 10x longer.

    The way I did it was go to the Xamarin Source code (open source), look for the Android frame renderer, and copy paste it. At the time they had hardcoded Radius = 5 into their source code. I just changed it to make it customizable.

  • BrightLeeBrightLee KRMember ✭✭✭

    Yes, I tried it with using same way when I did it to button.

            ExtendedFrame frame = (ExtendedFrame)this.Element;
            var _normal = new Android.Graphics.Drawables.Drawable
    
            _normal.SetStroke((int)frame.BorderWidth, frame.BorderColor.ToAndroid());
            _normal.SetCornerRadius((float)frame.Radius);
    

    like this.
    but no luck.

    Could you share your code?

    And plus, where can you see the code?
    https://github.com/xamarin/Xamarin.Forms
    Did you find it from here?

    @TonyD Thanks.

  • TonyDTonyD USMember ✭✭✭

    @BBright

    For Android my code is this file:

    https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.Android/Renderers/FrameRenderer.cs

    and I replaced this:
    float rx = Forms.Context.ToPixels(5);
    float ry = Forms.Context.ToPixels(5);

    changed 5 to custom property. Not sure why XF team decided to force everyone to use 5 pixels.

  • BrightLeeBrightLee KRMember ✭✭✭

    Thanks again @TonyD
    I'm sorry I have to ask you again, Did you build xamarin.forms your own??

    How could you possibly change it? I can not override DrawBackground method.

    I tried like this. but no luck. It's working incorrectly.

        public override void Draw(Android.Graphics.Canvas canvas)
        {
            base.Draw(canvas);
    
            ExtendedFrame frame = (ExtendedFrame)this.Element;
            int width = (int)frame.Width;
            int height = (int)frame.Height;
    
            using (var paint = new Paint { AntiAlias = true })
            using (var path = new Path())
            using (Path.Direction direction = Path.Direction.Cw)
            using (Paint.Style style = Paint.Style.Fill)
            using (var rect = new RectF(0, 0, width, height))
            {
                float rx = Forms.Context.ToPixels(5);
                float ry = Forms.Context.ToPixels(5);
                path.AddRoundRect(rect, rx, ry, direction);
    
                global::Android.Graphics.Color color;// = _frame.BackgroundColor.ToAndroid();
                color = global::Android.Graphics.Color.Red;
    
                paint.SetStyle(style);
                paint.Color = color;
    
                canvas.DrawPath(path, paint);
            }
        }
    
  • TonyDTonyD USMember ✭✭✭

    Just replace all frame with extendedframe. I didn't rebuild XF for this.

  • BrightLeeBrightLee KRMember ✭✭✭

    Hi, @TonyD For your help, It's working now. Thanks.

    Another problem here. I noticed it does not crop frame and its children but just drawing rounded rect on background.
    Do you think I can crop rounded corner as I expected?

  • BrightLeeBrightLee KRMember ✭✭✭

    I meant cropping inside contents not working.

  • TonyDTonyD USMember ✭✭✭

    @BBright - did you do IsClippedToBounds = true? My inside content is a bit far from the corner so it's possible that inside cropping isn't working.

  • BrightLeeBrightLee KRMember ✭✭✭

    @TonyD Yeap, I did. but cropping is not working. source code is just drawing background rounded.
    Never mind.

  • TonyDTonyD USMember ✭✭✭

    @BBright I just checked and inside cropping seems to work for iOS but not Android. If you figure out the latter, please post it here!

  • BrightLeeBrightLee KRMember ✭✭✭

    @TonyD Thanks for double checking.
    Yeap. I would.

  • DemiVisionDemiVision USMember ✭✭

    Has anyone tried accomplishing this by using a 9-patch image with something like Forms9Patch? You could then presumably get a nice feathered drop shadow together with perfectly sharp, rounded corners, with virtually zero performance hit. And then put a layout with transparent background on top it. I haven't tried this yet myself, but I do need the functionality, and it seems like it would work.

  • EhsanJahanagiriEhsanJahanagiri USMember ✭✭
    edited February 13

    This article describes how to customize border shadow effects for both platform by custom renderer
    https://www.c-sharpcorner.com/article/xamariin-forms-border-shadow-effects-using-custom-frame-renderer/

Sign In or Register to comment.