If you’ve used the Xamarin.Forms Frame
element on iOS and have HasShadow
set to true, then you might notice that on iOS, the shadow is quite abrasive and overwhelming.
We can update just the iOS FrameRenderer
to create some better shadows, so we can go from this:
to this:
In order to override all Frame
elements, we will need to create our custom renderer and also set it to export to the default Frame
. If you do not want it to apply to all Frames
, then you can create a custom control that inherits from frame and then apply the renderer to that new element type. We’ll example both here:
MaterialFrameRenderer.cs
[assembly: ExportRenderer(typeof(Frame), typeof(MaterialFrameRenderer))] namespace YOU_IOS_NAMESPACE { /// <summary> /// Renderer to update all frames with better shadows matching material design standards /// </summary> public class MaterialFrameRenderer : FrameRenderer { public override void Draw(CGRect rect) { base.Draw(rect); // Update shadow to match better material design standards of elevation Layer.ShadowRadius = 2.0f; Layer.ShadowColor = UIColor.Gray.CGColor; Layer.ShadowOffset = new CGSize(2, 2); Layer.ShadowOpacity = 0.80f; Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath; Layer.MasksToBounds = false; } } }
That’s all you need in your iOS project in order to apply it everywhere. Now, if you want to apply it to a new custom Element
, we can create it and apply it like so:
MaterialFrame.xaml.cs
namespace YOUR_PCL_NAMESPACE { public class MaterialFrame : Frame { // no other code needs to go here unless you want more customizable properties. } }
Then create your renderer and export it for you new Element
:
MaterialFrameRenderer.cs
[assembly: ExportRenderer(typeof(MaterialFrame), typeof(MaterialFrameRenderer))] namespace YOU_IOS_NAMESPACE { /// <summary> /// Renderer to update all frames with better shadows matching material design standards /// </summary> public class MaterialFrameRenderer : FrameRenderer { public override void Draw(CGRect rect) { base.Draw(rect); // Update shadow to match better material design standards of elevation Layer.ShadowRadius = 2.0f; Layer.ShadowColor = UIColor.Gray.CGColor; Layer.ShadowOffset = new CGSize(2, 2); Layer.ShadowOpacity = 0.80f; Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath; Layer.MasksToBounds = false; } } }
And now your frames can look much nicer~
If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!
Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.
Awesome post! One thing I would like to mention is that this can cause the background color to bleed over the frame’s rounded corners.
To fix this I created a new Xamarin Forms Control derived from Frame and added an InnerBackground property. Assign your desired background color to the new InnerBackground property and set the Frame’s actual BackgroundColor to transparent. Then in the custom renderer, assign Frame.InnerBackground to Layer.BackgroundColor.
LikeLike
For Frames that have rounded corners I changed the implementation a bit. First of all I did not override Draw method but the OnElementChanged. And I’m not sure what exactly the ShadowPath line does but I removed it. My code looks like this:
[assembly: ExportRenderer(typeof(MaterialFrame), typeof(MaterialFrameRenderer))]
namespace MY_IOS_NAMESPACE
{
public class MaterialFrameRenderer : FrameRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs e)
{
base.OnElementChanged(e);
// Update shadow to match better material design standards of elevation
Layer.ShadowRadius = 2.0f;
Layer.ShadowColor = UIColor.LightGray.CGColor;
Layer.ShadowOffset = new CGSize(2, 2);
Layer.ShadowOpacity = 0.80f;
Layer.MasksToBounds = false;
}
}
}
And that’s it, your frame with rounded corners will have a nice rounded shadow as well.
LikeLike
I know what ShadowPath does. it caused to me a lot of headache. Cutting it off resolved my problems.
LikeLike
“Then create your renderer and export it for you new Element” ya lost me
LikeLike
We create a custom Xamarin.Forms renderer. If you keep reading, that’s exactly what the source code below is and does.
LikeLike
Hi,
Thanks for the solution! I’ve been successfully implemented this in my source code. Unfortunately, this code has one little problem… If I use this on a continuous-loading list view the shadow will get thicker the further I scroll down the list. I suspect the renderer redraws every shadow on all of the CardView whenever the list view loads new item. Can you help me with this issue?
Regards,
Sangadji
LikeLike
Try taking it out of the `Draw` override and put it in the constructor.
LikeLike
For me, I deleted this line Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath;
LikeLiked by 1 person
Also, it seems brute but in OnElementPropertyChanged method in the renderer works better
LikeLike
Very well. Should check HasShadow attribute:
if (Element.HasShadow)
{
……
Or when overriding OnElementChanged:
if (e.NewElement != null && e.NewElement.HasShadow)
LikeLike
Awesome Poste Alex.
Keep it comming.
LikeLike
Amazing solution, thank you very much.
If someone had problem like me, that shadow was not rendered again when frame view changed size, then just move that code to function LayoutSubviews.
IE.
public override void LayoutSubviews()
{
base.LayoutSubviews();
Layer.ShadowRadius = 2.0f;
Layer.ShadowColor = UIColor.Gray.CGColor;
Layer.ShadowOffset = new CGSize(2, 2);
Layer.ShadowOpacity = 0.80f;
Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath;
Layer.MasksToBounds = false;
}
LikeLike
If you find your shadows are still thick then add this before setting your shadow settings.
foreach(var v in Subviews) {
v.Layer.ShadowOpacity = 0;
}
LikeLike