Skip to content

Instantly share code, notes, and snippets.

@rudyryk
Last active September 5, 2024 09:51
Show Gist options
  • Save rudyryk/8cbe067a1363b45351f6 to your computer and use it in GitHub Desktop.
Save rudyryk/8cbe067a1363b45351f6 to your computer and use it in GitHub Desktop.
C# — Xamarin.Forms custom simple badge view + rounded box view via custom renderer
//
// Badge.cs
// Created by Alexey Kinev on 19 Jan 2015.
//
// Licensed under The MIT License (MIT)
// http://opensource.org/licenses/MIT
//
// Copyright (c) 2015 Alexey Kinev <[email protected]>
//
using System;
using Xamarin.Forms;
namespace ZeroFiveBit.Forms.Basic
{
/// <summary>
/// Badge.
/// </summary>
public class Badge : AbsoluteLayout
{
/// <summary>
/// The text property.
/// </summary>
public static readonly BindableProperty TextProperty =
BindableProperty.Create("Text", typeof(String), typeof(Badge), "");
/// <summary>
/// The box color property.
/// </summary>
public static readonly BindableProperty BoxColorProperty =
BindableProperty.Create("BoxColor", typeof(Color), typeof(Badge), Color.Default);
/// <summary>
/// The text.
/// </summary>
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
/// <summary>
/// Gets or sets the color of the box.
/// </summary>
public Color BoxColor
{
get { return (Color)GetValue(BoxColorProperty); }
set { SetValue(BoxColorProperty, value); }
}
/// <summary>
/// The box.
/// </summary>
protected RoundedBox Box;
/// <summary>
/// The label.
/// </summary>
protected Label Label;
/// <summary>
/// Initializes a new instance of the <see cref="ZeroFiveBit.Forms.Basic.Badge"/> class.
/// </summary>
public Badge(double size, double fontSize)
{
HeightRequest = size;
WidthRequest = HeightRequest;
// Box
Box = new RoundedBox {
CornerRadius = HeightRequest / 2
};
Box.SetBinding(BackgroundColorProperty, new Binding("BoxColor", source: this));
Children.Add(Box, new Rectangle(0, 0, 1.0, 1.0), AbsoluteLayoutFlags.All);
// Label
Label = new Label {
TextColor = Color.White,
FontSize = fontSize,
XAlign = TextAlignment.Center,
YAlign = TextAlignment.Center
};
Label.SetBinding(Label.TextProperty, new Binding("Text",
BindingMode.OneWay, source: this));
Children.Add(Label, new Rectangle(0, 0, 1.0, 1.0), AbsoluteLayoutFlags.All);
// Auto-width
SetBinding(WidthRequestProperty, new Binding("Text", BindingMode.OneWay,
new BadgeWidthConverter(WidthRequest), source: this));
// Hide if no value
SetBinding(IsVisibleProperty, new Binding("Text", BindingMode.OneWay,
new BadgeVisibleValueConverter(), source: this));
// Default color
BoxColor = Color.Red;
}
}
/// <summary>
/// Badge visible value converter.
/// </summary>
class BadgeVisibleValueConverter : IValueConverter
{
#region IValueConverter implementation
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var text = value as string;
return !String.IsNullOrEmpty(text);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
/// <summary>
/// Badge width converter.
/// </summary>
class BadgeWidthConverter : IValueConverter
{
/// <summary>
/// The width of the base.
/// </summary>
readonly double baseWidth;
/// <summary>
/// The width ratio.
/// </summary>
const double widthRatio = 0.33;
public BadgeWidthConverter(double baseWidth)
{
this.baseWidth = baseWidth;
}
#region IValueConverter implementation
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var text = value as string;
if ((text != null) && (text.Length > 1))
{
// We won't measure text length exactly here!
// May be we should, but it's too tricky. So,
// we'll just approximate new badge width as
// linear function from text legth.
return baseWidth * (1 + widthRatio * (text.Length - 1));
}
return baseWidth;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
//
// RoundedBox.cs
// Created by Alexey Kinev on 19 Jan 2015.
//
// Licensed under The MIT License (MIT)
// http://opensource.org/licenses/MIT
//
// Copyright (c) 2015 Alexey Kinev <[email protected]>
//
using System;
using Xamarin.Forms;
namespace ZeroFiveBit.Forms.Basic
{
public class RoundedBox : BoxView
{
/// <summary>
/// The corner radius property.
/// </summary>
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create("CornerRadius", typeof(double), typeof(RoundedBox), 0.0);
/// <summary>
/// Gets or sets the corner radius.
/// </summary>
public double CornerRadius
{
get { return (double)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
}
}
//
// RoundedBoxRenderer_Droid.cs
// Created by Alexey Kinev on 26 Apr 2015.
//
// Licensed under The MIT License (MIT)
// http://opensource.org/licenses/MIT
//
// Copyright (c) 2015 Alexey Kinev <[email protected]>
//
using System;
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Graphics;
using ZeroFiveBit.Forms.Basic;
using ZeroFiveBit.Forms.Basic.Droid;
[assembly: ExportRenderer(typeof(RoundedBox), typeof(RoundedBoxRenderer))]
namespace ZeroFiveBit.Forms.Basic.Droid
{
public class RoundedBoxRenderer : BoxRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e)
{
base.OnElementChanged(e);
SetWillNotDraw(false);
Invalidate();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == RoundedBox.CornerRadiusProperty.PropertyName)
{
Invalidate();
}
}
public override void Draw(Canvas canvas)
{
var box = Element as RoundedBox;
var rect = new Rect();
var paint = new Paint() {
Color = box.BackgroundColor.ToAndroid(),
AntiAlias = true,
};
GetDrawingRect(rect);
var radius = (float)(rect.Width() / box.Width * box.CornerRadius);
canvas.DrawRoundRect(new RectF(rect), radius, radius, paint);
}
}
}
//
// RoundedBoxRenderer_iOS.cs
// Created by Alexey Kinev on 19 Jan 2015.
//
// Licensed under The MIT License (MIT)
// http://opensource.org/licenses/MIT
//
// Copyright (c) 2015 Alexey Kinev <[email protected]>
//
using System;
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using ZeroFiveBit.Forms.Basic;
using ZeroFiveBit.Forms.Basic.iOS;
[assembly: ExportRenderer(typeof(RoundedBox), typeof(RoundedBoxRenderer))]
namespace ZeroFiveBit.Forms.Basic.iOS
{
public class RoundedBoxRenderer : BoxRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e)
{
base.OnElementChanged(e);
if (Element != null)
{
Layer.MasksToBounds = true;
UpdateCornerRadius(Element as RoundedBox);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == RoundedBox.CornerRadiusProperty.PropertyName)
{
UpdateCornerRadius(Element as RoundedBox);
}
}
void UpdateCornerRadius(RoundedBox box)
{
Layer.CornerRadius = (float)box.CornerRadius;
}
}
}
@rudyryk
Copy link
Author

