XF: Custom Control – Initials View

Ich habe bereits vor einiger Zeit gezeigt, wie wir ein eignes Xamarin.Forms Control aus den vorhandenen Standard-Controls entwickelt haben, nämlich den Labeled Switch. Dazu habe ich vor kurzem auch ein Video auf meinem YouTube-Kanal veröffentlicht, wo ich die einzelnen Schritte noch einmal aufzeige. Ich möchte hier nun die Gelegenheit nutzen ein weiteres Control mit euch zu entwickeln. Dabei handelt es sich um die Initials View, welche man vielleicht von Mail-Programmen kennt. Die Initials View soll von einem Namen später die Initialen ermitteln und in einem Kreis anzeigen. Die Farbe des Kreises wird dabei automatisch vom Namen her „berechnet“.

Hier mal ein Bild von dem fertigen Ergebnis unter Android.

Ausgangslage bildet ein leeres Xamarin.Forms Projekt. Ich habe bereits alle NuGet-Pakete aktualisiert und das Visual Material Package hinzugefügt und initialisiert. Unsere Initials View soll aus einer abgerundeten BoxView, einem Label und ein bisschen Magie bestehen, um die passende Farbe zu berechnen.

Wir erstellen zunächst einen Ordner Controls und darin eine ContentView mit den Namen InitialsView. Anschließend aktualisieren wir den Content und fügen eine BoxView und ein Label innerhalb eines Grids hinzu.

<ContentView.Content>
    <Grid>
        <BoxView x:Name="ContentBoxView"
                 VerticalOptions="Center"
                 HorizontalOptions="Center" />
        <Label x:Name="ContentLabel"
               VerticalOptions="Center"
               HorizontalOptions="Center" />
    </Grid>
</ContentView.Content>

Wir verwenden hierbei die x:Name-Property, um später Zugriff auf die Elemente zu erhalten. Nun müssen wir unserem Control noch ein paar Bindable Properties spendieren. Wir benötigen verschiedene Informationen über die Farben, z.B. die Standard-Hintergrundfarbe oder eine helle bzw. dunkle Text-Farbe. Außerdem benötigen wir auch noch die Name-Property, welche später verwendet wird, um die Initialen zu bestimmen. Wir wechseln dazu in die Code-Behind Datei und ergänzen die folgenden Properties.

public static readonly BindableProperty DefaultBackgroundColorProperty
    = BindableProperty.Create(nameof(DefaultBackgroundColor), typeof(Color),
        typeof(InitialsView), Color.LightGray);

public Color DefaultBackgroundColor
{
    get => (Color)GetValue(DefaultBackgroundColorProperty);
    set => SetValue(DefaultBackgroundColorProperty, value);
}


public static readonly BindableProperty TextColorLightProperty
    = BindableProperty.Create(nameof(TextColorLight), typeof(Color),
        typeof(InitialsView), Color.White);

public Color TextColorLight
{
    get => (Color)GetValue(TextColorLightProperty);
    set => SetValue(TextColorLightProperty, value);
}


public static readonly BindableProperty TextColorDarkProperty
    = BindableProperty.Create(nameof(TextColorDark), typeof(Color),
        typeof(InitialsView), Color.Black);

public Color TextColorDark
{
    get => (Color)GetValue(TextColorDarkProperty);
    set => SetValue(TextColorDarkProperty, value);
}


public static readonly BindableProperty NameProperty
    = BindableProperty.Create(nameof(Name), typeof(string),
        typeof(InitialsView), string.Empty);

public string Name
{
    get => (string)GetValue(NameProperty);
    set => SetValue(NameProperty, value);
}

Im nächsten Schritt wollen wir nun eine Methode SetColors erstellen, welche als Parameter den Namen übergeben bekommt. Aus diesem Namen soll anschließend die passende Hintergrundfarbe „berechnet“ werden und anschließend noch geschaut werden, ob wir die helle oder die dunkle Text-Farbe benötigen.

private void SetColors(string name)
{
    // get color for the provided text
    var hexColor = "#FF" + Convert.ToString(name.GetHashCode(), 16).Substring(0, 6);

    // fix issue if value is too short
    if (hexColor.Length == 8)
        hexColor += "5";

    // create color from hex value
    var color = Color.FromHex(hexColor);

    // set backgroundcolor of contentboxview
    ContentBoxView.BackgroundColor = color;

    // get brightness and set textcolor
    var brightness = color.R * .3 + color.G * .59 + color.B * .11;
    ContentLabel.TextColor = brightness < 0.5 ? TextColorLight : TextColorDark;
}

