在Canvas上测量要绘制的文本高度(Android)

158

有没有一种简单明了的方法来测量文本的高度? 我目前使用的方法是使用Paint的measureText()函数获取文本的宽度,然后通过试错法找到大致高度值。我也尝试过使用FontMetrics,但这些似乎都是近似方法,不够准确。

我正在尝试为不同分辨率缩放内容。我可以做到,但最终会得到非常冗长的代码以及许多计算来确定相对大小。我讨厌这样!一定有更好的方法。

8个回答

307

根据需要,测量高度有不同的方法。

#1 getTextBounds

如果你正在做一些像精确居中少量固定文本这样的事情,你可能想要使用getTextBounds。你可以像这样获取边界矩形:

Rect bounds = new Rect();
mTextPaint.getTextBounds(mText, 0, mText.length(), bounds);
int height = bounds.height();

如下图所示,不同的字符串会给出不同的高度(以红色显示)。

enter image description here

在一些情况下,这些不同的高度可能是一个缺点,因为您需要一个无论文本内容如何都保持不变的固定高度。请参见下一节。

#2 Paint.FontMetrics

您可以从字体度量中计算字体的高度。 高度始终相同,因为它是从字体而不是任何特定的文本字符串获取的。

Paint.FontMetrics fm = mTextPaint.getFontMetrics();
float height = fm.descent - fm.ascent;

基线是文本所在的线。 下行基线通常是字符将要下降到的最远位置,上行基线通常是字符将要上升到的最远位置。 要获取高度,您必须减去上行高度,因为它是负值。(基线是 y=0y 沿屏幕向上减小。)

请看下面的图片。 两个字符串的高度都是 234.375

enter image description here

如果你想得到行高而不仅仅是文本高度,可以这样做:

float height = fm.bottom - fm.top + fm.leading; // 265.4297

这些是该行的底部顶部。行间距通常为零,但您仍应将其添加。

上面的图像来自此项目。您可以尝试使用它来了解字体度量如何工作。

#3 StaticLayout

对于多行文本的高度测量,您应该使用StaticLayout。我在这个答案中详细讨论了它,但获取此高度的基本方法如下:

String text = "This is some text. This is some text. This is some text. This is some text. This is some text. This is some text.";

TextPaint myTextPaint = new TextPaint();
myTextPaint.setAntiAlias(true);
myTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
myTextPaint.setColor(0xFF000000);

int width = 200;
Layout.Alignment alignment = Layout.Alignment.ALIGN_NORMAL;
float spacingMultiplier = 1;
float spacingAddition = 0;
boolean includePadding = false;

StaticLayout myStaticLayout = new StaticLayout(text, myTextPaint, width, alignment, spacingMultiplier, spacingAddition, includePadding);

float height = myStaticLayout.getHeight(); 

2
@MichealJohnson,我在这里将该应用添加为 GitHub 项目 - Suragch
1
“getTextSize” 给你什么信息? - android developer
1
Paint的getTextSize()方法可以获取字体大小,单位为像素(与sp单位不同)。@androiddeveloper - Suragch
2
像素单位中的字体大小与测量高度以及FontMetrics尺寸之间的关系是什么?这是我想更深入探讨的问题。 - Suragch
1
@RonTLV,使用FontMetrics时,它是以像素为单位的。当你涉及到这样的低级图形渲染时,一切都是以像素为单位的。在上面的StaticLayout示例中,我通过乘以设备密度来进行dp转换。也可以参考这个问答 - Suragch
显示剩余10条评论

145

2
这会导致非常奇怪的结果,当我评估文本的高度时。短文本的高度为12,而真正长的文本的高度为16(假设字体大小为16)。对我来说毫无意义(Android 2.3.3)。 - AgentKnopf
41
在文本中出现下行字符时,高度上的差异就产生了,例如,“High”比“Low”更高,因为字母“g”的一部分在基准线下方。 - FrinkTheBrave
如果您不想使用差异值,可以改用Paint.FontMetrics - Top-Master

91

@bramp的回答是正确的,但它只是部分正确,因为它没有提到计算出来的边界将是包含完整文本的最小矩形,并具有隐式起始坐标0,0。

这意味着,例如“Py”的高度将与“py”、“hi”、“oi”或“aw”的高度不同,因为它们在像素级别上需要不同的高度。

这绝不等同于经典Java中的FontMetrics。

虽然文本的宽度不是太麻烦,但高度是。

特别是,如果您需要将绘制的文本垂直居中对齐,请尝试获取文本"a"(不带引号),而不是使用您打算绘制的文本。适合我...

