简体   繁体   English

围绕 ImageSpan 中心垂直对齐文本

[英]Align text around ImageSpan center vertical

I have an ImageSpan inside of a piece of text.我在一段文本中有一个ImageSpan What I've noticed is that the surrounding text is always drawn at the bottom of the text line -- to be more precise, the size of the text line grows with the image but the baseline of the text does not shift upward.我注意到周围的文本总是绘制在文本行的底部——更准确地说,文本行的大小随着图像的增长而增长,但文本的基线不会向上移动。 When the image is noticeably larger than the text size, the effect is rather unsightly.当图像明显大于文本大小时,效果相当难看。

Here is a sample, the outline shows bounds of the TextView :这是一个示例,大纲显示了TextView边界:在此处输入图片说明

I am trying to have the surrounding text be centered vertically with respect to the image being displayed.我试图让周围的文本相对于显示的图像垂直居中。 Here is the same sample with blue text showing the desired location:以下是显示所需位置的蓝色文本示例:

在此处输入图片说明

Here are the constraints that I'm bound by:以下是我受到的约束:

  • I cannot use compound drawables.我不能使用复合绘图。 The images must be able to be shown between words.图像必须能够在单词之间显示。
  • The text may be multiline depending on the content.根据内容,文本可能是多行的。 I have no control over this.我对此无能为力。
  • My images are larger than the surrounding text and I cannot reduce their size.我的图像比周围的文本大,我无法缩小它们的大小。 While the sample image above is larger than the actual images (to demonstrate the current behavior), the actual images are still large enough that this problem is noticeable.虽然上面的示例图像比实际图像大(以演示当前行为),但实际图像仍然足够大,这个问题很明显。

I've tried using the android:gravity="center_vertical" attribute on the TextView, but this does not have any effect.我试过在 TextView 上使用android:gravity="center_vertical"属性,但这没有任何效果。 I believe this just vertically centers the text lines , but within the text line the text is still drawn at the bottom.我相信这只是使文本垂直居中,但在文本行内,文本仍绘制在底部。

My current train of thought is to create a custom span that shifts the baseline of the text based on the height of the line and the current text size.我目前的思路是创建一个自定义跨度,根据行高和当前文本大小移动文本的基线。 This span would encompass the entire text, and I would have to compute the intersection with the ImageSpan s so I can avoid shifting the images as well.这个跨度将包含整个文本,我必须计算与ImageSpan的交集,这样我也可以避免移动图像。 This sounds rather daunting and I'm hoping someone can suggest another approach.这听起来相当令人生畏,我希望有人可以提出另一种方法。

Any and all help is appreciated!任何和所有的帮助表示赞赏!

My answer tweaks the first answer.我的答案调整了第一个答案。 Actually I have tried both two methods above, and I don't think they are really center vertical.其实上面两种方法我都试过,我觉得它们不是真正的居中垂直。 It would make the drawable more center if it's placed in between ascent and descent , rather than top and bottom .如果将 drawable 放置在ascentdescent之间,而不是topbottom之间,则会使 drawable 更加居中。 So as to the second answer, it aligns the center of the drawable to the baseline of the text, rather than the center of that text.至于第二个答案,它将可绘制对象的中心与文本的基线对齐,而不是该文本的中心。 Here's my solution:这是我的解决方案:

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

I also rewrite getSize to keep the FontMetrics of drawable the same as other text, otherwise the parent view won't wrap the content correctly.我还重写了getSize以保持 drawable 的 FontMetrics 与其他文本相同,否则父视图将无法正确包装内容。

After reading the source code of TextView, I think we can use the baseLine of eache text line which is "y".看了TextView的源码后,我觉得我们可以使用每个文本行的baseLine为“y”。 And it will work even if you set lineSpaceExtra.即使您设置了 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();
    }

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

It might be a bit late but I've found a way to do it, no matter the image size.可能有点晚了,但无论图像大小如何,我都找到了一种方法。 You need to create a class extending ImageSpan and override the methods getSize() and getCachedDrawable() (we don't need to change the last one, but this method from DynamicDrawableSpan is private and cannot be accessed in another way from the child class).您需要创建一个扩展 ImageSpan 的类并覆盖方法getSize()getCachedDrawable() (我们不需要更改最后一个,但DynamicDrawableSpan此方法是私有的,不能从子类以其他方式访问) . In getSize(...) , you can then redefined the way DynamicDrawableSpan set the ascent/top/descent/bottom of the line and achieve what you want to do.getSize(...) ,您可以重新定义DynamicDrawableSpan设置线的上升/顶部/下降/底部的方式并实现您想要执行的操作。

