Align text around ImageSpan center vertical
Asked Answered
O

12

54

I have an ImageSpan inside of a piece of text. What I've noticed is that the surrounding text is always drawn at the bottom of the text line -- to be more precise, the size of the text line grows with the image but the baseline of the text does not shift upward. When the image is noticeably larger than the text size, the effect is rather unsightly.

Here is a sample, the outline shows bounds of the TextView: enter image description here

I am trying to have the surrounding text be centered vertically with respect to the image being displayed. Here is the same sample with blue text showing the desired location:

enter image description here

Here are the constraints that I'm bound by:

  • I cannot use compound drawables. The images must be able to be shown between words.
  • The text may be multiline depending on the content. I have no control over this.
  • My images are larger than the surrounding text and I cannot reduce their size. While the sample image above is larger than the actual images (to demonstrate the current behavior), the actual images are still large enough that this problem is noticeable.

I've tried using the android:gravity="center_vertical" attribute on the TextView, but this does not have any effect. I believe this just vertically centers the text lines, but within the text line the text is still drawn at the bottom.

My current train of thought is to create a custom span that shifts the baseline of the text based on the height of the line and the current text size. This span would encompass the entire text, and I would have to compute the intersection with the ImageSpans so I can avoid shifting the images as well. This sounds rather daunting and I'm hoping someone can suggest another approach.

Any and all help is appreciated!

Otolaryngology answered 2/9, 2014 at 16:36 Comment(5)
Did you figure out how to do this?Elect
I am looking to achieve the same stuff, however, the answer of ptilli make it looks like it is working, but it is not really doing what shoud be done, which is centering the text with the cursor. I'll let you know if I can figure out somethingAshwell
Answers below works only with image smaller height than text. Author and me needs opposite trickTitian
Possible duplicate of Aligning ImageSpan to the top of the TextViewViewing
@Viewing I'm not trying to align the image span, so I don't think this is a duplicate.Otolaryngology
A
31

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!

Ashwell answered 10/2, 2015 at 2:7 Comment(14)
How to use it? Creating same class I guess isn't enough.Titian
You just have to replace the ImageSpan you were using with the CenteredImageSpan and it will center your image automatically with the textAshwell
but what if I use ImageSpan class not directly but by using Html.fromHtml(String) ?Titian
There is a bug, entry variable doesn't exists anywhere, but your code try to use it.Titian
It only works with image height smaller than text height.Titian
@Titian I just tried this solution and it appears to be working fine for an image larger than the text height. The posted code does have some errors: remove the "entry" variable in one of the constructors, and change "=>" to ">=" in getSize()Otolaryngology
For some reason fm.descent = 3 * extraSpace / 8 + initialDescent; works and fm.descent = extraSpace / 2 + initialDescent; aligns the text to the top. Following this tutorial btw to add image and text: guides.codepath.com/android/…Mccollum
how can i user it to make align text with bottom of a image?Hegemony
@Hegemony What you described sounds like the default behavior in the original post. I don't think you would use this code.Otolaryngology
@Otolaryngology sorry it was mistake, i was thinking of 'top alignment', got my solution by replacing "fm.descent = extraSpace / 2 + initialDescent;" by "fm.descent = extraSpace + initialDescent;"Hegemony
Please use paint.getFontMetricsInt() instead of fm.Because fm is a middle variablePastelist
I want to know how we can use this class. CenteredImageSpan imageSpan = new CenteredImageSpan(drawableResId, _____); what i need to pass as second variable?Donela
@SrikanthK you need to pass the aligment, e.g. DynamicDrawableSpan.ALIGN_BOTTOMAshwell
having error in overriding draw and getbound methodsBunnybunow
C
72

My answer tweaks the first answer. Actually I have tried both two methods above, and I don't think they are really center vertical. It would make the drawable more center if it's placed in between ascent and descent, rather than top and bottom. So as to the second answer, it aligns the center of the drawable to the baseline of the text, rather than the center of that text. Here's my solution:

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

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

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

    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

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

