【问题标题】:Align text around ImageSpan center vertical围绕 ImageSpan 中心垂直对齐文本
【发布时间】:2014-10-27 00:08:44
【问题描述】:

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

这是一个示例,轮廓显示了TextView 的边界:

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

以下是我受约束的约束:

  • 我不能使用复合绘图。图片必须能够在单词之间显示。
  • 文本可能是多行的,具体取决于内容。我无法控制。
  • 我的图像比周围的文字大,我无法缩小它们的大小。虽然上面的示例图片比实际图片大(以演示当前行为),但实际图片仍然足够大,这个问题很明显。

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

我目前的思路是创建一个自定义跨度,根据行高和当前文本大小移动文本的基线。这个跨度将包含整个文本,我必须计算与ImageSpans 的交集,这样我也可以避免移动图像。这听起来相当令人生畏,我希望有人能提出另一种方法。

感谢您的任何帮助!

【问题讨论】:

  • 你知道怎么做吗?
  • 我正在寻找相同的东西,但是,ptilli 的答案使它看起来像是在工作,但它并没有真正做应该做的事情,即用光标使文本居中.如果我能弄清楚什么,我会告诉你的
  • 以下答案仅适用于高度小于文本的图像。作者和我需要相反的技巧
  • @rds 我不想对齐图像跨度,所以我不认为这是重复的。

标签: android text spannable imagespan


【解决方案1】:

我的答案调整了第一个答案。其实我上面两种方法我都试过了,我不认为它们真的是中心垂直的。如果将drawable放在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 以保持drawable的FontMetrics与其他文本相同,否则父视图将无法正确包装内容。

【讨论】:

  • 这也是回答我问题的最佳解决方案。这是我的问题...stackoverflow.com/questions/31249475/…
  • 完美运行。确保在使用类时提供上下文和资源名称。示例:ImageSpan imageSpan = new CenteredImageSpan(getApplicationContext(), R.drawable.ic_youricon)(与前面的示例不同)
  • 不能在 4" 设备上工作 480x800 hdpi 图标在底部被剪切 文本显示正确,但在 xxhdpi 设备上工作
  • 用此抽奖替换抽奖: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; // this is the key transY -= paint.getFontMetricsInt().descent / 2; canvas.translate(x, transY); b.draw(canvas); canvas.restore(); }
  • 如果图标比文字大,图标会被剪掉
【解决方案2】:

看了TextView的源码后,我想我们可以使用每个文本行的baseLine,也就是“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();
    }

}

【讨论】:

  • 无论图像大小如何,这都非常有效。当图像大于测试时,大多数其他答案都存在切割图像的问题。谢谢
  • 像魅力一样工作,感谢您抽出时间并弄清楚它节省了我的一些时间。
  • 这太棒了!壮丽的!迷人!
  • 所有热门答案都不好用,除了这个。谢谢!
【解决方案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();
                }
            };

【讨论】:

  • 运行良好且简单。能解释一下底部顶部,x y 参数的含义吗?我无法理解,也没有文档解释。谢谢
  • 用这个draw替换来自@misaka-10032的draw,代码适用于所有分辨率
【解决方案4】:

可能有点晚了,但我已经找到了一种方法来做到这一点,无论图像大小如何。您需要创建一个扩展 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;
}

如果你对这门课有任何问题,请告诉我!

【讨论】:

  • 您只需将您使用的 ImageSpan 替换为 CenteredImageSpan,它会自动将您的图像与文本居中
  • 有一个bug,入口变量在任何地方都不存在,但是你的代码尝试使用它。
  • @ElSajko 我刚刚尝试了这个解决方案,它似乎适用于大于文本高度的图像。发布的代码确实有一些错误:删除其中一个构造函数中的“entry”变量,并将getSize()中的“=>”更改为“>=”
  • 出于某种原因,fm.descent = 3 * extraSpace / 8 + initialDescent; 有效,fm.descent = extraSpace / 2 + initialDescent; 将文本与顶部对齐。按照本教程顺便添加图像和文本:guides.codepath.com/android/…
  • 请用paint.getFontMetricsInt()代替fm。因为fm是一个中间变量
【解决方案5】:

通过创建一个继承自 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;

考虑到本地化可能不是一个好的做法,但对我有用。要在文本中间设置图像,您自然需要将文本中的标记替换为 span。

【讨论】:

  • 使用较大的图像会发生什么?
【解决方案6】:

我的答案调整了 misaka-10032 的答案。完美运行!

公共静态类 CenteredImageSpan 扩展 ImageSpan { 私有 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;
    }
}

------------更新 ---------------------------------- --------------- 当图片变大时修复

公共静态类 CenteredImageSpan 扩展 ImageSpan { 私有 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;
    }
}

【讨论】:

    【解决方案7】:

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

    这是@WindRider 答案的修改版本。此外,它在 Kotlin 中。并且支持drawable size自定义。

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

    【讨论】:

      【解决方案8】:

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

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

      【讨论】:

        【解决方案9】:

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

        【讨论】:

          【解决方案10】:

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

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

          【讨论】:

            【解决方案11】:

            此解决方案有效。我已经对其进行了测试并使用了一段时间。它不考虑上升和体面,但它在中心对齐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;
              }
            }
            

            【讨论】:

              猜你喜欢
              • 2018-11-12
              • 2011-03-18
              • 2012-08-15
              • 2020-01-23
              • 2020-06-25
              • 1970-01-01
              • 2019-02-09
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多