Как избежать переноса текста в TextView для автоматического изменения размера текста?

Фон

После долгих поисков лучшего решения для автоматического изменения размера TextView (в соответствии с содержимым, размером, минимальными и максимальными строками и ограничениями по размеру шрифта) я сделал объединенное решение для всего этого, здесь.

ПРИМЕЧАНИЕ. Я не использую другие решения, потому что они работают плохо, у каждого свои проблемы (что-то не поддерживается, текст выходит за пределы TextView, текст усекается,...).

Демонстрация его работы:

https://raw.githubusercontent.com/AndroidDeveloperLB/AutoFitTextView/master/animationPreview.gif

Проблема

В некоторых случаях последний символ одной строки переносится на следующую строку, например:

введите описание изображения здесь

Зеленый — это границы TextView, красный — за его пределами.

Код

По сути, учитывая размер TextView, его минимальный и максимальный размер шрифта и минимальные и максимальные строки, а также содержимое (текст), которое должно быть внутри, он находит (используя двоичный поиск), какой размер шрифта должен соответствовать границам TextView.

Код уже доступен на Github, но вот на всякий случай:

public class AutoResizeTextView extends AppCompatTextView {
    private static final int NO_LINE_LIMIT = -1;
    private final RectF _availableSpaceRect = new RectF();
    private final SizeTester _sizeTester;
    private float _maxTextSize, _spacingMult = 1.0f, _spacingAdd = 0.0f, _minTextSize;
    private int _widthLimit, _maxLines;
    private boolean _initialized = false;
    private TextPaint _paint;

    private interface SizeTester {
        /**
         * @param suggestedSize  Size of text to be tested
         * @param availableSpace available space in which text must fit
         * @return an integer < 0 if after applying {@code suggestedSize} to
         * text, it takes less space than {@code availableSpace}, > 0
         * otherwise
         */
        int onTestSize(int suggestedSize, RectF availableSpace);
    }

    public AutoResizeTextView(final Context context) {
        this(context, null, android.R.attr.textViewStyle);
    }

    public AutoResizeTextView(final Context context, final AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

    public AutoResizeTextView(final Context context, final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);

        // using the minimal recommended font size
        _minTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics());
        _maxTextSize = getTextSize();
        _paint = new TextPaint(getPaint());
        if (_maxLines == 0)
            // no value was assigned during construction
            _maxLines = NO_LINE_LIMIT;
        // prepare size tester:
        _sizeTester = new SizeTester() {
            final RectF textRect = new RectF();

            @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
            @Override
            public int onTestSize(final int suggestedSize, final RectF availableSPace) {
                _paint.setTextSize(suggestedSize);
                final TransformationMethod transformationMethod = getTransformationMethod();
                final String text;
                if (transformationMethod != null)
                    text = transformationMethod.getTransformation(getText(), AutoResizeTextView.this).toString();
                else
                    text = getText().toString();

                final boolean singleLine = getMaxLines() == 1;
                if (singleLine) {
                    textRect.bottom = _paint.getFontSpacing();
                    textRect.right = _paint.measureText(text);
                } else {
                    final StaticLayout layout = new StaticLayout(text, _paint, _widthLimit, Alignment.ALIGN_NORMAL, _spacingMult, _spacingAdd, true);
                    // return early if we have more lines
                    if (getMaxLines() != NO_LINE_LIMIT && layout.getLineCount() > getMaxLines())
                        return 1;
                    textRect.bottom = layout.getHeight();
                    int maxWidth = -1;
                    for (int i = 0; i < layout.getLineCount(); i++)
                        if (maxWidth < layout.getLineRight(i) - layout.getLineLeft(i))
                            maxWidth = (int) layout.getLineRight(i) - (int) layout.getLineLeft(i);
                    textRect.right = maxWidth;
                }
                textRect.offsetTo(0, 0);
                if (availableSPace.contains(textRect))
                    // may be too small, don't worry we will find the best match
                    return -1;
                // else, too big
                return 1;
            }
        };
        _initialized = true;
    }

    @Override
    public void setAllCaps(boolean allCaps) {
        super.setAllCaps(allCaps);
        adjustTextSize();
    }

    @Override
    public void setTypeface(final Typeface tf) {
        super.setTypeface(tf);
        adjustTextSize();
    }

    @Override
    public void setTextSize(final float size) {
        _maxTextSize = size;
        adjustTextSize();
    }

    @Override
    public void setMaxLines(final int maxlines) {
        super.setMaxLines(maxlines);
        _maxLines = maxlines;
        adjustTextSize();
    }

    @Override
    public int getMaxLines() {
        return _maxLines;
    }

    @Override
    public void setSingleLine() {
        super.setSingleLine();
        _maxLines = 1;
        adjustTextSize();
    }

    @Override
    public void setSingleLine(final boolean singleLine) {
        super.setSingleLine(singleLine);
        if (singleLine)
            _maxLines = 1;
        else _maxLines = NO_LINE_LIMIT;
        adjustTextSize();
    }

    @Override
    public void setLines(final int lines) {
        super.setLines(lines);
        _maxLines = lines;
        adjustTextSize();
    }

    @Override
    public void setTextSize(final int unit, final float size) {
        final Context c = getContext();
        Resources r;
        if (c == null)
            r = Resources.getSystem();
        else r = c.getResources();
        _maxTextSize = TypedValue.applyDimension(unit, size, r.getDisplayMetrics());
        adjustTextSize();
    }

    @Override
    public void setLineSpacing(final float add, final float mult) {
        super.setLineSpacing(add, mult);
        _spacingMult = mult;
        _spacingAdd = add;
    }

    /**
     * Set the lower text size limit and invalidate the view
     *
     * @param minTextSize
     */
    public void setMinTextSize(final float minTextSize) {
        _minTextSize = minTextSize;
        adjustTextSize();
    }

    private void adjustTextSize() {
        // This is a workaround for truncated text issue on ListView, as shown here: https://github.com/AndroidDeveloperLB/AutoFitTextView/pull/14
        // TODO think of a nicer, elegant solution.
//    post(new Runnable()
//    {
//    @Override
//    public void run()
//      {
        if (!_initialized)
            return;
        final int startSize = (int) _minTextSize;
        final int heightLimit = getMeasuredHeight() - getCompoundPaddingBottom() - getCompoundPaddingTop();
        _widthLimit = getMeasuredWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
        if (_widthLimit <= 0)
            return;
        _paint = new TextPaint(getPaint());
        _availableSpaceRect.right = _widthLimit;
        _availableSpaceRect.bottom = heightLimit;
        superSetTextSize(startSize);
//      }
//    });
    }

    private void superSetTextSize(int startSize) {
        int textSize = binarySearch(startSize, (int) _maxTextSize, _sizeTester, _availableSpaceRect);
        super.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
    }

    private int binarySearch(final int start, final int end, final SizeTester sizeTester, final RectF availableSpace) {
        int lastBest = start, lo = start, hi = end - 1, mid;
        while (lo <= hi) {
            mid = lo + hi >>> 1;
            final int midValCmp = sizeTester.onTestSize(mid, availableSpace);
            if (midValCmp < 0) {
                lastBest = lo;
                lo = mid + 1;
            } else if (midValCmp > 0) {
                hi = mid - 1;
                lastBest = hi;
            } else return mid;
        }
        // make sure to return last best
        // this is what should always be returned
        return lastBest;
    }

    @Override
    protected void onTextChanged(final CharSequence text, final int start, final int before, final int after) {
        super.onTextChanged(text, start, before, after);
        adjustTextSize();
    }

    @Override
    protected void onSizeChanged(final int width, final int height, final int oldwidth, final int oldheight) {
        super.onSizeChanged(width, height, oldwidth, oldheight);
        if (width != oldwidth || height != oldheight)
            adjustTextSize();
    }
}

