在ImageSpan中垂直居中文本

53
我在文本中放置了一个 ImageSpan,我注意到周围的文本总是绘制在文本行的底部 -- 更准确地说,文本行的大小随着图像的大小而增长,但文本的基线不会向上移动。当图像明显大于文本大小时,效果相当不好看。 这里是一个示例,轮廓显示了TextView的边界: enter image description here 我试图使周围的文本与显示的图像垂直居中对齐。以下是带有蓝色文本的同一示例,显示所需位置: enter image description here 我遵循的约束条件如下:
- 我不能使用复合绘图。 图像必须能够在单词之间显示。 - 根据内容,文本可能是多行的。 这点我无法控制。 - 我的图像比周围的文本大,我不能缩小它们的大小。 虽然上面的示例图像大于实际图像(以展示当前行为),但实际图像仍然足够大,以至于这个问题是明显的。
我尝试使用TextView上的“android:gravity =" center_vertical "”属性,但这没有任何效果。 我认为这只是垂直居中文本,但在文本行中,文本仍然绘制在底部。
我的当前想法是创建一个自定义span,根据行高和当前文本大小移动文本的基线。 这个span将包围整个文本,并且我必须计算与的交点,以便避免移动图像。 这听起来相当困难,我希望有人可以提出另一种方法。
感谢您提供的所有帮助!

1
你弄清楚怎么做了吗? - WindsurferOak
1
我正在尝试实现相同的功能,但是ptilli的答案让它看起来像是在工作,但实际上并没有做到应该做的事情,即将文本与光标居中。如果我能想出什么东西,我会告诉你的。 - jbiral
1
以下答案仅适用于图像高度小于文本的情况。作者和我需要相反的技巧。 - ElSajko
@rds 我不是在尝试对齐图像跨度,所以我认为这不是重复的。 - Karakuri
11个回答

72

我的答案对第一个答案进行了微调。实际上,我已经尝试过上面的两种方法,并且我认为它们并没有真正居中垂直。如果将可绘制对象放置在ascentdescent之间而不是topbottom之间,会使其更加居中。至于第二个答案,它将可绘制对象的中心与文本的基线对齐,而不是该文本的中心。以下是我的解决方案:

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's 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方法,以保持可绘制文本的字体度量与其他文本相同,否则父视图将无法正确地包裹内容。

我也重新编写getSize方法,以确保绘制文本的字体测量与其他文本相同,否则父视图将无法正确地包裹内容。


这是最好的解决方案,也回答了我的问题。这是我的问题... http://stackoverflow.com/questions/31249475/cannot-set-image-and-text-in-center-vertical-android-pagertabstrip-android - otnieldocs
完美运行。确保在使用该类时提供上下文和资源名称。例如:ImageSpan imageSpan = new CenteredImageSpan(getApplicationContext(), R.drawable.ic_youricon)(与前面的示例不同)。 - logic
2
在4英寸设备上无法正常工作,480x800高密度像素图标底部被裁剪,文本显示正确,但在xxhdpi设备上可以正常工作。 - chin87
用这个draw替换原来的draw: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 transY = bottom - b.getBounds().bottom; // 这是关键 transY -= paint.getFontMetricsInt().descent / 2; canvas.translate(x, transY); b.draw(canvas); canvas.restore(); } - chin87
8
如果图标比文字更大,图标将被裁剪。 - chefish
我对下面的答案进行了微调,在我的情况下完美地工作。 - heng li

51

读完 TextView 的源代码后,我认为我们可以使用每个文本行的基线 "y",即“baseLine”。 即使您设置了 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();
    }

}

1
这个方法非常好,无论图像大小如何。大多数其他答案在图像大于测试时都会出现裁剪图像的问题。谢谢。 - Sojan P R
1
非常好用,感谢您花时间解决问题,这节省了我很多时间。 - KingKongCoder
这太棒了!壮丽!迷人! - sudoExclaimationExclaimation
所有的顶级答案都不太好用,除了这个。谢谢! - Oleksandr Albul
这个答案非常好! - undefined

39
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();
                }
            };

工作良好且简单。能否解释一下底部上方、x y 参数的含义?我无法理解,也没有文档说明。谢谢。 - Victor Choy
2
用这个draw替换@misaka-10032的draw,代码就可以在所有分辨率上运行。 - chin87

31

可能已经有点晚了,但我找到了一种方法来实现它,无论图像大小如何。您需要创建一个扩展ImageSpan的类,并覆盖方法getSize()getCachedDrawable()(我们不需要更改最后一个方法,但这个方法来自DynamicDrawableSpan是私有的,不能通过子类以其他方式访问)。在getSize(...)中,您可以重新定义DynamicDrawableSpan设置行的ascent/top/descent/bottom的方式,并实现您想要做的事情。

这是我的示例代码:

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;
}

如果你在那门课上遇到了任何问题,请告诉我!


1
你只需要将你之前使用的ImageSpan替换为CenteredImageSpan,它就会自动将图片与文本居中。 - jbiral
1
代码里有一个bug,入口变量在任何地方都不存在,然而你的代码却尝试使用它。 - ElSajko
3
@ElSajko,我刚尝试了这个解决方案,对于一个高度大于文本高度的图像,它似乎工作得很好。发布的代码确实有一些错误:在其中一个构造函数中删除“entry”变量,并在getSize()中将“=>”更改为“> =”。 - Karakuri
3
某种原因下,fm.descent = 3 * extraSpace / 8 + initialDescent; 能够正常工作,而 fm.descent = extraSpace / 2 + initialDescent; 则会将文本对齐到顶部。顺便提一下,您可以参考此教程来添加图像和文本:https://guides.codepath.com/android/google-play-style-tabs-using-tablayout - auroranil
3
请使用paint.getFontMetricsInt()代替fm。因为fm是一个中间变量。 - chefish
显示剩余9条评论