这就是我的意思:

Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);

paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(textSize);

Rect bounds = new Rect();
paint.getTextBounds("a", 0, 1, bounds);

buffer.drawText(this.myText, canvasWidth >> 1, (canvasHeight + bounds.height()) >> 1, paint);
// remember x >> 1 is equivalent to x / 2, but works much much faster

垂直居中文本意味着垂直居中边界矩形——对于不同的文本(大写字母、长字母等)是不同的。但实际上,我们想要做的是对呈现文本的基线进行对齐,以便它们不会出现升起或凹陷的情况。因此,只要我们知道最小字母“a”的中心,我们就可以重复使用其对齐方式来对齐其余文本。这将使所有文本都居中对齐,并将它们的基线对齐。


27
好久没见过 x >> 1 了。只为这个点赞 :) - keaukraine
71
一款优秀的现代编译器会将x / 2优化为x >> 1 - intrepidis
41
鉴于Chris的评论,读代码时x / 2更友好。请注意,我保持了原文的意思,并尽可能让其通俗易懂,未包含任何解释性内容。 - d3dave
在这个例子中,buffer是什么?它是传递给draw(Canvas)方法的canvas吗? - AutonomousApps
@AutonomousApps 是的,这就是画布。 - Chisko

17

高度是您在Paint变量上设置的文本大小。

另一种找到高度的方法是

mPaint.getTextSize();

3
您可以使用android.text.StaticLayout类来指定所需的范围,然后调用getHeight()方法。您可以通过调用其draw(Canvas)方法来绘制布局中包含的文本。

2
您可以使用getTextSize()方法轻松获取Paint对象的文本大小。 例如:
Paint mTextPaint = new Paint (Paint.ANTI_ALIAS_FLAG);
//use densityMultiplier to take into account different pixel densities
final float densityMultiplier = getContext().getResources()
            .getDisplayMetrics().density;  
mTextPaint.setTextSize(24.0f*densityMultiplier);

//...

float size = mTextPaint.getTextSize();

24.0f 是从哪里来的? - Erigami
24.0f只是一个文本大小的示例。 - moondroid

1

你必须使用从getTextBounds()返回的Rect.width()Rect.Height()。这对我很有效。


2
如果你正在处理多个文本段落,那么这并不适用。原因在我上面的回答中已经解释了。 - Nar Gar

0
如果有人仍然遇到问题,这是我的代码。
我有一个自定义视图,它是正方形(宽度=高度),我想为它分配一个字符。 onDraw() 显示如何获取字符的高度,尽管我没有使用它。字符将显示在视图的中间。
public class SideBarPointer extends View {

    private static final String TAG = "SideBarPointer";

    private Context context;
    private String label = "";
    private int width;
    private int height;

    public SideBarPointer(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public SideBarPointer(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
    }

    public SideBarPointer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.context = context;
        init();
    }

    private void init() {
//        setBackgroundColor(0x64FF0000);
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        height = this.getMeasuredHeight();
        width = this.getMeasuredWidth();

        setMeasuredDimension(width, width);
    }

    protected void onDraw(Canvas canvas) {
        float mDensity = context.getResources().getDisplayMetrics().density;
        float mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity;

        Paint previewPaint = new Paint();
        previewPaint.setColor(0x0C2727);
        previewPaint.setAlpha(200);
        previewPaint.setAntiAlias(true);

        Paint previewTextPaint = new Paint();
        previewTextPaint.setColor(Color.WHITE);
        previewTextPaint.setAntiAlias(true);
        previewTextPaint.setTextSize(90 * mScaledDensity);
        previewTextPaint.setShadowLayer(5, 1, 2, Color.argb(255, 87, 87, 87));

        float previewTextWidth = previewTextPaint.measureText(label);
//        float previewTextHeight = previewTextPaint.descent() - previewTextPaint.ascent();
        RectF previewRect = new RectF(0, 0, width, width);

        canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity, previewPaint);
        canvas.drawText(label, (width - previewTextWidth)/2, previewRect.top - previewTextPaint.ascent(), previewTextPaint);

        super.onDraw(canvas);
    }

    public void setLabel(String label) {
        this.label = label;
        Log.e(TAG, "Label: " + label);

        this.invalidate();
    }
}

4
在onDraw()中分配内存是一个负面操作,如果你在类中声明字段(并在构造函数中初始化),并在onDraw()中重复使用它们,可以获得很多性能优势。 - zoltish

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