I\'m looking for an optimal way to resize wrapping text in a TextView
so that it will fit within its getHeight and getWidth bounds. I\'m not simply looking for
Providing this version of top answer rewritten on C# for those who codes on Xamarin.Android. Worked for me well.
/**
* DO WHAT YOU WANT TO PUBLIC LICENSE
* Version 2, December 2004
*
* Copyright (C) 2004 Sam Hocevar
*
* Everyone is permitted to copy and distribute verbatim or modified
* copies of this license document, and changing it is allowed as long
* as the name is changed.
*
* DO WHAT YOU WANT TO PUBLIC LICENSE
* TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
*
* 0. You just DO WHAT YOU WANT TO.
*/
using System;
using Android.Content;
using Android.Runtime;
using Android.Text;
using Android.Util;
using Android.Widget;
using Java.Lang;
namespace App.GuestGuide.Droid.Controls
{
public class OnTextResizeEventArgs : EventArgs
{
public TextView TextView { get; set; }
public float OldSize { get; set; }
public float NewSize { get; set; }
}
///
///
/// Text view that auto adjusts text size to fit within the view.
/// If the text size equals the minimum text size and still does not
/// fit, append with an ellipsis.
///
public class AutoResizeTextView : TextView
{
///
/// Minimum text size for this text view
///
public static float MIN_TEXT_SIZE = 10;
///
/// Our ellipse string
///
private const string Ellipsis = "...";
private float _mMaxTextSize;
private float _mMinTextSize = MIN_TEXT_SIZE;
///
/// Register subscriber to receive resize notifications
///
public event EventHandler OnTextResize;
///
/// Flag for text and/or size changes to force a resize
///
private bool _needsResize;
///
/// Text size that is set from code. This acts as a starting point for resizing
///
private float _textSize;
///
/// Text view line spacing multiplier
///
private float _spacingMult = 1.0f;
///
/// Text view additional line spacing
///
private float _spacingAdd;
///
/// Add ellipsis to text that overflows at the smallest text size
///
public bool ShouldAddEllipsis { get; set; }
///
///
/// Override the set text size to update our internal reference values
///
public override float TextSize
{
get => base.TextSize;
set
{
base.TextSize = value;
_textSize = TextSize;
}
}
///
/// Temporary upper bounds on the starting text size
///
public float MaxTextSize
{
get => _mMaxTextSize;
// Set the upper text size limit and invalidate the view
set
{
_mMaxTextSize = value;
RequestLayout();
Invalidate();
}
}
///
/// Lower bounds for text size
///
public float MinTextSize
{
get => _mMinTextSize;
//Set the lower text size limit and invalidate the view
set
{
_mMinTextSize = value;
RequestLayout();
Invalidate();
}
}
public AutoResizeTextView(Context context) : this(context, null)
{
}
public AutoResizeTextView(Context context, IAttributeSet attrs) : this(context, attrs, 0)
{
}
public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
_textSize = TextSize;
}
public AutoResizeTextView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes)
{
_textSize = TextSize;
}
protected AutoResizeTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
{
_textSize = TextSize;
}
///
///
/// When text changes, set the force resize flag to true and reset the text size.
///
///
///
///
///
protected override void OnTextChanged(ICharSequence text, int start, int lengthBefore, int lengthAfter)
{
_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;
}
}
public override void SetTextSize([GeneratedEnum] ComplexUnitType unit, float size)
{
base.SetTextSize(unit, size);
_textSize = TextSize;
}
///
///
/// 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;
}
///
/// Reset the text to the original size
///
public void ResetTextSize()
{
if (_textSize > 0)
{
base.SetTextSize(ComplexUnitType.Px, _textSize);
_mMaxTextSize = _textSize;
}
}
///
///
/// Resize text after measuring
///
///
///
///
///
///
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
if (changed || _needsResize)
{
var widthLimit = (right - left) - CompoundPaddingLeft - CompoundPaddingRight;
var 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()
{
var heightLimit = Height - PaddingBottom - PaddingTop;
var widthLimit = Width - PaddingLeft - PaddingRight;
ResizeText(widthLimit, heightLimit);
}
///
/// Resize the text size with specified width and height
///
///
///
public void ResizeText(int width, int height)
{
ICharSequence text = null;
if (!string.IsNullOrEmpty(Text))
{
text = new Java.Lang.String(Text);
}
// Do not resize if the view does not have dimensions or there is no text
if (text == null || text.Length() == 0 || height <= 0 || width <= 0 || _textSize == 0)
{
return;
}
if (TransformationMethod != null)
{
text = TransformationMethod.GetTransformationFormatted(text, this);
}
// Get the text view's paint object
var textPaint = Paint;
// Store the current text size
var oldTextSize = textPaint.TextSize;
// If there is a max text size set, use the lesser of that and the default text size
var targetTextSize = _mMaxTextSize > 0 ? System.Math.Min(_textSize, _mMaxTextSize) : _textSize;
// Get the required text height
var textHeight = GetTextHeight(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 && targetTextSize > _mMinTextSize)
{
targetTextSize = System.Math.Max(targetTextSize - 2, _mMinTextSize);
textHeight = GetTextHeight(text, textPaint, width, targetTextSize);
}
// If we had reached our minimum text size and still don't fit, append an ellipsis
if (ShouldAddEllipsis && targetTextSize == _mMinTextSize && textHeight > height)
{
// Draw using a static layout
// modified: use a copy of TextPaint for measuring
var paint = new TextPaint(textPaint);
// Draw using a static layout
var layout = new StaticLayout(text, paint, 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
var lastLine = layout.GetLineForVertical(height) - 1;
// If the text would not even fit on a single line, clear it
if (lastLine < 0)
{
Text = string.Empty;
}
// Otherwise, trim to the previous line and add an ellipsis
else
{
var start = layout.GetLineStart(lastLine);
var end = layout.GetLineEnd(lastLine);
var lineWidth = layout.GetLineWidth(lastLine);
var ellipseWidth = textPaint.MeasureText(Ellipsis);
// Trim characters off until we have enough room to draw the ellipsis
while (width < lineWidth + ellipseWidth)
{
lineWidth = textPaint.MeasureText(text.SubSequence(start, --end + 1));
}
Text = text.SubSequence(0, end) + Ellipsis;
}
}
}
// Some devices try to auto adjust line spacing, so force default line spacing
// and invalidate the layout as a side effect
SetTextSize(ComplexUnitType.Px, targetTextSize);
SetLineSpacing(_spacingAdd, _spacingMult);
var notifyArgs = new OnTextResizeEventArgs
{
TextView = this,
NewSize = targetTextSize,
OldSize = oldTextSize
};
// Notify the listener if registered
OnTextResize?.Invoke(this, notifyArgs);
// Reset force resize flag
_needsResize = false;
}
///
/// Set the text size of the text paint object and use a static layout to render text off screen before measuring
///
///
///
///
///
///
private int GetTextHeight(ICharSequence source, TextPaint paint, int width, float textSize)
{
// modified: make a copy of the original TextPaint object for measuring
// (apparently the object gets modified while measuring, see also the
// docs for TextView.getPaint() (which states to access it read-only)
// Update the text paint object
var paintCopy = new TextPaint(paint)
{
TextSize = textSize
};
// Measure using a static layout
var layout = new StaticLayout(source, paintCopy, width, Layout.Alignment.AlignNormal, _spacingMult, _spacingAdd, true);
return layout.Height;
}
}
}