Wir wollen nun noch eine Methode SetName schreiben, welche die Initialen aus dem Namen ermittelt. Diese Methode ist relativ simpel, denn wir teilen den Namen an den Leerzeichen und nehmen stets den ersten Buchstaben vom Vornamen und vom Nachnamen.

private void SetName(string name)
{
    if (string.IsNullOrEmpty(name))
    {
        ContentLabel.Text = string.Empty;
        ContentBoxView.BackgroundColor = DefaultBackgroundColor;
        return;
    }

    var words = name.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
    if (words.Length == 1)
    {
        ContentLabel.Text = words[0][0].ToString();
    }
    else if (words.Length > 1)
    {
        var initialsString = words[0][0].ToString() 
             + words[words.Length - 1][0].ToString();
        ContentLabel.Text = initialsString;
    }
    else
    {
        ContentLabel.Text = string.Empty;
    }

    SetColors(name);
}

Derzeit hat die BoxView noch keine abgerundeten Ecken und wir behandeln auch noch nicht die Größe korrekt. Daher schreiben wir jetzt noch eine Methode InitControl, welche eine gewünschte Größe übergeben bekommt. Damit werden dann sowohl Höhe als auch Breite festgelegt, sowie der passende CornerRadius berechnet. Sollte auch schon ein Name vorhanden sein, so rufen wir unsere Methode SetName auf.

private void InitControl(double size)
{
    // set width and height of contentboxview
    ContentBoxView.HeightRequest = size;
    ContentBoxView.WidthRequest = size;

    // calculate corner radius of contentboxview
    ContentBoxView.CornerRadius = size / 2;

    // set default background
    ContentBoxView.BackgroundColor = DefaultBackgroundColor;

    // set fontsize
    ContentLabel.FontSize = (size / 2) - 5;

    // check if name is already present
    if (!string.IsNullOrEmpty(Name))
        SetName(Name);
}

Wir müssen jetzt diese Methode noch irgendwie aufrufen. Da es standardmäßig keinen Lebenszyklus für eine ContentView gibt, nutzen wir die Methode OnParetSet, welche wir überschreiben wollen. Hier überprüfen wir, ob der Width– oder HeightRequest gesetzt ist, wenn dies nicht der Fall ist, so nutzen wir einen Standardwert für die Größe von 50px und ansonsten nutzen wir Width– und HeightRequest zum Ermitteln der passenden Größe.

protected override void OnParentSet()
{
    base.OnParentSet();

    if (WidthRequest == -1 || HeightRequest == -1)
    {
        InitControl(50);
    }
    else
    {
        InitControl(Math.Min(WidthRequest, HeightRequest));
    }
}

Bevor wir unsere Initials View jetzt einmal ausprobieren können, müssen wir noch das PropertyChanged auf der Name-Property implementieren. Denn wenn sich der Name ändert, dann wollen wir die Methode SetName aufrufen, welche dann die Initialen ermittelt und die passende Farbe setzt.

public static readonly BindableProperty NameProperty
    = BindableProperty.Create(nameof(Name), typeof(string),
        typeof(InitialsView), string.Empty, 
        propertyChanged: OnNamePropertyChanged);

public string Name
{
    get => (string)GetValue(NameProperty);
    set => SetValue(NameProperty, value);
}

private static void OnNamePropertyChanged(BindableObject bindable, 
    object oldValue, object newValue)
{
    if (!(bindable is InitialsView initialsView))
        return;

    initialsView.SetName((string)newValue);
}

Wir wechseln nun auf die MainPage und fügen dort eine InitialsView hinzu. Außerdem nutzen wir noch ein Entry, wo der Nutzer einen Namen eingeben kann.

<StackLayout VerticalOptions="Center"
             HorizontalOptions="Center">

    <controls:InitialsView WidthRequest="250"
                           HeightRequest="250"
                           Name="{Binding Text, Source={x:Reference NameEntry}}" />

    <Entry x:Name="NameEntry"
           Placeholder="Name" />
    
</StackLayout>

Nun können wir die App einmal starten, um uns das Ergebnis anzuschauen. Die folgende Animation zeigt die Verwendung der UWP-Version, aber natürlich verhält sich das Control unter Android und iOS identisch.

Diesen Artikel habe ich bereits in englischer Sprache auf Medium veröffentlicht. Außerdem habe ich den Code auf GitHub veröffentlicht, so dass ihr diesen als Grundlage nutzen könnt, um dieses Control nach euren Wünschen noch zu erweitern.

Map Control mit bindable Pins erweitern Rückblick: Expert Day for Xamarin 2020 Mac fernsteuern mit VNC