AutoResizeTextView

Hi,

I have just finished translating the AutoResizeTextView class found here
http://stackoverflow.com/questions/5033012/auto-scale-textview-text-to-fit-within-bounds

And thought I'd share the code:


using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Widget;
using Android.Util;
using Android.Graphics;
using Java.Lang;
using Math = System.Math;
using String = System.String;

namespace AndroClient.CustomViews
{
///


/// TextView that automatically resizes it's content to fit the layout dimensions
///

public class AutoResizeTextView : TextView
{

    // Minimum text size for this text view
    public static float MinTextSize = 20;

    // Interface for resize notifications
    public interface IOnTextResizeListener
    {
        void OnTextResize(TextView textView, float oldSize, float newSize);
    }

    #region Fields
    private static readonly Canvas TextResizeCanvas = new Canvas();

    // Our ellipse string
    private const String Ellipsis = "...";

    // Registered resize listener
    private IOnTextResizeListener _textResizeListener;

    // Flag for text and/or size changes to force a resize
    private bool _needsResize = false;

    // Text size that is set from code. This acts as a starting point for resizing
    private float _textSize;

    // Temporary upper bounds on the starting text size
    private float _maxTextSize = 0;

    // Lower bounds for text size
    private float _minTextSize = MinTextSize;

    // Text view line spacing multiplier
    private float _spacingMult = 1.0f;

    // Text view additional line spacing
    private float _spacingAdd = 0.0f;

    // Add ellipsis to text that overflows at the smallest text size
    private bool _addEllipsis = true;
    #endregion

    #region Constructors
    public AutoResizeTextView(IntPtr a, JniHandleOwnership b) : base(a, b) { }

    // Default constructor override
    public AutoResizeTextView(Context context)
        : this(context, null)
    {

    }

    // Default constructor when inflating from XML file
    public AutoResizeTextView(Context context, IAttributeSet attrs)
        : this(context, attrs, 0)
    {

    }

    // Default constructor override
    public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyle)
        : base(context, attrs, defStyle)
    {
        _textSize = TextSize;
    }
    #endregion

    #region Public Methods
    //When text changes, set the force resize flag to true and reset the text size.
    protected override void OnTextChanged(ICharSequence text, int start, int before, int after)
    {
        _needsResize = true;
        // Since this view may be reused, it is good to reset the text size
        ResetTextSize();
    }

    // If the text view size changed, set the force resize flag to true
    protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
    {
        if (w != oldw || h != oldh)
        {
            _needsResize = true;
        }
    }

    //Register listener to receive resize notifications
    public void SetOnResizeListener(IOnTextResizeListener listener)
    {
        _textResizeListener = listener;
    }


    public override void SetTextSize(ComplexUnitType unitType, float size)
    {
        base.SetTextSize(unitType, size);
        _textSize = TextSize;
    }

    /**
     * Override the set text size to update our internal reference values
     */
    //@Override
    //public void setTextSize(int unit, float size) {
    //    super.setTextSize(unit, size);
    //    mTextSize = getTextSize();
    //}

    // Override the set line spacing to update our internal reference values
    public override void SetLineSpacing(float add, float mult)
    {
        base.SetLineSpacing(add, mult);
        _spacingMult = mult;
        _spacingAdd = add;
    }

    //Set the upper text size limit and invalidate the view
    public void SetMaxTextSize(float maxTextSize)
    {
        _maxTextSize = maxTextSize;
        RequestLayout();
        Invalidate();
    }

    //Return upper text size limit
    public float GetMaxTextSize()
    {
        return _maxTextSize;
    }

    //Set the lower text size limit and invalidate the view
    public void SetMinTextSize(float minTextSize)
    {
        _minTextSize = minTextSize;
        RequestLayout();
        Invalidate();
    }

    //Return lower text size limit
    public float SetMinTextSize()
    {
        return _minTextSize;
    }

    //Set flag to add ellipsis to text that overflows at the smallest text size
    public void SetAddEllipsis(bool addEllipsis)
    {
        _addEllipsis = addEllipsis;
    }

    //Return flag to add ellipsis to text that overflows at the smallest text size
    public bool GetAddEllipsis()
    {
        return _addEllipsis;
    }

