对齐ImageSpan中心垂直周围的文本

时间:2014-09-02 16:36:40

标签: android text spannable imagespan

我在一段文字中有一个ImageSpan。我注意到的是,周围的文本总是在文本行的底部绘制 - 更准确地说,文本行的大小随着图像的增长而增加,但文本的基线不会向上移动。当图像明显大于文本大小时,效果相当难看。

以下是一个示例,大纲显示TextView的边界: enter image description here

我试图让周围的文字相对于正在显示的图像垂直居中。以下是蓝色文本显示所需位置的相同示例:

enter image description here

以下是我受约束的约束:

  • 我不能使用复合drawables。图像必须能够在单词之间显示。
  • 根据内容,文字可能是多行的。我无法控制这一点。
  • 我的图片比周围的文字大,我无法减小尺寸。虽然上面的示例图像比实际图像大(为了演示当前行为),但实际图像仍然足够大,以至于这个问题很明显。

我尝试在TextView上使用android:gravity="center_vertical"属性,但这没有任何效果。我相信这只是文本的垂直居中,但在文本行中,文本仍然在底部绘制。

我目前的思路是创建一个自定义范围,根据线条的高度和当前文字大小来移动文本的基线。这个范围将涵盖整个文本,我将不得不计算与ImageSpan的交集,所以我也可以避免移动图像。这听起来相当令人生畏,我希望有人能提出另一种方法。

感谢任何和所有帮助!

11 个答案:

答案 0 :(得分:58)

我的回答调整了第一个答案。实际上我已经尝试了上述两种方法,我认为它们并不是真正的中心垂直方向。如果它位于ascentdescent之间,而不是topbottom,它会使drawable更加居中。因此,对于第二个答案,它将drawable的中心与文本的基线对齐,而不是该文本的中心。这是我的解决方案:

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以使FontMetrics的drawable与其他文本保持一致,否则父视图将无法正确包装内容。

答案 1 :(得分:26)

可能有点晚了,但无论图像大小如何,我都找到了一种方法。您需要创建一个扩展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 :(得分:21)

while(true) {
    auto file = CreateFileA(
        "test.txt",
        GENERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (file == INVALID_HANDLE_VALUE) {
        std::cerr << "create failed: " << std::error_code(GetLastError(), 
            std::system_category()).message() << std::endl;
        continue;
    }

    if (!CloseHandle(file)) 
        std::cerr << "close failed: " << std::error_code(GetLastError(), 
            std::system_category()).message() << std::endl;

    if (!DeleteFileA("test.txt"))
        std::cerr << "delete failed: " << std::error_code(GetLastError(), 
            std::system_category()).message() << std::endl;
}

答案 3 :(得分:18)

在阅读了TextView的源代码之后,我想我们可以使用eache文本行的baseLine,它是&#34; y&#34;。 即使你设置了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();
    }

}

答案 4 :(得分:4)

我通过创建一个继承自ImageSpan的类来获得一个有效的解决方案。

然后从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;

考虑到本地化,也许不是一个好习惯,但对我有用。要在文本中间设置图像,您自然需要使用跨度替换文本中的标记。

答案 5 :(得分:1)

此解决方案根据实际字母大小提供垂直居中。它支持使用大写字母和小写字母居中。例如,查看字母附近的标记字符:X•。该解决方案可以达到类似的效果。

这是@WindRider答案的修改版本。另外,它在科特林。它支持可绘制尺寸的自定义。

创建此解决方案的原因是为了提供更好的视觉效果。许多其他解决方案都使用字体提升。但是在某些情况下,它似乎会引起视觉问题。例如,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
    }

}

答案 6 :(得分:0)

我的答案调整了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 :(得分:0)

在创建图像跨度时,必须添加垂直对齐标记DynamicDrawableSpan.ALIGN_CENTER。那应该使图像的中心与文本对齐。

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

答案 8 :(得分:0)

您可以使用 ImageSpan.ALIGN_CENTER。我在各种模拟器上对其进行了测试,它似乎适用于 API <29

答案 9 :(得分:-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;
    }

答案 10 :(得分:-1)

此解决方案有效。我测试了它并且正在使用它一段时间。它不考虑上升和体面,但它在中心对齐drawable。

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