Reputation: 915
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
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
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)
}
}
}
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="Ă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
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