    //Reset the text to the original size
    public void ResetTextSize()
    {
        if (_textSize > 0)
        {
            base.SetTextSize(ComplexUnitType.Px, _textSize);
            _maxTextSize = _textSize;
        }
    }

    //Resize text after measuring
    protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
    {
        if (changed || _needsResize)
        {
            int widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
            int heightLimit = (bottom - top) - CompoundPaddingBottom - CompoundPaddingTop;
            ResizeText(widthLimit, heightLimit);
        }
        base.OnLayout(changed, left, top, right, bottom);
    }

    //Resize the text size with default width and height
    public void ResizeText()
    {
        int heightLimit = Height - PaddingBottom - PaddingTop;
        int widthLimit = Width - PaddingLeft - PaddingRight;
        ResizeText(widthLimit, heightLimit);
    }


    // Resize the text size with specified width and height
    public void ResizeText(int width, int height)
    {
        string text = Text;
        // Do not resize if the view does not have dimensions or there is no text
        if (string.IsNullOrEmpty(text) || height <= 0 || width <= 0 || _textSize == 0)
            return;
        // Get the text view's paint object
        TextPaint textPaint = Paint;
        // Store the current text size
        float oldTextSize = textPaint.TextSize;
        // If there is a max text size set, use the lesser of that and the default text size
        float targetTextSize = _maxTextSize > 0 ? Math.Min(_textSize, _maxTextSize) : _textSize;

        // Get the required text height
        int textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
        int textWidth = GetTextWidth(text, textPaint, width, targetTextSize);

        // Until we either fit within our text view or we had reached our min text size, incrementally try smaller sizes
        while (((textHeight >= height) || (textWidth >= width)) && targetTextSize > _minTextSize)
        {
            targetTextSize = Math.Max(targetTextSize - 2, _minTextSize);
            textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
            textWidth = GetTextWidth(text, textPaint, width, targetTextSize);
        }

        // If we had reached our minimum text size and still don't fit, append an ellipsis
        if (_addEllipsis && targetTextSize == _minTextSize && textHeight > height)
        {
            // Draw using a static layout
            StaticLayout layout = new StaticLayout(text, textPaint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, false);
            // Check that we have a least one line of rendered text
            if (layout.LineCount > 0)
            {
                // Since the line at the specific vertical position would be cut off,
                // we must trim up to the previous line
                int lastLine = layout.GetLineForVertical(height) - 1;
                // If the text would not even fit on a single line, clear it
                if (lastLine < 0)
                {
                    Text = "";
                }
                // Otherwise, trim to the previous line and add an ellipsis
                else
                {
                    int start = layout.GetLineStart(lastLine);
                    int end = layout.GetLineEnd(lastLine);
                    float lineWidth = layout.GetLineWidth(lastLine);
                    float ellipseWidth = textPaint.MeasureText(Ellipsis);

                    // Trim characters off until we have enough room to draw the ellipsis
                    while (width < lineWidth + ellipseWidth)
                    {
                        lineWidth = textPaint.MeasureText(text.Substring(start, --end + 1));
                    }
                    Text = (text.Substring(0, end) + Ellipsis);
                }
            }
        }

        // Some devices try to auto adjust line spacing, so force default line spacing
        // and invalidate the layout as a side effect
        textPaint.TextSize = targetTextSize;
        SetLineSpacing(_spacingAdd, _spacingMult);

        // Notify the listener if registered
        if (_textResizeListener != null)
        {
            _textResizeListener.OnTextResize(this, oldTextSize, targetTextSize);
        }

        // Reset force resize flag
        _needsResize = false;
    }

    #endregion

    #region Private methods

    // Set the text size of the text paint object and use a static layout to render text off screen before measuring
    private int GetTextHeight(string source, TextPaint paint, int width, float textSize)
    {
        // Update the text paint object
        paint.TextSize = textSize;
        // Measure using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true);
        return layout.Height;
    }

    // Set the text size of the text paint object and use a static layout to render text off screen before measuring
    private int GetTextWidth(string source, TextPaint paint, int width, float textSize)
    {
        // Update the text paint object
        paint.TextSize = textSize;
        // Draw using a static layout
        StaticLayout layout = new StaticLayout(source, paint, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true);
        layout.Draw(TextResizeCanvas);
        return layout.Width;
    }
    #endregion
}

}