rudyryk commented Jul 27, 2015

Oh, sorry I didn't see comments earlier.

Metrics.Pixels(size) is actually not necessary and could be replaced with size, so I did it. That was my helper to scale elements automatically for iPhone / iPad development.

@rudyryk
Copy link
Author

rudyryk commented Jul 27, 2015

Hi @stammi922! Hmm, seems like you're trying to set badge height to screen height. That's not an idea :) Badge just should have size (in points) specified as constructor parameter.

@michaeldimoudis
Copy link

Hi @rudyryk, thank's for this! I implemented the same thing with a complete different approach in iOS but needed it for Android too so tried using your code. Works great on iOS, however if I put badges in a ListView, initially they show up good, but when I start scrolling, there are some weird artifacts that sometimes appear. Have you noticed this? Is their a way to fix that?

@prajaktashinde
Copy link

@rudyryk will you please give me screen shots how this code's output is coming? It will be helpful.
Thanks in advance :)

@repper
Copy link

repper commented Jun 6, 2016

Hey @rudyryk the example was very helpful. But I have a problem, I am using ViewRender for RelativeLayout to convert that it into rounded corner layout. And I am able to do it with your codes help. But the problem is that whatever children I have added, does not shows up inside that. Is it because of the Draw method, because it is called after I add the children in the extended RelativeLayout. Thanks

@sacredgeometry
Copy link

sacredgeometry commented Jun 9, 2016

I am having the same issue as @repper, anyone got any ideas please?

@rudyryk
Copy link
Author

rudyryk commented Jun 15, 2016

Hi @repper

I am using ViewRender for RelativeLayout to convert that it into rounded corner layout.

Hm, I think that won't work simply like that. Probably I'm missing something, but Xamarin.Forms didn't provide valid hooks to replace layout renderers. There are predefined renderers for simple views in Forms like BoxRenderer etc. and we can tweak them as I usually did.

@bemnet4u
Copy link

bemnet4u commented Jul 9, 2016

I noticed that i had to add horizontal and vertical option. Otherwise i was getting rectangles.

<controls:Badge Grid.Row="0" Grid.Column="2" Text="3" WidthRequest="25" HeightRequest="25"VerticalOptions="Center"HorizontalOptions="Center"/>

@padhisandeepkumar
Copy link

