Подтвердить что ты не робот

Выровнять текст вокруг вертикальной оси ImageSpan

У меня есть ImageSpan внутри фрагмента текста. Я заметил, что окружающий текст всегда рисуется в нижней части текстовой строки - точнее, размер текстовой строки растет с изображением, но исходный текст не смещается вверх. Когда изображение заметно больше размера текста, эффект довольно неприглядный.

Вот пример, схема показывает границы TextView: enter image description here

Я пытаюсь окружить окружающий текст по вертикали относительно отображаемого изображения. Вот такой же образец с синим текстом, показывающим желаемое местоположение:

enter image description here

Вот ограничения, с которыми я связан:

  • Я не могу использовать составные чертежи. Изображения должны быть показаны между словами.
  • Текст может быть многострочным в зависимости от содержимого. Я не контролирую это.
  • Мои изображения больше, чем окружающий текст, и я не могу уменьшить их размер. В то время как изображение образца выше, чем фактические изображения (для демонстрации текущего поведения), фактические изображения по-прежнему достаточно велики, чтобы эта проблема была заметна.

Я попытался использовать атрибут android:gravity="center_vertical" в TextView, но это не имеет никакого эффекта. Я считаю, что это просто вертикально центрирует текстовые строки, но внутри текстовой строки текст по-прежнему рисуется внизу.

Мой текущий ход мысли заключается в создании настраиваемого диапазона, который сдвигает базовую линию текста в зависимости от высоты строки и текущего размера текста. Этот диапазон охватывал бы весь текст, и мне пришлось бы вычислить пересечение с ImageSpan, чтобы я мог избежать смещения изображений. Это звучит довольно сложно, и я надеюсь, что кто-то может предложить другой подход.

Любая помощь приветствуется!

4b9b3361

Ответ 1

Это может быть немного поздно, но я нашел способ сделать это, независимо от размера изображения. Вам нужно создать класс, расширяющий ImageSpan, и переопределить методы getSize() и getCachedDrawable() (нам не нужно изменять последний, но этот метод из DynamicDrawableSpan является закрытым и не может быть доступен другим способом из дочернего класса), В getSize(...) вы можете затем переопределить способ, которым DynamicDrawableSpan устанавливает подъем/верх/спуск/низ линии и достигает того, что вы хотите сделать.

Вот мой пример класса:

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;

import java.lang.ref.WeakReference;

public class CenteredImageSpan extends ImageSpan {

    // Extra variables used to redefine the Font Metrics when an ImageSpan is added
    private int initialDescent = 0;
    private int extraSpace = 0;

    public CenteredImageSpan(final Drawable drawable) {
        this(drawable, DynamicDrawableSpan.ALIGN_BOTTOM);
    }

    public CenteredImageSpan(final Drawable drawable, final int verticalAlignment) {
        super(drawable, verticalAlignment);
    }

    @Override
    public void draw(Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, Paint paint) {
        getDrawable().draw(canvas);
    }

    // Method used to redefined the Font Metrics when an ImageSpan is added
    @Override
    public int getSize(Paint paint, CharSequence text,
                       int start, int end,
                       Paint.FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();

        if (fm != null) {
            // Centers the text with the ImageSpan
            if (rect.bottom - (fm.descent - fm.ascent) >= 0) {
                // Stores the initial descent and computes the margin available
                initialDescent = fm.descent;
                extraSpace = rect.bottom - (fm.descent - fm.ascent);
            }

            fm.descent = extraSpace / 2 + initialDescent;
            fm.bottom = fm.descent;

            fm.ascent = -rect.bottom + fm.descent;
            fm.top = fm.ascent;
        }

        return rect.right;
    }

    // Redefined locally because it is a private member from DynamicDrawableSpan
    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;

        if (wr != null)
            d = wr.get();

        if (d == null) {
            d = getDrawable();
            mDrawableRef = new WeakReference<>(d);
        }

        return d;
    }

    private WeakReference<Drawable> mDrawableRef;
}

Дайте мне знать, если у вас возникнут проблемы с этим классом!

Ответ 2

Мой ответ изменяет первый ответ. На самом деле я попробовал оба метода выше, и я не думаю, что они действительно вертикальные по центру. Это сделало бы более привлекательным, если бы оно помещалось между ascent и descent, а не top и bottom. Что касается второго ответа, он выравнивает центр доступного к базовой линии текста, а не центр этого текста. Здесь мое решение:

public class CenteredImageSpan extends ImageSpan {
  private WeakReference<Drawable> mDrawableRef;

  public CenteredImageSpan(Context context, final int drawableRes) {
    super(context, drawableRes);
  }

  @Override
  public int getSize(Paint paint, CharSequence text,
                     int start, int end,
                     Paint.FontMetricsInt fm) {
    Drawable d = getCachedDrawable();
    Rect rect = d.getBounds();

    if (fm != null) {
      Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
      // keep it the same as paint fm
      fm.ascent = pfm.ascent;
      fm.descent = pfm.descent;
      fm.top = pfm.top;
      fm.bottom = pfm.bottom;
    }

    return rect.right;
  }