Posts

  • CheesebaronCheesebaron DKInsider, University mod

    @HotBridge cool stuff. Wouldn't it be more awesome that you made a GitHub repository with the library and made a sample to show off how it works?

  • HotBridgeHotBridge GBMember

    Sure, I will try to do this over the weekend.
    basically the AutoResizeText view will resize the text Horizontally and Vertically to make use of all the layout

  • CheesebaronCheesebaron DKInsider, University mod

    Also as a small improvement, I would create that resize text listener as a C# event as well.

    So first create an EventArg for that:

    public TextResizedEventArgs : EventArgs
    {
        public float OldSize { get; set; }
        public float NewSize { get; set; }
    }
    

    Then add a delegate:

    public delegate void TextResizedEventHandler(object sender, TextResizedEventArgs args);
    

    And in your class add:

    public event TextResizedEventHandler TextResized;
    

    And call it where you call textResizeListener like:

    if(null != TextResized)
        TextResized(this, new TextResizedEventArgs
                          {
                              OldSize = oldTextSize,
                              NewSize = targetTextSize
                          });
    

    Then you can just subscribe to the event instead of having to implement a class that implements the IOnTextResizeListener:

    var autoResizeTextView = FindViewById<AutoResizeTextView>(Resource.Id.autoResizeTv);
    autoResizeTextView.TextResized += (s, e) => { /* do whatever you want */ };
    
  • TommyBaggettTommyBaggett USUniversity ✭✭✭

    Hi @HotBridge, thanks for sharing your code. Did you ever set up a Github repo with this code in it?

  • DualDubDualDub GBMember ✭✭

    (I used to be HotBridge)
    Nope, sorry - busy getting our studio's first game out.
    Might get the github sorted when I have more time on my plate.

  • CheesebaronCheesebaron DKInsider, University mod

    @DualDub if you don't mind I could make a GitHub repo with the code. However, I would need to know what license it is under, so I can attribute you correctly.

  • DualDubDualDub GBMember ✭✭

    It's under the "do whatever you want with it" licence

  • KrishnaChaitanyaKrishnaChaitanya USMember
    edited October 2013

    Hi @HotBridge,
    Can you give me Sample how to use this ? I am getting error at
    // Default constructor when inflating from XML file

    public AutoResizeTextView(Context context, IAttributeSet attrs)
        : this(context, attrs, 0)
    {
    
    }
    

    without any description

  • DualDubDualDub GBMember ✭✭

    Ensure you have correctly spelled AutoResizeTextView in your axml and that it comes with height + width

  • @DualDub - if you have 2 secs, could you create a quick test solution for this? It's absolutely invaluable, but I can't get it to work - just a blank textview. It's tantalising close

    Cheers

  • PedroPachoPedroPacho ESMember

    It doesn´t work properly with API 4, is there another solution? thanks

  • ChrisSwainChrisSwain USMember, University ✭✭

    Is this out on Github anywhere yet? If not, I'd be happy to put it out there.

  • ChrisSwainChrisSwain USMember, University ✭✭
    edited August 2015

    I added this control to my project and used the following xml in my axml file:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:p1="http://schemas.android.com/apk/res/android"
        p1:orientation="vertical"
        p1:minWidth="25px"
        p1:minHeight="25px"
        p1:layout_width="match_parent"
        p1:layout_height="match_parent"
        p1:id="@+id/linearLayout1"
        p1:gravity="center_vertical">
        <LinearLayout
            p1:orientation="horizontal"
            p1:minWidth="25px"
            p1:minHeight="25px"
            p1:layout_width="match_parent"
            p1:layout_height="match_parent"
            p1:id="@+id/linearLayout2"
            p1:gravity="center_horizontal">
            <testtimer.android.controls.AutoResizeTextView
                p1:text="HH:MM:SS"
                p1:layout_width="match_parent"
                p1:layout_height="match_parent"
                p1:id="@+id/countdownText"
                p1:gravity="center" />
        </LinearLayout>
    </LinearLayout>
    

    But the text show is still very small even though the text view fills the full height and width of the device. Any ideas?

    My goal is to make the text in the form "HH:MM:SS" fill the screen horizontally no matter if the orientation is portrait or landscape. Similar to how the digital clock app I use on my phone all of the time at night does.

Sign In or Register to comment.