padhisandeepkumar commented Aug 1, 2016

@rudyryk, I'm not able to add to toolbaritem is there any way add on that at run time?

Waiting for your response,.....

@sportzbee
Copy link

Hi Sandeep,

Have you able to added the badge to toolbar item?

@rudyryk
Copy link
Author

rudyryk commented Aug 28, 2016

@padhisandeepkumar @sportzbee Adding a badge to toolbar item can't be simply done with Xamarin.Forms. It has to be implemented per-platform and this component isn't really helpful for that case.

Actually, it's probably even easier :) Because iOS8+ natively supports UIBarButton.badgeValue! Not sure about Android, though.

@rubgithub
Copy link

Xaml sample:

<badge:Badge Text="3" WidthRequest="25" HeightRequest="25" VerticalOptions="Center" HorizontalOptions="Center" BoxColor="Blue">
            <x:Arguments>
                <x:Double>30</x:Double>
                <x:Double>12</x:Double>
            </x:Arguments>
        </badge:Badge>

@jaisont07
Copy link

Hi, the text on the badge gets misaligned at times. ie. it gets misaligned when an App is kept in the background and then resumed. Any idea on it?
badgeissue

@johanvandijke
Copy link

@jaisont07: Do you have a fix already for the misalignment? I'm facing the same issue

@yoel30
Copy link

yoel30 commented Nov 15, 2017

How to use it? can someone show an example of implementation? I just learning xamarin forms.

@rraallvv
Copy link

rraallvv commented Feb 6, 2018

@sportzbee I added the badge to the ToolbarItem for iOS using a custom renderer as explained in this blog post.

By the way, if anyone has been able to do the same for Android, I'm interested, I've googled for a solution without any luck.

@pjavax
Copy link

pjavax commented Feb 23, 2018

How can i use it? I have grid and inside it i have 7 image(Grid.Row="0" Grid.Column="0" and so on) how can i do to show badge each cell? The values that will be shown comes from a method like this(the value to be show is resultado):

decimal realizado = 0;
                        decimal previsto = 0;

                        foreach (var item in indItem)
                        {
                            var val = item.Nome == "Previsto" ? previsto = item.Valor : realizado = item.Valor;
                        }

                        decimal desvio = Math.Abs(previsto - realizado);

                        if (desvio < 1000)
                            resultado = Math.Truncate(desvio).ToString();
                        else
                            while (desvio >= 1000 && siglas.Count > 0)
                            {
                                desvio /= 1000;
                                resultado = Math.Truncate(desvio) + siglas[0];
                                siglas.RemoveAt(0);
                            }

                        if (CorIndicador == "VERDE")
                            image.Source = ImageSource.FromResource("Estapar.AppOperacional.Images.faturamento caixa-28.png");
                        else
                        {
                            var pinta = CorIndicador == "VERMELHO" ? image.Source = ImageSource.FromResource("Estapar.AppOperacional.Images.faturamento caixa-26.png") : image.Source = ImageSource.FromResource("Estapar.AppOperacional.Images.faturamento caixa-27.png");
                        }
                        label.Text = resultado;


and my xaml is like this


<ContentPage.Content>
        <StackLayout Padding="0" Spacing="0" Margin="0">
            <Grid x:Name="grd" RowSpacing="1" ColumnSpacing="1" Padding="0" Margin="0" >
            <Grid.RowDefinitions>
                <RowDefinition Height="1" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
                </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
                <Image x:Name="imgDesvioFat" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Source="{local:ImageResource Operacional.Images.faturamento caixa-28.png}" Aspect="AspectFill">
                        <Image.GestureRecognizers>
                            <TapGestureRecognizer Tapped="OnDesvioFaturamentoTapReconizerTapped" NumberOfTapsRequired="1"></TapGestureRecognizer>
                        </Image.GestureRecognizers>
                    </Image>
                    
                    <!--<Label x:Name="lblFaturamento" Text="" Grid.Row="0" Grid.Column="0" TextColor="White" FontSize="9" FontAttributes="Bold" />-->    
                  
                <!--</Grid>-->
                <Image x:Name="imgTckCancelados" Grid.Row="1" Grid.Column="1" Source="{local:ImageResource Operacinal.Images.tickets cancelados-05.png}" Aspect="AspectFill">
                    <Image.GestureRecognizers>
                        <TapGestureRecognizer Tapped="OnTckCanceladosTapGestureReconizerTapped" NumberOfTapsRequired="1"></TapGestureRecognizer>
                    </Image.GestureRecognizers>
                </Image>

i shown just two images, but i have 7 images and the code is such. How can i show the badge and the value inside with my code?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment