hoshiKuzu
hoshiKuzu

Reputation: 915

Calculate baseline y for drawing text on canvas in Android

I am using the following code to draw text.

    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

enter image description here

Canvas.drawText needs the y coordinate as a baseline. Some strings such as "back" in the above output does not have a descent. It gets cut and it does not make sense to subtract paint.getFontMetrics().descent in those cases. How do I make sure that the baseline is calculated correctly?

Alternately, is there a text drawing method which takes the origin y coordinate and not the baseline?

I am looking for a way to draw text exactly within the bounds given by Paint.getTextBounds()

Upvotes: 0

Views: 682

Answers (2)

Cheticamp
Cheticamp

Reputation: 62831

Single-line text can be placed into a BoringLayout to get the location of the baseline. (StaticLayout can also be used, but it looks like you are dealing with single lines, so we will use BoringLayout.)

The code below shows how to place text into a layout to extract the baseline. Once the baseline is known (relative to zero at the top), the text can be drawn where we like on a 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)
        }
    }
}

enter image description here

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>

Upvotes: 1

Dmitry Yablokov
Dmitry Yablokov

Reputation: 176

I use this function to draw multiline text:

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

Please also check Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics

Upvotes: 0

Related Questions