Android 围绕 ImageSpan 中心垂直对齐文本
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/25628258/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Align text around ImageSpan center vertical
提问by Karakuri
I have an ImageSpan
inside of a piece of text. 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.
我有ImageSpan
一段文字的内部。我注意到周围的文本总是绘制在文本行的底部——更准确地说,文本行的大小随着图像的增长而增长,但文本的基线不会向上移动。当图像明显大于文本大小时,效果相当难看。
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. I believe this just vertically centers the text lines, but within the text line the text is still drawn at the bottom.
我已经尝试android:gravity="center_vertical"
在 TextView 上使用该属性,但这没有任何效果。我相信这只是使文本行垂直居中,但在文本行内,文本仍绘制在底部。
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. This sounds rather daunting and I'm hoping someone can suggest another approach.
我目前的思路是创建一个自定义跨度,根据行高和当前文本大小移动文本的基线。这个跨度将包含整个文本,我必须计算与ImageSpan
s的交集,这样我也可以避免移动图像。这听起来相当令人生畏,我希望有人可以提出另一种方法。
Any and all help is appreciated!
任何和所有的帮助表示赞赏!
采纳答案by jujux789
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). 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.
可能有点晚了,但无论图像大小如何,我都找到了一种方法。您需要创建一个扩展 ImageSpan 的类并覆盖方法getSize()
和getCachedDrawable()
(我们不需要更改最后一个,但此方法DynamicDrawableSpan
是私有的,不能从子类以其他方式访问)。在 中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!
如果您在该课程中遇到任何问题,请告诉我!
回答by misaka-10032
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
. 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:
我的答案调整了第一个答案。其实上面两种方法我都试过,我觉得它们不是真正的居中垂直。如果它被放置在ascent
and之间descent
,而不是top
and之间,它会使 drawable 更加居中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'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 与其他文本相同,否则父视图将无法正确包装内容。
回答by xuqingqi
After reading the source code of TextView, I think we can use the baseLine of eache text line which is "y". And it will work even if you set lineSpaceExtra.
看了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();
}
}
回答by boiledwater
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();
}
};
回答by ptiili
I got a working solution by creating a class that inherits from ImageSpan.
通过创建一个继承自ImageSpan的类,我得到了一个可行的解决方案。
Then modified draw implementation from 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.
然后从 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();
}
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.
考虑到本地化,这可能不是一个好的做法,但对我有用。要在文本中间设置图像,您自然需要用跨度替换文本中的标记。
回答by heng li
My answer tweaks the misaka-10032 answer. work perfect!
我的回答调整了 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;
}
}
回答by Rakesh Gopathi
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;
}
}
回答by Xiaofeng
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;
}