计算 Android 中 canvas 上绘制文本的基线 y

Calculate baseline y for drawing text on canvas in Android

我正在使用以下代码绘制文本。

    final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    final Rect rect = new Rect();
    paint.getTextBounds(text, 0, text.length(), rect);

    final int width = rect.width();
    final int height = rect.height();

    Bitmap rendered = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

    final Canvas gOfRendered = new Canvas(rendered);

    paint.setColor(Color.WHITE);

    gOfRendered.drawText(
            text, 0, height - paint.getFontMetrics().descent - paint.getFontMetrics().leading, paint
    );

    gOfRendered.drawColor(0x11ffffff); // to see the bounds

Canvas.drawText需要y坐标作为基线。 上述输出中的某些字符串(如“back”)没有下降。它被剪掉了,在这些情况下减去 paint.getFontMetrics().descent 是没有意义的。如何确保基线计算正确?

或者,有没有取原点y坐标而不取基线的文字绘制方法?

我正在寻找一种方法来精确地在 Paint.getTextBounds()

给定的范围内绘制文本

我用这个函数画多行文字:

void drawMultilineText(Canvas canvas, String text, float x, float y) {
    for (String line: text.split("\n")) {
        canvas.drawText(line, x, y, mPaintText);
        y += mPaintText.descent() - mPaintText.ascent();
    }
}

也请检查 Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics

单行文字可以放成一个BoringLayout to get the location of the baseline. (StaticLayout也可以,不过看起来你是在处理单行,所以我们就用BoringLayout .)

下面的代码展示了如何将文本放入布局中以提取基线。一旦基线已知(相对于顶部的零),就可以在 canvas.

上我们喜欢的地方绘制文本

MyView.kt

class MyView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val textToDisplay: String
    private var textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
        textSize = 98f
        color = Color.WHITE
    }

    // Layout for single lines
    private var boringLayout: BoringLayout

    // Contains just the text.
    private val wordBounds = Rect()

    private val wordBoundsFillPaint = Paint().apply {
        color = 0x55ffffff
        style = Paint.Style.FILL
    }

    init {
        context.obtainStyledAttributes(attrs, R.styleable.MyView, 0, 0).apply {
            textToDisplay = getString(R.styleable.MyView_android_text) ?: "Nothing"
        }.recycle()

        val metrics = BoringLayout.isBoring(textToDisplay, textPaint)
            ?: throw IllegalArgumentException("\"$textToDisplay\" is not boring.")

        // Get the text bounds that adhere tightly to the text.
        textPaint.getTextBounds(textToDisplay, 0, textToDisplay.length, wordBounds)

        // Get the layout for the text. These bounds include additional spacing used in the layout.
        boringLayout =
            BoringLayout.make(
                textToDisplay,
                textPaint,
                textPaint.measureText(textToDisplay).toInt(), Layout.Alignment.ALIGN_NORMAL,
                0f, 0f,
                metrics,
                false
            )

        val textBaseline = boringLayout.getLineBaseline(0)
        wordBounds.top = wordBounds.top + textBaseline
        wordBounds.bottom = wordBounds.bottom + textBaseline
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.withTranslation(
            // Center the words within the view.
            (width - boringLayout.width).toFloat() / 2,
            (height - boringLayout.height).toFloat() / 2
        ) {
            drawRect(wordBounds, wordBoundsFillPaint)
            // Using BoringLayout to draw text is preferred, but drawText() will work here as well.
            boringLayout.draw(this)
//            drawText(textToDisplay, 0f, boringLayout.getLineBaseline(0).toFloat(), textPaint)
        }
    }
}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.textbaseline.MyView
        android:id="@+id/myView"
        android:layout_width="200dp"
        android:layout_height="75dp"
        android:background="@android:color/black"
        android:text="Retrograde"
        app:layout_constraintBottom_toTopOf="@+id/myView2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <com.example.textbaseline.MyView
        android:id="@+id/myView2"
        android:layout_width="200dp"
        android:layout_height="75dp"
        android:layout_marginTop="8dp"
        android:background="@android:color/black"
        android:text="&#258;pple"
        app:layout_constraintBottom_toTopOf="@+id/myView3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/myView" />

    <com.example.textbaseline.MyView
        android:id="@+id/myView3"
        android:layout_width="200dp"
        android:layout_height="75dp"
        android:layout_marginTop="8dp"
        android:background="@android:color/black"
        android:text="back"
        app:layout_constraintBottom_toTopOf="@+id/myView4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/myView2" />

    <com.example.textbaseline.MyView
        android:id="@+id/myView4"
        android:layout_width="200dp"
        android:layout_height="75dp"
        android:layout_marginTop="8dp"
        android:background="@android:color/black"
        android:text="scene"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/myView3" />

</androidx.constraintlayout.widget.ConstraintLayout>