  @Override
  public void draw(@NonNull Canvas canvas, CharSequence text,
                   int start, int end, float x,
                   int top, int y, int bottom, @NonNull Paint paint) {
    Drawable b = getCachedDrawable();
    canvas.save();

    int drawableHeight = b.getIntrinsicHeight();
    int fontAscent = paint.getFontMetricsInt().ascent;
    int fontDescent = paint.getFontMetricsInt().descent;
    int transY = bottom - b.getBounds().bottom +  // align bottom to bottom
        (drawableHeight - fontDescent + fontAscent) / 2;  // align center to center

    canvas.translate(x, transY);
    b.draw(canvas);
    canvas.restore();
  }

  // Redefined locally because it is a private member from DynamicDrawableSpan
  private Drawable getCachedDrawable() {
    WeakReference<Drawable> wr = mDrawableRef;
    Drawable d = null;

    if (wr != null)
      d = wr.get();

    if (d == null) {
      d = getDrawable();
      mDrawableRef = new WeakReference<>(d);
    }

    return d;
  }
}

Я также переписываю getSize, чтобы сохранить шрифты FontMetrics одинаковыми с другими текстами, в противном случае родительский вид не будет корректно завершать контент.

Ответ 3

ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM) {
                public void draw(Canvas canvas, CharSequence text, int start,
                        int end, float x, int top, int y, int bottom,
                        Paint paint) {
                    Drawable b = getDrawable();
                    canvas.save();

                    int transY = bottom - b.getBounds().bottom;
                    // this is the key 
                    transY -= paint.getFontMetricsInt().descent / 2;

                    canvas.translate(x, transY);
                    b.draw(canvas);
                    canvas.restore();
                }
            };

Ответ 4

Прочитав исходный код TextView, я думаю, что мы можем использовать baseLine текстовой строки eache, которая является "y". И он будет работать, даже если вы установите lineSpaceExtra.

public class VerticalImageSpan extends ImageSpan {

    public VerticalImageSpan(Drawable drawable) {
        super(drawable);
    }

    /**
     * update the text line height
     */
    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end,
                       Paint.FontMetricsInt fontMetricsInt) {
        Drawable drawable = getDrawable();
        Rect rect = drawable.getBounds();
        if (fontMetricsInt != null) {
            Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
            int fontHeight = fmPaint.descent - fmPaint.ascent;
            int drHeight = rect.bottom - rect.top;
            int centerY = fmPaint.ascent + fontHeight / 2;

            fontMetricsInt.ascent = centerY - drHeight / 2;
            fontMetricsInt.top = fontMetricsInt.ascent;
            fontMetricsInt.bottom = centerY + drHeight / 2;
            fontMetricsInt.descent = fontMetricsInt.bottom;
        }
        return rect.right;
    }

    /**
     * see detail message in android.text.TextLine
     *
     * @param canvas the canvas, can be null if not rendering
     * @param text the text to be draw
     * @param start the text start position
     * @param end the text end position
     * @param x the edge of the replacement closest to the leading margin
     * @param top the top of the line
     * @param y the baseline
     * @param bottom the bottom of the line
     * @param paint the work paint
     */
    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end,
                     float x, int top, int y, int bottom, Paint paint) {

        Drawable drawable = getDrawable();
        canvas.save();
        Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
        int fontHeight = fmPaint.descent - fmPaint.ascent;
        int centerY = y + fmPaint.descent - fontHeight / 2;
        int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
        canvas.translate(x, transY);
        drawable.draw(canvas);
        canvas.restore();
    }

}

Ответ 5

Я получил рабочее решение, создав класс, который наследуется от ImageSpan.

Затем модифицируется реализация draw от DynamicDrawableSpan. По крайней мере, эта реализация работает, когда высота моего изображения меньше высоты шрифта. Не знаете, как это работает для больших изображений, подобных вашим.

@Override
public void draw(Canvas canvas, CharSequence text,
    int start, int end, float x,
    int top, int y, int bottom, Paint paint) {
    Drawable b = getCachedDrawable();
    canvas.save();

    int bCenter = b.getIntrinsicHeight() / 2;
    int fontTop = paint.getFontMetricsInt().top;
    int fontBottom = paint.getFontMetricsInt().bottom;
    int transY = (bottom - b.getBounds().bottom) -
        (((fontBottom - fontTop) / 2) - bCenter);


    canvas.translate(x, transY);
    b.draw(canvas);
    canvas.restore();
}

Также пришлось повторно использовать реализацию из DynamicDrawableSpan, поскольку она была закрытой.

private Drawable getCachedDrawable() {
    WeakReference<Drawable> wr = mDrawableRef;
    Drawable d = null;

    if (wr != null)
        d = wr.get();

    if (d == null) {
        d = getDrawable();
        mDrawableRef = new WeakReference<Drawable>(d);
    }

    return d;
}

