2

Background

I know how to set multiple spans on a partial text within a static text, as I've asked here :

final SpannableString text = new SpannableString("Hello stackOverflow");
text.setSpan(new RelativeSizeSpan(1.5f), text.length() - "stackOverflow".length(), text.length(),
            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.RED), 3, text.length() - 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(text);

This will set the "stackOverflow" to have 2 spans on itself.

I also know how to set a drawable span on a part of the text, as I've asked here.

The problem

Now I need to set 2 spans on a text that's generated from formatting a text with a placeholder, while still having other styles being set as usual.

For example, suppose I have the next text in strings.xml:

<string name="potential_free_upgrade_1_d_months">
    <![CDATA[
    Potential free upgrade: <uu><b><font color=\'#3792e5\'>%1$d months</font></b></uu>]]>
</string>

enter image description here

The plan is that "%1$d months" will have a text color of "#3792e5" and will have a special underline that is a bit more below than the default one. I used a special customized tag "uu" for the special underline, to be handled in code.

Thing is, no matter what I do, I can't find how to have both the text color AND the underline being shown together.

What I've tried

Since this problem has a placeholder (and the text can be different around the text to be formatted), I had to use "Html.FromHtml" :

    String formattedStr = getString(R.string.potential_free_upgrade_1_d_months, 9);
    Spanned textToShow = Html.fromHtml(formattedStr, null, new TagHandler() {
        int start;

        @Override
        public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
            switch (tag) {
                case "uu":
                    if (opening)
                        start = output.length();
                    else {
                        int end = output.length();
                        //output.setSpan(new ForegroundColorSpan(0xff3792e5), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        output.setSpan(
                                new DrawableSpan(ResourcesCompat.getDrawable(getResources(), R.drawable.bit_below_underline, null)),
                                start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
            }
        }
    });
    titleTextView.setText(textToShow);

DrawableSpan.java

public class DrawableSpan extends ReplacementSpan {
    private Drawable mDrawable;
    private final Rect mPadding;

    public DrawableSpan(Drawable drawable) {
        super();
        mDrawable = drawable;
        mPadding = new Rect();
        mDrawable.getPadding(mPadding);
    }

    @Override
    public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
        RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
        mDrawable.setBounds((int) rect.left - mPadding.left, (int) rect.top - mPadding.top, (int) rect.right + mPadding.right, (int) rect.bottom + mPadding.bottom);
        canvas.drawText(text, start, end, x, y, paint);
        mDrawable.draw(canvas);
    }

    @Override
    public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        return Math.round(paint.measureText(text, start, end));
    }

    private float measureText(Paint paint, CharSequence text, int start, int end) {
        return paint.measureText(text, start, end);
    }
}

res/drawable/bit_below_underline.xml

<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="line">
    <padding android:bottom="30dp"/>
    <stroke
        android:width="1dp"
        android:color="#3792e5"/>
</shape>

I tried to call the 2 "setSpan" together (first is commented in the code above), but it didn't help.

The question

How can I set 2 spans on the part of the text, as I've specified above (partial text with placeholder) , so that one will be of text-color, and another of an customized-underline ?

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270

1 Answers1

1

When tested your code, it seems like ReplacementSpans are drawn before any CharacterStyleSpan . So try to use MetricAffectingSpan .It will draw before ReplacementSpans .

So Use custom MetricAffectingSpan instead of ForegroundColorSpan

import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;

public class BGColorSpan extends MetricAffectingSpan {

    private int color;

    public BGColorSpan(int color) {
        this.color = color;
    }

    @Override
    public void updateMeasureState(TextPaint textPaint) {
        textPaint.setColor(color);
    }

    @Override
    public void updateDrawState(TextPaint textPaint) {
        textPaint.setColor(color);
    }
} 

OK . I am trying to explain the issue from the android SDK Source code, In this for loop there is continue keyword it will skip the rendering of CharacterStyleSpan

 final float originalX = x;
    for (int i = start, inext; i < measureLimit; i = inext) {
        TextPaint wp = mWorkPaint;
        wp.set(mPaint);

        inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
                mStart;
        int mlimit = Math.min(inext, measureLimit);

        ReplacementSpan replacement = null;

        for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
            // Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
            // empty by construction. This special case in getSpans() explains the >= & <= tests
            if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
                    (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
            MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
            if (span instanceof ReplacementSpan) {
                replacement = (ReplacementSpan)span;
            } else {
                // We might have a replacement that uses the draw
                // state, otherwise measure state would suffice.
                span.updateDrawState(wp);
            }
        }

        if (replacement != null) {
            x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
                    bottom, fmi, needWidth || mlimit < measureLimit);

            // I think this line making your issue
            continue;
        }

        for (int j = i, jnext; j < mlimit; j = jnext) {
            jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
                    mStart;
            int offset = Math.min(jnext, mlimit);

            wp.set(mPaint);
            for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
                // Intentionally using >= and <= as explained above
                if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
                        (mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;

                CharacterStyle span = mCharacterStyleSpanSet.spans[k];
                span.updateDrawState(wp);
            }

            // Only draw hyphen on last run in line
            if (jnext < mLen) {
                wp.setHyphenEdit(0);
            }
            x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
                    top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
        }
    }       
Krish
  • 3,860
  • 1
  • 19
  • 32
  • I don't understand your solution. What exactly to change? – android developer Apr 20 '17 at 13:12
  • In your code , You are using ForegroundColorSpan. Please replace it with the code i mentioned here (BGColorSpan). – Krish Apr 20 '17 at 13:15
  • It works, but why does it work? What's different between ForegroundColorSpan and your class? Isn't ForegroundColorSpan just supposed to change the text color? It did it so far, with previous tests... What's going on? – android developer Apr 20 '17 at 13:37
  • ForegroundColorSpan is extended from CharacterStyle , this is low priority than ReplacementSpans. – Krish Apr 20 '17 at 13:39
  • What does it mean in this context? That no matter which span I use, it will replace the ForegroundColorSpan ? If so, why does it work fine with the code I wrote before (shown here: http://stackoverflow.com/a/14981952/878126 ) ? What's the difference ? – android developer Apr 20 '17 at 13:41
  • In this question you are using ReplacementSpan with ForegroundColorSpan. This will not work . I dont know why maybe it is a bug. check this link http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.0.2_r1/android/text/TextLine.java#936 – Krish Apr 20 '17 at 13:46
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/142186/discussion-between-krish-and-android-developer). – Krish Apr 20 '17 at 13:47
  • Oh you mean DrawableSpan ? From which span should I have extended? – android developer Apr 20 '17 at 13:50
  • DrawableSpan is ok with this context, but the ForegroundColorSpan which will not draw after a ReplacementSpan . That is my assumption from the link i mentioned above. – Krish Apr 20 '17 at 13:54
  • But even when I switched the order of them , it didn't work. Why would one replace the other even in this case? – android developer Apr 20 '17 at 14:10
  • Did you check the code that i send you? If you read it you will understand it. – Krish Apr 20 '17 at 14:11
  • You mean the Android Framework code? I don't understand almost any of it. – android developer Apr 20 '17 at 14:25
  • I have updated the answer with the source code description . Please check and accept the answer if it helps you. – Krish Apr 20 '17 at 14:42
  • What would it cause to skip CharacterStyleSpan in the above cases I've mentioned ? What's the difference between using it for SpannableString and TagHandler ? – android developer Apr 20 '17 at 16:49
  • Exactly what I needed! Thank you so much. – lilienberg May 29 '20 at 09:40