preface

We now see ImageSpan almost every day in Android apps, such as App custom emojis and small ICONS in text.

What's the difference between an emoji in an input method and an emoji in an ImageSpan?
base
TextView
ImageSpan



ImageSpan

ImageSpan

1. ImageSpan comes from ReplacementSpan

ReplacementSpan
updateMeasureState
updateDrawState

/** * This method does nothing, since ReplacementSpans are measured * explicitly instead of affecting Paint properties. */
public void updateMeasureState(TextPaint p) {}/** * This method does nothing, since ReplacementSpans are drawn * explicitly instead of affecting Paint properties. */
public void updateDrawState(TextPaint ds) {}Copy the code

GetSize and Draw are added to determine the position of the picture and draw the picture content


/**
 * Returns the width of the span. Extending classes can set the height of the span by updating
 * attributes of {@link android.graphics.Paint.FontMetricsInt}. If the span covers the whole
 * text, and the height is not set,
 * {@link #draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)} will not be
 * called for the span.
 *
 * @param paint Paint instance.
 * @param text Current text.
 * @param start Start character index for span.
 * @param end End character index for span.
 * @param fm Font metrics, can be null.
 * @return Width of the span.
 */
public abstract int getSize(@NonNull Paint paint, CharSequence text,
                    @IntRange(from = 0) int start, @IntRange(from = 0) int end,
                    @Nullable Paint.FontMetricsInt fm);

/**
 * Draws the span into the canvas.
 *
 * @param canvas Canvas into which the span should be rendered.
 * @param text Current text.
 * @param start Start character index for span.
 * @param end End character index for span.
 * @param x Edge of the replacement closest to the leading margin.
 * @param top Top of the line.
 * @param y Baseline.
 * @param bottom Bottom of the line.
 * @param paint Paint instance.
 */
public abstract 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);

Copy the code

The arguments in getSize are explained clearly in the comments, but the FontMetricsInt is used to change the height. It will also change the layout of TextView and the baseline of drawing images. The layout of TextView will be discussed later. In addition to top, Ascent, Descent, Bottom and leading, you can refer to the following figure. For details, you need to search online. HenCoder tutorial: Custom draw View 1-3 drawText() text

draw
HenCoder
y
baseline
draw
ellipsize



DynamicDrawableSpan
ALIGN_BOTTOM
ALIGN_BASELINE
public abstract Drawable getDrawable();
DynamicDrawableSpan
ImageSpan
drawable
resouce id
uri



DynamicDrawableSpan
ImageSpan
ImageSpan
getSize
draw

What ImageSpan getSize and Draw do

FontMetricsInt, fm.top = fm.ascent = -drawable height; fm.bottom = fm.descent = 0; And returns the width of drawable. At first glance, this looks fine, but there are pits in this area, more on that later. The draw method handles the alignment, and the bottom-aligned transY = bottom-b.getbounds ().bottom; “And then move the canvas over to paint. TransY -= paint.getFontMetricsint ().descent; The reason is that the baseline is above bottom and below descent, so it is aligned with the baseline after subtracting descent. Note: “bottom alignment” refers to the bottom of the entire line of text, not the bottom of the text, so let’s try different alignment.

ALIGN_BOTTOM
ALIGN_BASELINE
baseline



draw
ImageSpan
draw
canvas.translate(x, top);

3. What about the center-aligned ImageSpan

Of course, what we really want to see is that the text is centered so it looks more comfortable. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Because that’s how you line up your text, and then figure out how much the image needs to be offset relative to it. So the question is, where is baseline? This is done using the FontMetricsInt in getSize, which sets the ascent and Descent of the image. The ascent and Descent are both referenced to the baseline, Start with baseline = 0, ascent < 0 at top, descent > 0 at bottom. From getSize implementation of DynamicDrawableSpan, it can be known that the baseline is coincided with descent, and to center the text is to center the ascent and descent of the text, so we know the baseline. You can determine the position of the image when it is centered with the text.

ascent
baseline
descent
ascent
top
transY
y
baseline
fm
FontMetrics
textHeight = fm.descent - fm.ascent
drawableHeight



baseline
descent
transY = y - fm.descent

offsetHeight = textHeight - drawableHeight
transY -= offsetHeight / 2
trasnY -= drawableHeight



transY = y + (fm.descent + fm.ascent) / 2 - drawableHeight / 2


y + (fm.descent + fm.ascent) / 2
drawableHeight / 2



Align center




While we were still basking in the beauty of "center alignment", the QA student suddenly raised a Bug: "Why is the bottom part of your image truncated?" "... What?"

harmonious
free
TextView
padding
padding
0
Align center
ImageSpan
TextView



baseline
getSize
TextView
TextView
onMeasure
Layout
StaticLayout
Span
TextView
Layout
Span
LineHeightSpan

bottom
Align center
getSize
TextView



draw
Align center

@Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
// return super.getSize(paint, text, start, end, fm);
        Drawable d = getDrawable();
        Rect rect = d.getBounds();

        float drawableHeight = rect.height();
        Paint.FontMetrics paintFm = paint.getFontMetrics();
        float textHeight = paintFm.descent - paintFm.ascent;

        if(fm ! =null) {
            float textCenter = (paintFm.descent + paintFm.ascent) / 2;
            fm.ascent = fm.top = (int) (textCenter - drawableHeight / 2);
            fm.descent = fm.bottom = (int) (drawableHeight + fm.ascent);
        }

        return rect.right;
    }
Copy the code

Align center
ImageSpan
getSize


TextView
TexView
EditText



getSize
Bitmap
getDrawable

4, Ellipsis of Ellipsize is “eaten”

flat
ImageSpan
flat
Etc.
,
ImageSpan
ImageSpan
eat



TextView
Layout
getEllipsisStart
getEllipsisCount
draw
TextView
text


text

'. '
'... '

'\uFEFF'
BOM (Byte Order Mark)
Big-Endian
'\uFEFF'
Little-Endian
'\uFFFE'
draw

text = text.toString().replace("\uFEFF"."").replace("\uFFFE"."");
if (start >= text.length()) {
    return;
}
Copy the code

To be safe, ‘\uFFFE’ is also replaced, so that anything beyond the ellipsis is not drawn again.

text
'... '
'... '

if (end > text.length()) {
    end = text.length();
}
String subText = text.subSequence(start, end).toString();
if ("...".equals(subText)) {
    canvas.drawText(text, start, end, x, y, paint);
    return;
}
Copy the code

That’s OK!

The end of the

Long winded so much, but also the use of ImageSpan encountered problems clear, because there is no detailed study of TextView and its Layout source, there is a problem, please also criticize you. Attached below do experiment with the source, github.com/funnywolfda… . The next article will cover the ClickableSpan pit and my solution.