private WeakReference<Drawable> mDrawableRef;

И вот как я использую его как статический метод, который вставляет изображение перед текстом.

public static CharSequence formatTextWithIcon(Context context, String text,
    int iconResourceId) {
    SpannableStringBuilder sb = new SpannableStringBuilder("X");

    try {
        Drawable d = context.getResources().getDrawable(iconResourceId);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 
        CenteredImageSpan span = new CenteredImageSpan(d); 
        sb.setSpan(span, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        sb.append(" " + text); 
    } catch (Exception e) {
        e.printStackTrace();
        sb.append(text); 
    }

    return sb;

Возможно, это не хорошая практика, связанная с локализацией, но работает для меня. Чтобы установить изображения в середине текста, вам, естественно, нужно будет заменить токены в тексте пробелами.

Ответ 6

Мой ответ подправляет ответ misaka-10032. работать отлично!

public static class CenteredImageSpan extends ImageSpan {
    private WeakReference<Drawable> mDrawableRef;

    CenteredImageSpan(Context context, final int drawableRes) {
        super(context, drawableRes);
    }

    public CenteredImageSpan(@NonNull Drawable d) {
        super(d);
    }

    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text,
                     int start, int end, float x,
                     int top, int y, int bottom, @NonNull Paint paint) {
        Drawable b = getCachedDrawable();
        canvas.save();
        int transY = top + (bottom - top - b.getBounds().bottom)/2;
        canvas.translate(x, transY);
        b.draw(canvas);
        canvas.restore();
    }

    // Redefined locally because it is a private member from DynamicDrawableSpan
    private Drawable getCachedDrawable() {
        WeakReference<Drawable> wr = mDrawableRef;
        Drawable d = null;

        if (wr != null)
            d = wr.get();

        if (d == null) {
            d = getDrawable();
            mDrawableRef = new WeakReference<>(d);
        }

        return d;
    }
}

Ответ 7

Моя улучшенная версия: масштабируемые метрики шрифта увеличены по сравнению с метриками текстового шрифта. Таким образом, расстояние между линиями будет правильно подсчитано.

@Override
public int getSize(Paint paint, CharSequence text,
                   int start, int end,
                   Paint.FontMetricsInt fm) {
    Drawable d = getCachedDrawable();
    Rect rect = d.getBounds();
    float drawableHeight = Float.valueOf(rect.height());


    if (fm != null) {
        Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
        float fontHeight = pfm.descent - pfm.ascent;
        float ratio = drawableHeight / fontHeight;

        fm.ascent = Float.valueOf(pfm.ascent * ratio).intValue();
        fm.descent = Float.valueOf(pfm.descent * ratio).intValue();
        fm.top = fm.ascent;
        fm.bottom = fm.descent;
    }

Ответ 8

Это решение работает. Я проверил это и использую это в течение некоторого времени. Это не считает подъем и приличный, но выравнивает рисование по центру.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.text.style.ImageSpan;

import java.lang.ref.WeakReference;

public class CustomImageSpan extends ImageSpan {

  /**
   * A constant indicating that the center of this span should be aligned
   * with the center of the surrounding text
   */
  public static final int ALIGN_CENTER = -12;
  private WeakReference<Drawable> mDrawable;
  private int mAlignment;

  public CustomImageSpan(Context context, final int drawableRes, int alignment) {
    super(context, drawableRes);
    mAlignment = alignment;
  }

  @Override
  public int getSize(Paint paint, CharSequence text,
                     int start, int end,
                     Paint.FontMetricsInt fm) {
    Drawable d = getCachedDrawable();
    Rect rect = d.getBounds();
    if (fm != null) {
      Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
      fm.ascent = pfm.ascent;
      fm.descent = pfm.descent;
      fm.top = pfm.top;
      fm.bottom = pfm.bottom;
    }
    return rect.right;
  }

  @Override
  public void draw(@NonNull Canvas canvas, CharSequence text,
                   int start, int end, float x,
                   int top, int y, int bottom, @NonNull Paint paint) {
    if (mAlignment == ALIGN_CENTER) {
      Drawable cachedDrawable = getCachedDrawable();
      canvas.save();
      //Get the center point and set the Y coordinate considering the drawable height for aligning the icon vertically
      int transY = ((top + bottom) / 2) - cachedDrawable.getIntrinsicHeight() / 2;
      canvas.translate(x, transY);
      cachedDrawable.draw(canvas);
      canvas.restore();
    } else {
      super.draw(canvas, text, start, end, x, top, y , bottom, paint);
    }
  }

  // Redefined locally because it is a private member from DynamicDrawableSpan
  private Drawable getCachedDrawable() {
    WeakReference<Drawable> wr = mDrawable;
    Drawable d = null;
    if (wr != null) {
      d = wr.get();
    }
    if (d == null) {
      d = getDrawable();
      mDrawable = new WeakReference<>(d);
    }
    return d;
  }
}