Reputation: 23833
I have an Xamarin application. One of the pages I want to display an Image
in a circle, rather than square. To do this I have created a custom rendered for each of the platforms following some online guidance. The classes are below; first in the (portable) project I have
public class CircleImage : Image
{
public static readonly BindableProperty BorderThicknessProperty =
BindableProperty.Create(propertyName: nameof(BorderThickness),
returnType: typeof(float),
declaringType: typeof(CircleImage),
defaultValue: 0F);
public float BorderThickness
{
get { return (float)GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
}
public static readonly BindableProperty BorderColorProperty =
BindableProperty.Create(propertyName: nameof(BorderColor),
returnType: typeof(Color),
declaringType: typeof(CircleImage),
defaultValue: Color.White);
public Color BorderColor
{
get { return (Color)GetValue(BorderColorProperty); }
set { SetValue(BorderColorProperty, value); }
}
public static readonly BindableProperty FillColorProperty =
BindableProperty.Create(propertyName: nameof(FillColor),
returnType: typeof(Color),
declaringType: typeof(CircleImage),
defaultValue: Color.Transparent);
public Color FillColor
{
get { return (Color)GetValue(FillColorProperty); }
set { SetValue(FillColorProperty, value); }
}
}
Then for Android, I have the renderer
[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
namespace GL.Droid.Renderer
{
[Preserve(AllMembers = true)]
public class CircleImageRenderer : ImageRenderer
{
#pragma warning disable CS0618 // Type or member is obsolete.
public CircleImageRenderer() : base()
#pragma warning restore CS0618 // Type or member is obsolete.
{
}
public CircleImageRenderer(Context context) : base(context) { }
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously.
public async static void Init()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
var temp = DateTime.Now;
}
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
// Only enable hardware accelleration on lollipop.
if ((int)Build.VERSION.SdkInt < 21)
{
SetLayerType(LayerType.Software, null);
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == CircleImage.BorderColorProperty.PropertyName ||
e.PropertyName == CircleImage.BorderThicknessProperty.PropertyName ||
e.PropertyName == CircleImage.FillColorProperty.PropertyName)
{
Invalidate();
}
}
protected override bool DrawChild(Canvas canvas, Android.Views.View child, long drawingTime)
{
try
{
var radius = (float)Math.Min(Width, Height) / 2f;
var borderThickness = ((CircleImage)Element).BorderThickness;
var strokeWidth = 0f;
if (borderThickness > 0)
{
var logicalDensity = Android.App.Application.Context.Resources.DisplayMetrics.Density;
strokeWidth = (float)Math.Ceiling(borderThickness * logicalDensity + .5f);
}
radius -= strokeWidth / 2f;
var path = new Path();
path.AddCircle(Width / 2.0f, Height / 2.0f, radius, Path.Direction.Ccw);
canvas.Save();
canvas.ClipPath(path);
var paint = new Paint
{
AntiAlias = true
};
paint.SetStyle(Paint.Style.Fill);
paint.Color = ((CircleImage)Element).FillColor.ToAndroid();
canvas.DrawPath(path, paint);
paint.Dispose();
var result = base.DrawChild(canvas, child, drawingTime);
path.Dispose();
canvas.Restore();
path = new Path();
path.AddCircle(Width / 2f, Height / 2f, radius, Path.Direction.Ccw);
if (strokeWidth > 0.0f)
{
paint = new Paint
{
AntiAlias = true,
StrokeWidth = strokeWidth
};
paint.SetStyle(Paint.Style.Stroke);
paint.Color = ((CircleImage)Element).BorderColor.ToAndroid();
canvas.DrawPath(path, paint);
paint.Dispose();
}
path.Dispose();
return result;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unable to create circle image: " + ex);
}
return base.DrawChild(canvas, child, drawingTime);
}
}
}
This works great and gives me the following looking layout
Now for the iOS and where the problem lies, we have the following, this (as far as I can tell), matches the Android implementation which is below
[assembly: ExportRenderer(typeof(CircleImage), typeof(CircleImageRenderer))]
namespace GL.iOS.Renderer
{
[Preserve(AllMembers = true)]
public class CircleImageRenderer : ImageRenderer
{
#pragma warning disable CS0108 // Member hides inherited member; missing new keyword
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
public async static void Init()
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
#pragma warning restore CS0108 // Member hides inherited member; missing new keyword
{
var temp = DateTime.Now;
}
protected override void OnElementChanged(ElementChangedEventArgs<Image> e)
{
base.OnElementChanged(e);
if (Element == null)
return;
CreateCircle();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == VisualElement.HeightProperty.PropertyName ||
e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == CircleImage.BorderColorProperty.PropertyName ||
e.PropertyName == CircleImage.BorderThicknessProperty.PropertyName ||
e.PropertyName == CircleImage.FillColorProperty.PropertyName)
{
CreateCircle();
}
}
private void CreateCircle()
{
try
{
var min = Math.Min(Element.Width, Element.Height);
Control.Layer.CornerRadius = (nfloat)(min / 2.0);
Control.Layer.MasksToBounds = false;
Control.BackgroundColor = ((CircleImage)Element).FillColor.ToUIColor();
Control.ClipsToBounds = true;
var borderThickness = ((CircleImage)Element).BorderThickness;
// Remove previously added layers.
var tempLayer = Control.Layer.Sublayers?
.Where(p => p.Name == borderName)
.FirstOrDefault();
tempLayer?.RemoveFromSuperLayer();
var externalBorder = new CALayer();
externalBorder.Name = borderName;
externalBorder.CornerRadius = Control.Layer.CornerRadius;
externalBorder.Frame = new CGRect(-.5, -.5, min + 1, min + 1);
externalBorder.BorderColor = ((CircleImage)Element).BorderColor.ToCGColor();
externalBorder.BorderWidth = ((CircleImage)Element).BorderThickness;
Control.Layer.AddSublayer(externalBorder);
}
catch (Exception ex)
{
Debug.WriteLine("Unable to create circle image: " + ex);
}
}
const string borderName = "borderLayerName";
}
}
But this gives me the rendered output of
My XAML is
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage x:Class="GL.ProfilePage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Title="Generation London"
xmlns:local="clr-namespace:GL;assembly=GL"
xmlns:Controls="clr-namespace:GL.Controls"
xmlns:Converters="clr-namespace:GL.Converters"
BackgroundColor="White">
<ContentPage.Resources>
<ResourceDictionary>
<Converters:ResizingImageConverter x:Key="ResizingImageConverter"/>
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<ScrollView>
<Grid ColumnSpacing="0" RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Aspect="AspectFill"
Source="login_background.jpg" />
<Image Aspect="Fill"
Margin="0,-1,0,-1"
Source="curved_mask.png"
VerticalOptions="End" />
<Controls:CircleImage BorderThickness="2"
BorderColor="{x:Static local:Settings.LightPurple}"
WidthRequest="100"
HeightRequest="100"
TranslationY="50"
HorizontalOptions="FillAndExpand"
VerticalOptions="End"
Source="{Binding ProfilePicture, Converter={StaticResource ResizingImageConverter}}">
<!--<Image.Source>
<UriImageSource Uri="{Binding ProfilePicture}" CacheValidity="90"/>
</Image.Source>-->
</Controls:CircleImage>
<StackLayout Grid.Row="1" Padding="0,50,0,00" HorizontalOptions="Center">
<Label x:Name="fullName" Style="{StaticResource MainLabel}"/>
<Label Margin="0,-5" Style="{StaticResource SubLabel}" Text="{Binding Occupation}" />
</StackLayout>
<Grid Grid.Row="2" Margin="0,30" ColumnSpacing="0" RowSpacing="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout>
<Label Style="{StaticResource ValueLabel}" Text="{Binding DateOfBirth, StringFormat='{0:dd/MM/yyyy}'}"/>
<Label Style="{StaticResource CaptionLabel}" Text="DOB"/>
</StackLayout>
<StackLayout Grid.Column="1">
<Label x:Name="workTubeStation" Style="{StaticResource ValueLabel}"/>
<Label Style="{StaticResource CaptionLabel}" Text="Nearest Tube"/>
</StackLayout>
<StackLayout Grid.Column="2">
<Label x:Name="gender" Style="{StaticResource ValueLabel}"/>
<Label Style="{StaticResource CaptionLabel}" Text="Gender"/>
</StackLayout>
</Grid>
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="1"
Margin="0,-5"
Text="Interests"
Style="{StaticResource MainLabel}"/>
</Grid>
<ContentView Grid.Row="4" Padding="5">
<ListView x:Name="userInterests"
RowHeight="35"
ItemsSource="{Binding Interests}"
ItemTapped="NoOpInterestSelected"
HorizontalOptions="Center"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Label Text="{Binding .}"
Style="{StaticResource ValueLabel}"
HorizontalTextAlignment="Center"
YAlign="Center" />
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentView>
<Button Grid.Row="5"
Margin="20"
Style="{StaticResource EditButton}"
Clicked="OnEditProfile"
Text="Edit"/>
</Grid>
</ScrollView>
</ContentPage.Content>
</ContentPage>
Q. Why is the circle container not being rendered correctly?
Thanks for your time.
Upvotes: 0
Views: 128
Reputation: 9990
You haven't shown your XAML, but based on your renderer and your output it seems that the Image
is covering not just the photo part, but rather the whole screen width, which makes your code work (corner radius and drawing the ellipse) to appear on the unexpected parts and eventually results in what you have shown. The renderer code expects that the Image control has no transparent parts (e.g. that it uses AspectFill
)
Upvotes: 1
Reputation: 18861
If you want to set the Rounded Corner for the control ,Refer the following code
...
Control.Layer.MasksToBounds = true;
Control.Layer.CornerRadius = (nfloat)(min / 2.0);
Control.Layer.BorderColor = ((CircleImage)Element).BorderColor.ToCGColor();
Control.Layer.BorderWidth = ((CircleImage)Element).BorderThickness;;
...
You don't need to add a new sublayer on layer.If you do want to do it.Refer to this similar issue.
Upvotes: 0