requestLayout() on a TextView does not update spans

Viewed 377

As per the official documentation, if you need to update previously added span inside a TextView without using the heavy setText() routine, you can keep a reference to the span you'll need to update:

Change internal span attributes

If you need to change only an internal attribute of a mutable span, such as the bullet color in a custom bullet span, you can avoid the overhead from calling setText() multiple times by keeping a reference to the span as it's created. When you need to modify the span, you can modify the reference and then call either invalidate() or requestLayout() on the TextView, depending on the type of attribute that you changed.

In the code example below, a custom bullet point implementation has a default color of red that changes to gray when clicking a button:

public class MainActivity extends AppCompatActivity {

    private BulletPointSpan bulletSpan = new BulletPointSpan(Color.RED);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        SpannableString spannable = new SpannableString("Text is spantastic");
        // setting the span to the bulletSpan field
        spannable.setSpan(bulletSpan, 0, 4, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
        styledText.setText(spannable);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // change the color of our mutable span
                bulletSpan.setColor(Color.GRAY);
                // color won’t be changed until invalidate is called
                styledText.invalidate();
            }
        });
    }
}

So to replicate this, I attach a DrawableSpan to the spannable text I pass to my TextView, and then I update that span later when my drawable is ready (fetched from a distant location). The problem is that my call to requestLayout() does not update the span I just updated.

 DrawableSpan span = new DrawableSpan();
 text.setSpan(span, i1, i2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
 textView.setText(text, TextView.BufferType.SPANNABLE);
 mRequestManager.load(source).into(new SimpleTarget<Drawable>() {
                    @Override
                    public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
                        span.setDrawable(resource);
                        textView.requestLayout();
                    }
                });

Here is the DrawableSpan I use, it's the exact copy paste of the framework DynamicDrawableSpan which is the super class of ImageSpan (that justs provide overloads for loading drawalbe from various inputs). I had to create my own span to allow me setting the drawable later on.

public class DrawableSpan extends ReplacementSpan {

    private static final String TAG = "DynamicDrawableSpan";

    /**
     * A constant indicating that the bottom of this span should be aligned
     * with the bottom of the surrounding text, i.e., at the same level as the
     * lowest descender in the text.
     */
    public static final int ALIGN_BOTTOM = 0;

    /**
     * A constant indicating that the bottom of this span should be aligned
     * with the baseline of the surrounding text.
     */
    public static final int ALIGN_BASELINE = 1;

    private final int mVerticalAlignment;
    private Drawable mDrawable;

    public DrawableSpan() {
        this(null);
    }

    public DrawableSpan(Drawable drawable) {
        this(drawable, ALIGN_BOTTOM);
    }

    public DrawableSpan(Drawable drawable, int verticalAlignment) {
        setDrawable(drawable);
        mVerticalAlignment = verticalAlignment;
    }

    public void setDrawable(Drawable drawable) {
        if (drawable == null) return;
        mDrawable = drawable;
        mDrawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end,
                       Paint.FontMetricsInt fm) {
        if (mDrawable == null) return 0;
        Rect rect = mDrawable.getBounds();
        if (fm != null) {
            fm.ascent = -rect.bottom;
            fm.descent = 0;
            fm.top = fm.ascent;
            fm.bottom = 0;
        }
        return rect.right;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end, float x,
                     int top, int y, int bottom, Paint paint) {

        if (mDrawable == null) return;

        canvas.save();
        int transY = bottom - mDrawable.getBounds().bottom;
        if (mVerticalAlignment == ALIGN_BASELINE) {
            transY -= paint.getFontMetricsInt().descent;
        }
        canvas.translate(x, transY);
        mDrawable.draw(canvas);
        canvas.restore();
    }

}

The weirdest thing is that if I lock and unlock the device, getSize() and draw() methods are called and the spans are displayed (the ascent and top measure are not applied but at least it is shown).

1 Answers
Related