I also rewrite getSize to keep the FontMetrics of drawable the same as other text, otherwise the parent view won't wrap the content correctly.

Complement answered 2/9, 2014 at 16:36 Comment(6)
This is the best solution which answering my qusetion too. This is my question... #31249975Bounden
Works perfectly. Make sure you provide context and resource name when you use the class. Example: ImageSpan imageSpan = new CenteredImageSpan(getApplicationContext(), R.drawable.ic_youricon) (unlike the previous example)Crackup
did'nt work on 4" device 480x800 hdpi icons get cut at bottom text is shown correctly, but worked on xxhdpi devicesBarri
Replace draw with this draw: public void draw( Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { Drawable b = getCachedDrawable(); canvas.save(); int transY = bottom - b.getBounds().bottom; // this is the key transY -= paint.getFontMetricsInt().descent / 2; canvas.translate(x, transY); b.draw(canvas); canvas.restore(); }Barri
If the icon is larger than the text,the icon wil be cutPastelist
I tweaks this answer below,then work perfectly in my caseFast
D
52

After reading the source code of TextView, I think we can use the baseLine of eache text line which is "y". And it will work even if you set lineSpaceExtra.

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

}
Downbow answered 5/8, 2016 at 11:43 Comment(6)
This works great irrespective of the Image size. Most other answers have the issue of chopping the image when it is larger than the test. Thank youCassicassia
works like charm, thanks for taking time and figuring it out saved some of my time.Jilljillana
This is glorious! Magnificent! Charming!Destructible
All top answers don't work well, except this one. Thanks!Postexilian
This answer works great!Chantry
Not sure why but the ImageSpan(drawable: Drawable) constructor wasn't working for me, I needed to use the ImageSpan(context: Context, @DrawableRes drawableRes: Int) constructorTextualist
V
39
ImageSpan imageSpan = new ImageSpan(d, ImageSpan.ALIGN_BOTTOM) {
                public void draw(Canvas canvas, CharSequence text, int start,
                        int end, float x, int top, int y, int bottom,
                        Paint paint) {
                    Drawable b = getDrawable();
                    canvas.save();

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

                    canvas.translate(x, transY);
                    b.draw(canvas);
                    canvas.restore();
                }
            };
Ventriloquist answered 18/7, 2015 at 13:19 Comment(2)
works well and simple. Could explain the bottom top, x y parameter' meanings? I can't understand and no docs explanation. ThanksSpoof
replace draw from @Complement with this draw and code is working on all resolutionsBarri
A
31

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!

Ashwell answered 10/2, 2015 at 2:7 Comment(14)
How to use it? Creating same class I guess isn't enough.Titian
You just have to replace the ImageSpan you were using with the CenteredImageSpan and it will center your image automatically with the textAshwell
but what if I use ImageSpan class not directly but by using Html.fromHtml(String) ?Titian
There is a bug, entry variable doesn't exists anywhere, but your code try to use it.Titian
It only works with image height smaller than text height.Titian
@Titian I just tried this solution and it appears to be working fine for an image larger than the text height. The posted code does have some errors: remove the "entry" variable in one of the constructors, and change "=>" to ">=" in getSize()Otolaryngology
For some reason fm.descent = 3 * extraSpace / 8 + initialDescent; works and fm.descent = extraSpace / 2 + initialDescent; aligns the text to the top. Following this tutorial btw to add image and text: guides.codepath.com/android/…Mccollum
how can i user it to make align text with bottom of a image?Hegemony
@Hegemony What you described sounds like the default behavior in the original post. I don't think you would use this code.Otolaryngology
@Otolaryngology sorry it was mistake, i was thinking of 'top alignment', got my solution by replacing "fm.descent = extraSpace / 2 + initialDescent;" by "fm.descent = extraSpace + initialDescent;"Hegemony
Please use paint.getFontMetricsInt() instead of fm.Because fm is a middle variablePastelist
I want to know how we can use this class. CenteredImageSpan imageSpan = new CenteredImageSpan(drawableResId, _____); what i need to pass as second variable?Donela
@SrikanthK you need to pass the aligment, e.g. DynamicDrawableSpan.ALIGN_BOTTOMAshwell
having error in overriding draw and getbound methodsBunnybunow
S
5

I got a working solution by creating a class that inherits from ImageSpan.

Then modified draw implementation from DynamicDrawableSpan. At least this implementation works when my image height is less than font height. Not sure how this works for bigger images like yours.

@Override
public void draw(Canvas canvas, CharSequence text,
    int start, int end, float x,
    int top, int y, int bottom, Paint paint) {
    Drawable b = getCachedDrawable();
    canvas.save();

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

Also had to reuse implementation from DynamicDrawableSpan as it was private.

private Drawable getCachedDrawable() {
    WeakReference<Drawable> wr = mDrawableRef;
    Drawable d = null;

    if (wr != null)
        d = wr.get();

    if (d == null) {
        d = getDrawable();
        mDrawableRef = new WeakReference<Drawable>(d);
    }

    return d;
}

private WeakReference<Drawable> mDrawableRef;

And this is how I use it as static method that inserts image in front of the text.

public static CharSequence formatTextWithIcon(Context context, String text,
    int iconResourceId) {
    SpannableStringBuilder sb = new SpannableStringBuilder("X");

    try {
        Drawable d = context.getResources().getDrawable(iconResourceId);
        d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); 
        CenteredImageSpan span = new CenteredImageSpan(d); 
        sb.setSpan(span, 0, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        sb.append(" " + text); 
    } catch (Exception e) {
        e.printStackTrace();
        sb.append(text); 
    }

    return sb;

Maybe not a good practice there considering localization, but works for me. To set images in the middle of the text, you'd naturally need to replace tokens in text with spans.

Stiffler answered 14/10, 2014 at 11:57 Comment(1)
what happens when you use a larger image?Otolaryngology
F
3

My answer tweaks the misaka-10032 answer. work perfect!

public static class CenteredImageSpan extends ImageSpan { private WeakReference mDrawableRef;

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

    public CenteredImageSpan(@NonNull Drawable d) {
        super(d);
    }

    @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 transY = top + (bottom - top - b.getBounds().bottom)/2;
        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;
    }
}

------------update ------------------------------------------------- fix when pic is bigger

public static class CenteredImageSpan extends ImageSpan { private WeakReference mDrawableRef;

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

    public CenteredImageSpan(@NonNull Drawable d) {
        super(d);
    }

    @Override
    public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable FontMetricsInt fm) {
        Drawable d = getCachedDrawable();
        Rect rect = d.getBounds();

        if (fm != null) {
            int i = rect.height()/3;
            fm.ascent = -i*2;
            fm.descent = i;

            fm.top = fm.ascent;
            fm.bottom = fm.descent;
        }

        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 transY = top + (bottom - top) / 2 - (b.getBounds().height() / 2);
        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;
    }
}
Fast answered 21/6, 2019 at 7:29 Comment(1)
Worked perfectly.Kato
S
2

This solution provides a vertical centering based on actual letter size. It supports centering using capital letters and lower-case letters. For example, look at the marker character near a letter: X•. This solution achieves a similar effect.

This is a modified version of @WindRider's answer. Also, it's in Kotlin. And it supports drawable size customization.

The reason why this solution is created is to provide a better visual result. A lot of other solutions use font ascent. But it appears to cause visual problems in some cases. Android's default Roboto font, for example, has ascent higher than a typical capital letter top border. And because of it, some manual adjustments were needed to properly center an image.

class CenteredImageSpan(context: Context,
                        drawableRes: Int,
                        private val centerType: CenterType = CenterType.CAPITAL_LETTER,
                        private val customHeight: Int? = null,
                        private val customWidth: Int? = null) : ImageSpan(context, drawableRes) {

    private var mDrawableRef: WeakReference<Drawable?>? = null

    override fun getSize(paint: Paint, text: CharSequence,
                         start: Int, end: Int,
                         fontMetrics: FontMetricsInt?): Int {

        if (fontMetrics != null) {
            val currentFontMetrics = paint.fontMetricsInt
            // keep it the same as paint's Font Metrics
            fontMetrics.ascent = currentFontMetrics.ascent
            fontMetrics.descent = currentFontMetrics.descent
            fontMetrics.top = currentFontMetrics.top
            fontMetrics.bottom = currentFontMetrics.bottom
        }

        val drawable = getCachedDrawable()
        val rect = drawable.bounds
        return rect.right
    }

    override fun draw(canvas: Canvas,
                      text: CharSequence,
                      start: Int,
                      end: Int,
                      x: Float,
                      lineTop: Int,
                      baselineY: Int,
                      lineBottom: Int,
                      paint: Paint) {
        val cachedDrawable = getCachedDrawable()
        val drawableHeight = cachedDrawable.bounds.height()

        val relativeVerticalCenter = getLetterVerticalCenter(paint)

        val drawableCenter = baselineY + relativeVerticalCenter
        val drawableBottom = drawableCenter - drawableHeight / 2

        canvas.save()
        canvas.translate(x, drawableBottom.toFloat())
        cachedDrawable.draw(canvas)
        canvas.restore()
    }

    private fun getLetterVerticalCenter(paint: Paint): Int =
         when (centerType) {
            CenterType.CAPITAL_LETTER -> getCapitalVerticalCenter(paint)
            CenterType.LOWER_CASE_LETTER -> getLowerCaseVerticalCenter(paint)
        }

    private fun getCapitalVerticalCenter(paint: Paint): Int {
        val bounds = Rect()
        paint.getTextBounds("X", 0, 1, bounds)
        return (bounds.bottom + bounds.top) / 2
    }

    private fun getLowerCaseVerticalCenter(paint: Paint): Int {
        val bounds = Rect()
        paint.getTextBounds("x", 0, 1, bounds)
        return (bounds.bottom + bounds.top) / 2
    }


    // Redefined here because it's private in DynamicDrawableSpan
    private fun getCachedDrawable(): Drawable {

        val drawableWeakReference = mDrawableRef
        var drawable: Drawable? = null
        if (drawableWeakReference != null) drawable = drawableWeakReference.get()
        if (drawable == null) {
            drawable = getDrawable()!!

            val width = customWidth ?: drawable.intrinsicWidth
            val height = customHeight ?: drawable.intrinsicHeight

            drawable.setBounds(0, 0,
                               width, height)
            mDrawableRef = WeakReference(drawable)
        }
        return drawable

    }

    enum class CenterType {
        CAPITAL_LETTER, LOWER_CASE_LETTER
    }

}
Squirmy answered 14/7, 2020 at 18:26 Comment(0)
C
1

If anybody is interested in Kotlin version of @xuqingqi:

class VerticalImageSpan(drawable: Drawable) : ImageSpan(drawable) {

    override fun getSize(
        paint: Paint,
        text: CharSequence,
        start: Int,
        end: Int,
        fontMetricsInt: FontMetricsInt?,
    ): Int {
        val drawable = drawable
        val rect = drawable.bounds
        fontMetricsInt?.let { fm ->
            val fmPaint = paint.fontMetricsInt
            val fontHeight = fmPaint.descent - fmPaint.ascent
            val drHeight = rect.bottom - rect.top
            val centerY = fmPaint.ascent + fontHeight / 2

            fm.apply {
                ascent = centerY - drHeight / 2
                top = fontMetricsInt.ascent
                bottom = centerY + drHeight / 2
                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 fun draw(
        canvas: Canvas,
        text: CharSequence,
        start: Int,
        end: Int,
        x: Float,
        top: Int,
        y: Int,
        bottom: Int,
        paint: Paint,
    ) {
        val drawable = drawable
        canvas.save()
        val fmPaint = paint.fontMetricsInt
        val fontHeight = fmPaint.descent - fmPaint.ascent
        val centerY = y + fmPaint.descent - fontHeight / 2
        val transY = centerY - (drawable.bounds.bottom - drawable.bounds.top) / 2
        canvas.translate(x, transY.toFloat())
        drawable.draw(canvas)
        canvas.restore()
    }
}
Catatonia answered 19/1 at 13:56 Comment(0)
Q
0

While creating your Image span you have to add Vertical Alignment Flag DynamicDrawableSpan.ALIGN_CENTER. That should align the image's center to the text.

val mySpannable = SpannableString("    $YourText")
mySpannable.setSpan(ImageSpan(yourDrawable, DynamicDrawableSpan.ALIGN_CENTER), 0, 1, 0)
Questionless answered 7/12, 2020 at 0:45 Comment(0)
L
0

You can use ImageSpan.ALIGN_CENTER. I tested it on various emulators and it seems to work for APIs < 29. This only works good with wrap_content width though. From my tests assigning a width programmatically or in xml, breaks the line height (?)

Lycaonia answered 21/6, 2021 at 17:51 Comment(0)
R
-1

My improved version: drawable font metrics zoomed relative to text font metrics. So that line spacing will be calculate correctly.

@Override
public int getSize(Paint paint, CharSequence text,
                   int start, int end,
                   Paint.FontMetricsInt fm) {
    Drawable d = getCachedDrawable();
    Rect rect = d.getBounds();
    float drawableHeight = Float.valueOf(rect.height());


    if (fm != null) {
        Paint.FontMetricsInt pfm = paint.getFontMetricsInt();
        float fontHeight = pfm.descent - pfm.ascent;
        float ratio = drawableHeight / fontHeight;

        fm.ascent = Float.valueOf(pfm.ascent * ratio).intValue();
        fm.descent = Float.valueOf(pfm.descent * ratio).intValue();
        fm.top = fm.ascent;
        fm.bottom = fm.descent;
    }
Robbegrillet answered 20/10, 2015 at 23:13 Comment(0)
M
-1

This solution works. I have tested it and am using it for sometime. It doesn't consider the ascent and decent but it Aligns the drawable in the center.

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.text.style.ImageSpan;

import java.lang.ref.WeakReference;

public class CustomImageSpan extends ImageSpan {

  /**
   * A constant indicating that the center of this span should be aligned
   * with the center of the surrounding text
   */
  public static final int ALIGN_CENTER = -12;
  private WeakReference<Drawable> mDrawable;
  private int mAlignment;

  public CustomImageSpan(Context context, final int drawableRes, int alignment) {
    super(context, drawableRes);
    mAlignment = alignment;
  }

  @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();
      fm.ascent = pfm.ascent;
      fm.descent = pfm.descent;
      fm.top = pfm.top;
      fm.bottom = pfm.bottom;
    }
    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) {
    if (mAlignment == ALIGN_CENTER) {
      Drawable cachedDrawable = getCachedDrawable();
      canvas.save();
      //Get the center point and set the Y coordinate considering the drawable height for aligning the icon vertically
      int transY = ((top + bottom) / 2) - cachedDrawable.getIntrinsicHeight() / 2;
      canvas.translate(x, transY);
      cachedDrawable.draw(canvas);
      canvas.restore();
    } else {
      super.draw(canvas, text, start, end, x, top, y , bottom, paint);
    }
  }

  // Redefined locally because it is a private member from DynamicDrawableSpan
  private Drawable getCachedDrawable() {
    WeakReference<Drawable> wr = mDrawable;
    Drawable d = null;
    if (wr != null) {
      d = wr.get();
    }
    if (d == null) {
      d = getDrawable();
      mDrawable = new WeakReference<>(d);
    }
    return d;
  }
}
Meteoric answered 13/3, 2018 at 13:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.