Here's my class example:这是我的课堂示例:

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

Let me know if you have any trouble with that class!如果您在该课程中遇到任何问题,请告诉我!

I got a working solution by creating a class that inherits from ImageSpan .通过创建一个继承自ImageSpan的类,我得到了一个可行的解决方案。

Then modified draw implementation from DynamicDrawableSpan.然后从 DynamicDrawableSpan 修改绘制实现。 At least this implementation works when my image height is less than font height.至少当我的图像高度小于字体高度时,这个实现是有效的。 Not sure how this works for bigger images like yours.不确定这如何适用于像您这样的更大图像。

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

Also had to reuse implementation from DynamicDrawableSpan as it was private.还必须重用 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;

And this is how I use it as static method that inserts image in front of the text.这就是我如何使用它作为在文本前面插入图像的静态方法。

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;

Maybe not a good practice there considering localization, but works for me.考虑到本地化,这可能不是一个好的做法,但对我有用。 To set images in the middle of the text, you'd naturally need to replace tokens in text with spans.要在文本中间设置图像,您自然需要用跨度替换文本中的标记。

My answer tweaks the misaka-10032 answer.我的回答调整了 misaka-10032 的回答。 work perfect!工作完美!

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

This solution provides a vertical centering based on actual letter size.此解决方案提供了基于实际字母大小的垂直居中。 It supports centering using capital letters and lower-case letters.它支持使用大写字母和小写字母居中。 For example, look at the marker character near a letter: X•.例如,查看字母附近的标记字符:X•。 This solution achieves a similar effect.该解决方案达到了类似的效果。

This is a modified version of @WindRider's answer.这是@WindRider 答案的修改版本。 Also, it's in Kotlin.此外,它在 Kotlin 中。 And it supports drawable size customization.并且它支持可绘制尺寸自定义。

The reason why this solution is created is to provide a better visual result.创建此解决方案的原因是为了提供更好的视觉效果。 A lot of other solutions use font ascent.许多其他解决方案使用字体上升。 But it appears to cause visual problems in some cases.但在某些情况下,它似乎会导致视觉问题。 Android's default Roboto font, for example, has ascent higher than a typical capital letter top border.例如,Android 的默认 Roboto 字体比典型的大写字母顶部边框具有更高的上升度。 And because of it, some manual adjustments were needed to properly center an image.正因为如此,需要进行一些手动调整才能使图像正确居中。

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
    }

}

While creating your Image span you have to add Vertical Alignment Flag DynamicDrawableSpan.ALIGN_CENTER.在创建图像跨度时,您必须添加垂直对齐标志 DynamicDrawableSpan.ALIGN_CENTER。 That should align the image's center to the text.这应该将图像的中心与文本对齐。

val mySpannable = SpannableString("    $YourText")
mySpannable.setSpan(ImageSpan(yourDrawable, DynamicDrawableSpan.ALIGN_CENTER), 0, 1, 0)

You can use ImageSpan.ALIGN_CENTER.您可以使用 ImageSpan.ALIGN_CENTER。 I tested it on various emulators and it seems to work for APIs < 29. This only works good with wrap_content width though.我在各种模拟器上对其进行了测试,它似乎适用于小于 29 的 API。不过,这只适用于 wrap_content 宽度。 From my tests assigning a width programmatically or in xml, breaks the line height (?)从我以编程方式或在 xml 中分配宽度的测试中,打破了行高 (?)

My improved version: drawable font metrics zoomed relative to text font metrics.我的改进版本:可绘制字体指标相对于文本字体指标进行了缩放。 So that line spacing will be calculate correctly.这样行间距将被正确计算。

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

This solution works.此解决方案有效。 I have tested it and am using it for sometime.我已经对其进行了测试并使用了一段时间。 It doesn't consider the ascent and decent but it Aligns the drawable in the center.它不考虑上升和体面,但它在中心对齐可绘制对象。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM