Search code examples
xamarinxamarin.formsxamarin.androidandroid-compatibility

Xamarin Forms Android Autosize Label TextCompat pre android 8 doesn't autosize text


I want to utilise the auto-sizing feature of android textviews in my xamarin forms solution so that as the text length grows, the font sizes shrinks to never overflow the bounds of the label, and doesn't get truncated. I've created a custom Label control to do so and added an android custom renderer. It's not working in Android 7 and below. It is working in Android 8 and above.

According to the docs autosize support was introduced in android 8, but can be supported back to Android 4 with AppCompat.v4. However, my custom rendered label just renders the default font size in Android pre 8. It works fine in 8+ devices, the label text resizes as needed to not overflow the bounds. The accepted answer to this question with a similar issue on native android says it can be to do with not setting a width and height, I've tried setting widthrequest and heightrequest explicitly and it doesn't change anything. Also setting maxlines=1 doesn't change anything. An alternative thread suggests that custom fonts are the culprit. I created a vanilla forms solution using the default device font, and get the same effect.

My code:

internal class AutosizeLabelRenderer : LabelRenderer
{
    #region constructor

    public AutosizeLabelRenderer(Context context) : base(context)
    {
    }

    #endregion

    #region overridable

    protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
    {
        base.OnElementChanged(e);

        if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }

        TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, autoLabel.AutoSizeMinTextSize,
                autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
        
    }

    #endregion
}

public class AutoSizeLabel : Label
{
    public int AutoSizeMaxTextSize
    {
        get => (int)GetValue(AutoSizeMaxTextSizeProperty);
        set => SetValue(AutoSizeMaxTextSizeProperty, value);
    }

    public static readonly BindableProperty AutoSizeMaxTextSizeProperty = BindableProperty.Create(
        nameof(AutoSizeMaxTextSize),        // the name of the bindable property
        typeof(int),     // the bindable property type
        typeof(AutoSizeLabel));      // the default value for the property

    public int AutoSizeMinTextSize
    {
        get => (int)GetValue(AutoSizeMinTextSizeProperty);
        set => SetValue(AutoSizeMinTextSizeProperty, value);
    }

    public static readonly BindableProperty AutoSizeMinTextSizeProperty = BindableProperty.Create(
        nameof(AutoSizeMinTextSize),        // the name of the bindable property
        typeof(int),     // the bindable property type
        typeof(AutoSizeLabel));      // the default value for the property


    public int AutoSizeStepGranularity
    {
        get => (int)GetValue(AutoSizeStepGranularityProperty);
        set => SetValue(AutoSizeStepGranularityProperty, value);
    }

    public static readonly BindableProperty AutoSizeStepGranularityProperty = BindableProperty.Create(
        nameof(AutoSizeStepGranularity),        // the name of the bindable property
        typeof(int),     // the bindable property type
        typeof(AutoSizeLabel));      // the default value for the property

    //
}

Not working: Android 7 - text does not shrink

enter image description here

Working as expected: Android 8 and above

enter image description here

Xaml for above images:

        <StackLayout HeightRequest="200" WidthRequest="100">
            <Label Text="Fixed width and height, sentences get longer, text should shrink" />
            <controls:AutoSizeLabel
                AutoSizeMaxTextSize="50"
                AutoSizeMinTextSize="8"
                AutoSizeStepGranularity="1"
                BackgroundColor="{StaticResource Shamrock}"
                HeightRequest="40"
                HorizontalOptions="Start"
                MaxLines="1"
                Text="A small sentence"
                WidthRequest="200" />
            <controls:AutoSizeLabel
                AutoSizeMaxTextSize="50"
                AutoSizeMinTextSize="8"
                AutoSizeStepGranularity="1"
                BackgroundColor="{StaticResource Shamrock}"
                HeightRequest="40"
                HorizontalOptions="Start"
                MaxLines="1"
                Text="A larger sentence that shrinks"
                WidthRequest="200" />
            <controls:AutoSizeLabel
                AutoSizeMaxTextSize="50"
                AutoSizeMinTextSize="8"
                AutoSizeStepGranularity="1"
                BackgroundColor="{StaticResource Shamrock}"
                HeightRequest="40"
                HorizontalOptions="Start"
                MaxLines="1"
                Text="An even larger sentence that shrinks more."
                WidthRequest="200" />
        </StackLayout>

Solution

  • Leo Zhu's answer got me most of the way there. There were a couple of extra steps I needed to take to get it fully working, so I'm posting the code as a separate answer here.

    Differences between mine and Leo's answer:

    1. Creating a new native control in scope like Leo suggested meant that it worked for a while but got disposed by the garbage collector and caused an exception when returning to the page after navigating away. To fix this I needed to override a property called ManageNativeControlLifetime to return false, and then manually manage disposing the object by overriding the dispose method and calling Control.RemoveFromParent();. This advice comes from a xamarin staff member in this thread.

    2. Formatting and binding context are not automatically inherited when creating the new native control and need to be set manually. I needed to add those based on my needs using the android specific binding syntax. You may need to add other formatting and binding code based on your needs, I'm just doing font colour, gravity and binding context here.

    I set the binding context with appCompatTextView.SetBindingContext(autoLabel.BindingContext);

    1. Once the binding context was set, I needed to add a new string property to my XF AutoSizeLabel class to pass in through XAML, then use it to set the binding path for the relevant property (In my case the text property). If more than one binding is required, you would need to add multiple new binding path properties for each required property. I set a specific binding like this:

      appCompatTextView.SetBinding("Text", new Binding(autoLabel.TextBindingPath));

      To facilitate this in my Xamarin Forms Xaml, my Xaml went from <Label Text="{Binding MyViewModelPropertyName}" /> to <controls:AutoSizeLabel TextBindingPath="MyViewModelPropertyName" />

    Here's the full code of the renderer:

        protected override bool ManageNativeControlLifetime => false;
    
        protected override void Dispose(bool disposing)
        {
            Control.RemoveFromParent();
            base.Dispose(disposing);
        }
    
        private AppCompatTextView appCompatTextView;
    
        protected override void OnElementChanged(ElementChangedEventArgs<Label> e)
        {
            base.OnElementChanged(e);
    
            if (e.NewElement == null || !(e.NewElement is AutoSizeLabel autoLabel) || Control == null) { return; }
    
            //v8 and above supported natively, no need for the extra stuff below.
            if (DeviceInfo.Version.Major >= 8)
            {
                Control?.SetAutoSizeTextTypeUniformWithConfiguration(
                    autoLabel.AutoSizeMinTextSize,
                    autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity,
                    (int)ComplexUnitType.Sp);
                return;
            }
    
                appCompatTextView = new AppCompatTextView(Context);
                appCompatTextView.SetTextColor(Element.TextColor.ToAndroid());
                appCompatTextView.SetMaxLines(1);
                appCompatTextView.Gravity = GravityFlags.Center;
                appCompatTextView.SetBindingContext(autoLabel.BindingContext);
                appCompatTextView.SetBinding("Text", new Binding(autoLabel.TextBindingPath));
                SetNativeControl(appCompatTextView);
            
    
            TextViewCompat.SetAutoSizeTextTypeUniformWithConfiguration(Control, autoLabel.AutoSizeMinTextSize, autoLabel.AutoSizeMaxTextSize, autoLabel.AutoSizeStepGranularity, (int)ComplexUnitType.Sp);
        }