Вопрос

Почему это происходит? Что я могу сделать, чтобы исправить это?


person android developer    schedule 30.04.2016    source источник
comment
Если вы хотите показать текст до конца, сделайте ширину TextView как match_parent   -  person Ameer Faisal    schedule 30.04.2016
comment
Я не об этом спрашивал. Я спросил, что не так в алгоритме, который меняет размер шрифта в зависимости от размера TextView (а также другие вещи). Установка ширины TextView на match_parent не поможет, потому что размер контейнера может быть проблематичным. Вы говорите о тех, кто использует библиотеку, которую я сделал, а не о самой ошибке библиотеки.   -  person android developer    schedule 30.04.2016


Ответы (2)


Кажется, это возможно с помощью библиотеки поддержки:

    <TextView
        android:layout_width="250dp" android:layout_height="wrap_content" android:background="#f00"
        android:breakStrategy="balanced" android:hyphenationFrequency="none"
        android:text="This is an example text" android:textSize="30dp" app:autoSizeTextType="uniform"/>

К сожалению, у него есть 2 недостатка:

  1. Не всегда хорошо распространяет слова. Сообщено здесь.

  2. Требуется Android API 23 и выше (здесь ).

Дополнительная информация здесь.

person android developer    schedule 16.07.2018

У меня была аналогичная проблема в моем проекте. Давно гуглил, StackOverflow (с вашим вопросом среди прочих). Ничего такого.

И моим окончательным «решением» было BreakIterator для слов + измерить их все, чтобы проверить эту ситуацию.

ОБНОВЛЕНИЕ (10 августа 2018 г.):

static public boolean isTextFitWidth(final @Nullable String source, final @NonNull BreakIterator bi, final @NonNull TextPaint paint, final int width, final float textSize)
{
    if (null == source || source.length() <= 0) {
        return true;
    }

    TextPaint paintCopy = new TextPaint();
    paintCopy.set(paint);
    paintCopy.setTextSize(textSize);

    bi.setText(source);
    int start = bi.first();
    for (int end = bi.next(); BreakIterator.DONE != end; start = end, end = bi.next()) {
        int wordWidth = (int)Math.ceil(paintCopy.measureText(source, start, end));
        if (wordWidth > width) {
            return false;
        }
    }
    return true;
}

static public boolean isTextFitWidth(final @NonNull TextView textView, final @NonNull BreakIterator bi, final int width, final @Nullable Float textSize)
{
    final int textWidth = width - textView.getPaddingLeft() - textView.getPaddingRight();
    return isTextFitWidth(textView.getText().toString(), bi, textView.getPaint(), textWidth,
            null != textSize ? textSize : textView.getTextSize());
}
person kpower    schedule 12.09.2016
comment
Я думаю, что нужно еще немного поработать, чтобы использовать его в самом TextView, чтобы он учитывал все свойства TextView. Но идея вроде норм. Вы, вероятно, пробуете несколько тестов, пока он не будет соответствовать ширине, верно? Но как вы используете BreakIterator ? - person android developer; 11.08.2018
comment
@androiddeveloper на самом деле я хотел показать вам окончательный компонент, но он был разработан в соответствии с соглашением о неразглашении, и мне было запрещено его публиковать ... В моей реализации я создал подкласс TextView, создал BreakIterator с BreakIterator.getWordInstance() в качестве окончательного частного свойства и передал методы из ответа выше. И да, чтобы получить окончательный размер, я провожу несколько тестов, подобных алгоритму быстрой сортировки, и каждый раз измеряю текст. Я делаю это на каждом onDraw тике, но с обязательной проверкой (размер или текст/подсказка, когда текста нет). - person kpower; 11.08.2018
comment
Так что этого кода, к сожалению, недостаточно. Он пропускает очень важную часть, чтобы заставить его работать... - person android developer; 11.08.2018