5

我创建了一个继承自ImageSpan的类来解决问题。

然后修改了从DynamicDrawableSpan继承而来的draw实现。至少在我的图片高度小于字体高度时,这个实现是有效的。不确定对于像你的更大的图片如何运作。

@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;

也许从本地化的角度来看这并不是一个好的实践,但对我来说可行。要将图像设置在文本中央,您自然需要使用跨度替换文本中的标记。


1
当您使用较大的图像时会发生什么? - Karakuri

3

我的答案微调了misaka-10032的回答,完美运作!

public static class CenteredImageSpan extends ImageSpan { private WeakReference 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;
    }
}

------------更新------------------------------------------------- 修复图片过大的问题

public static class CenteredImageSpan extends ImageSpan { private WeakReference mDrawableRef;

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

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

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

        if (fm != null) {
            int i = rect.height()/3;
            fm.ascent = -i*2;
            fm.descent = i;

            fm.top = fm.ascent;
            fm.bottom = fm.descent;
        }

        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 transY = top + (bottom - top) / 2 - (b.getBounds().height() / 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;
    }
}

完美运行。 - kish0n

2

该解决方案基于实际字母大小提供垂直居中。它支持使用大写字母和小写字母进行居中。例如,查看靠近字母的标记字符:X•。该解决方案可以实现类似的效果。

这是@WindRider答案的修改版本。同时,它使用Kotlin编写,并支持可绘制大小自定义。

创建此解决方案的原因是为了提供更好的视觉效果。许多其他解决方案使用字体上升。但在某些情况下,这似乎会引起视觉问题。例如,Android默认的Roboto字体具有比典型的大写字母顶部边界更高的上升值。因此,需要进行一些手动调整以正确地将图像居中。

class CenteredImageSpan(context: Context,
                        drawableRes: Int,
                        private val centerType: CenterType = CenterType.CAPITAL_LETTER,
                        private val customHeight: Int? = null,
                        private val customWidth: Int? = null) : ImageSpan(context, drawableRes) {

    private var mDrawableRef: WeakReference<Drawable?>? = null

    override fun getSize(paint: Paint, text: CharSequence,
                         start: Int, end: Int,
                         fontMetrics: FontMetricsInt?): Int {

        if (fontMetrics != null) {
            val currentFontMetrics = paint.fontMetricsInt
            // keep it the same as paint's Font Metrics
            fontMetrics.ascent = currentFontMetrics.ascent
            fontMetrics.descent = currentFontMetrics.descent
            fontMetrics.top = currentFontMetrics.top
            fontMetrics.bottom = currentFontMetrics.bottom
        }

        val drawable = getCachedDrawable()
        val rect = drawable.bounds
        return rect.right
    }

    override fun draw(canvas: Canvas,
                      text: CharSequence,
                      start: Int,
                      end: Int,
                      x: Float,
                      lineTop: Int,
                      baselineY: Int,
                      lineBottom: Int,
                      paint: Paint) {
        val cachedDrawable = getCachedDrawable()
        val drawableHeight = cachedDrawable.bounds.height()

        val relativeVerticalCenter = getLetterVerticalCenter(paint)

        val drawableCenter = baselineY + relativeVerticalCenter
        val drawableBottom = drawableCenter - drawableHeight / 2

        canvas.save()
        canvas.translate(x, drawableBottom.toFloat())
        cachedDrawable.draw(canvas)
        canvas.restore()
    }

    private fun getLetterVerticalCenter(paint: Paint): Int =
         when (centerType) {
            CenterType.CAPITAL_LETTER -> getCapitalVerticalCenter(paint)
            CenterType.LOWER_CASE_LETTER -> getLowerCaseVerticalCenter(paint)
        }

    private fun getCapitalVerticalCenter(paint: Paint): Int {
        val bounds = Rect()
        paint.getTextBounds("X", 0, 1, bounds)
        return (bounds.bottom + bounds.top) / 2
    }

    private fun getLowerCaseVerticalCenter(paint: Paint): Int {
        val bounds = Rect()
        paint.getTextBounds("x", 0, 1, bounds)
        return (bounds.bottom + bounds.top) / 2
    }


    // Redefined here because it's private in DynamicDrawableSpan
    private fun getCachedDrawable(): Drawable {

        val drawableWeakReference = mDrawableRef
        var drawable: Drawable? = null
        if (drawableWeakReference != null) drawable = drawableWeakReference.get()
        if (drawable == null) {
            drawable = getDrawable()!!

            val width = customWidth ?: drawable.intrinsicWidth
            val height = customHeight ?: drawable.intrinsicHeight

            drawable.setBounds(0, 0,
                               width, height)
            mDrawableRef = WeakReference(drawable)
        }
        return drawable

    }

    enum class CenterType {
        CAPITAL_LETTER, LOWER_CASE_LETTER
    }

}

0

您可以使用ImageSpan.ALIGN_CENTER。我在各种模拟器上进行了测试,似乎对于API<29的版本都有效。 但是,这仅适用于wrap_content宽度。从我的测试中得出的结论是,通过编程或在xml中分配宽度会破坏行高(?)


0
在创建Image span时,您必须添加DynamicDrawableSpan.ALIGN_CENTER垂直对齐标志。这应该将图像的中心与文本对齐。
val mySpannable = SpannableString("    $YourText")
mySpannable.setSpan(ImageSpan(yourDrawable, DynamicDrawableSpan.ALIGN_CENTER), 0, 1, 0)

-1

我的改进版本:可绘制字体度量相对于文本字体度量进行了缩放。这样就可以正确计算行间距。

@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;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接