Align Text Around Imagespan Center Vertical

Align text around ImageSpan center vertical

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.

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!

Android - ImageSpan - How to center align the image at the end of the text

You can try my CenteredImageSpan.
You can customize in draw method by calculating transY -= (paint.getFontMetricsInt().descent / 2 - 8);. (Good luck. :) )

public class CenteredImageSpan extends ImageSpan {
private WeakReference<Drawable> mDrawableRef;

// Extra variables used to redefine the Font Metrics when an ImageSpan is added
private int initialDescent = 0;
private int extraSpace = 0;

public CenteredImageSpan(Context context, final int drawableRes) {
super(context, drawableRes);
}

public CenteredImageSpan(Drawable drawableRes, int verticalAlignment) {
super(drawableRes, verticalAlignment);
}

@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;
// }

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

@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

int transY = bottom - b.getBounds().bottom;
// this is the key
transY -= (paint.getFontMetricsInt().descent / 2 - 8);

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

// 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;
}
}

EDIT

I implemented above code like this:

Drawable myIcon = getResources().getDrawable(R.drawable.btn_feedback_yellow);
int width = (int) Functions.convertDpToPixel(75, getActivity());
int height = (int) Functions.convertDpToPixel(23, getActivity());
myIcon.setBounds(0, 0, width, height);
CenteredImageSpan btnFeedback = new CenteredImageSpan(myIcon, ImageSpan.ALIGN_BASELINE);
ssBuilder.setSpan(
btnFeedback, // Span to add
getString(R.string.text_header_answer).length() - 1, // Start of the span (inclusive)
getString(R.string.text_header_answer).length(), // End of the span (exclusive)
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);// Do not extend the span when text add later

Aligning ImageSpan with Spanable String

Use "y" in onDraw method to find the baseline of text and then align the drawable to baseline of text view

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

}

Vertically align text next to an image?

Actually, in this case it's quite simple: apply the vertical align to the image. Since it's all in one line, it's really the image you want aligned, not the text.

<!-- moved "vertical-align:middle" style from span to img -->
<div>
<img style="vertical-align:middle" src="https://via.placeholder.com/60x60" alt="A grey image showing text 60 x 60">
<span style="">Works.</span>
</div>

Vertically aligning span with text, select, and span with image on single line?

Ok, I came as close as I could:

ff-clear

Basically:

  • The baseline of the texts in first span and select are (nearly) aligned
  • The image span is centered vertically in respect to the select

... and this is as good as I'd have it, I guess. Somewhat strangely, there's no vertical-align: middle; anywhere in the CSS to get this output? Here is an overlay from the visual inspect in Firefox:

ff-merged.png

... and here is the code:

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style type="text/css">
body {
font: 16px serif;
}
#mydiv span#s1 {
vertical-align: baseline;
line-height: 1.25em;
display: inline-block;
}
#mydiv span#s2 {
vertical-align: baseline;
line-height: 1.25em;
display: inline-block;
}
#mydiv span img {
vertical-align: sub;
line-height: 1em;
}
#mydiv select {
font: 16px serif;
line-height: 1.25em;
display: inline-block;
text-decoration: none;
background-color: #FFF;
vertical-align: baseline;
border: 2px solid gray;
padding: 0;
margin: 0;
}
</style>
<script type="text/javascript">
</script>
</head>

<body>
<h1>Hello World!</h1>

<div id="mydiv">
<span id="s1">Testing 1:</span>
<select id="myselect"><option value="1">[My option]</option></select>
<span id="s2"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAABHNCSVQICAgIfAhkiAAAAQxJREFUOI2l1DtOA0EMBuBPiGMgUBo4AQWioEAkCCpuACUSuQLcgYI+4QJ0NAREkWOkiBBCPBraFLsU6w3L7uZFfsmSx2P/Y8/YQz1a6GKAUcggbAc1/pdlwxb6SGfIEzYi5gpJkWQP3+GYoIc2miFtPMdeinfcFNbjTHKSYZBOwhE+arKkUM4Qa1NIclzXEbUK5UzLJEd+JxWibii9JUjSVeyG090MkjOcysqvxShYm3NkNBErywSXiV5C31yWqB/6yT/iO7IuP2fx589xHDEJdnLjog3ZwFvE3Bc3FhmRfbyG7yfWyw7loX3Ahb9D++i3Ib+wPem0eb6RRHbBjSlZj3GIW9WPrROlVfADiJ5vX7sneXIAAAAASUVORK5CYII="/></span>
</div>

</body>
</html>

If a better answer than this appears, I'll re-accept it ...

How to make ImageSpan align by baseline

That is just, well, wrong. The drawable's vertical placement is off since ALIGN_BASELINE is defined as follows:

A constant indicating that the bottom of this span should be aligned with the baseline of the surrounding text.

That is clearly not happening. See the end of this post for what is going wrong. I suggest the following for a fix:

// Override draw() to place the drawable correctly when ALIGN_BASELINE.
ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BASELINE) {
public void draw(Canvas canvas, CharSequence text, int start,
int end, float x, int top, int y, int bottom,
@NonNull Paint paint) {
if (mVerticalAlignment != ALIGN_BASELINE) {
super.draw(canvas, text, start, end, x, top, y, bottom, paint);
return;
}
Drawable b = getDrawable();
canvas.save();
// If we set transY = 0, then the drawable will be drawn at the top of the text.
// y is the the distance from the baseline to the top of the text, so
// transY = y will draw the top of the drawable on the baseline. We want the // bottom of the drawable on the baseline, so we subtract the height
// of the drawable.
int transY = y - b.getBounds().bottom;
canvas.translate(x, transY);
b.draw(canvas);

canvas.restore();
}
};

This code produces the following image which, I think, is correct.

Sample Image


Why is the box not drawn on the baseline?

Here is the source that draws the box from DynamicDrawableSpan#draw():

public void draw(@NonNull Canvas canvas, CharSequence text,
@IntRange(from = 0) int start, @IntRange(from = 0) int end, float x,
int top, int y, int bottom, @NonNull Paint paint) {
Drawable b = getCachedDrawable();
canvas.save();
int transY = bottom - b.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= paint.getFontMetricsInt().descent;
}
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}

And the documentation for DynamicDrawableSpan#draw(). The only argument to draw() that interests us is bottom:

bottom int: Bottom of the line.

For the solution code and the emulator used, transY is calculated by DynamicDrawableSpan as 94, i.e., the box will be drawn 94 pixels from the top. top is 178 while the baseline is defined as zero, so 178 - 94 = 84 pixels which is the height of the box or b.getBounds().bottom. This checks out.

In DynamicDrawableSpan#draw() in the same emulator, bottom = 224 while the drawable's height is still 84 as shown above. Why is bottom 224? This is the line height of the font.

transY is now calculated as 224 - 84 = 140 pixels. This will place the bottom of the box at the bottom of the line but below the baseline.

One more adjustment is made:

transY -= paint.getFontMetricsInt().descent;

On the test, paint.getFontMetricsInt().descent is 41, so transY now becomes 99. Since 94 is the right answer, 99 places the box five pixels too low and this is what you see.

See Android 101: Typography for description of Android font metrics.



Related Topics



Leave a reply



Submit