Cheok Yan Cheng Cheok Yan Cheng - 1 month ago 13
Android Question

Enlarge single character in SpannableString will have affect on line spacing (In Marshmallow only)

I have the following method, which enlarge the first character in

TextView
.

private void makeFirstLetterBig(TextView textView, String title) {
final SpannableString spannableString = new SpannableString(title);
int position = 0;
spannableString.setSpan(new RelativeSizeSpan(2.0f), position, position + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//textView.setText(spannableString.toString());
textView.setText(spannableString, BufferType.SPANNABLE);
}


Here's the
TextView
being used.

<TextView
android:id="@+id/title_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?attr/newsTitleTextViewSelector"
android:duplicateParentState="true"
android:text="TextView" />


Here's is the outcome before Marshmallow 6.

enter image description here

It looks pretty fine, as when the text content wraps to the 2nd line, the large first character doesn't affect 2nd line.

However, when running the same app in Marshmallow 6, I get the following outcome.

enter image description here

It seems that, the big character "I" is creating a large padding for the entire text content. This causes 2nd line (with Amazon) has a huge line spacing with 1st line.

May I know, how can I avoid such problem in marshmallow 6? I wish to have the same outcome as pre-marshmallow's.

p/s I filed a report at https://code.google.com/p/android/issues/detail?id=191187 too.

Update



On 9 December 2015, Google has fixed this issue and it will be released in Android 6.0.1 - https://code.google.com/p/android/issues/detail?id=191187

Answer

TL;DR The issue is caused by a bug in the method generate of StaticLayout.

The bug is a little different from what I thought initially, it doesn't affect all the rows but only those after the enlarged character, as you can see from the screenshot:

enter image description here

In Marshmallow a FontMetrics cache was added to avoid recomputing, as explained by a comment in the source code:

// measurement has to be done before performing line breaking
// but we don't want to recompute fontmetrics or span ranges the
// second time, so we cache those and then use those stored values

Unfortunately the fm.descent value available in the cache is discarded if it is lower than the previous cached value, as you can see in the snippet:

for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
    // retrieve end of span
    spanEnd = spanEndCache[spanEndCacheIndex++];

    // retrieve cached metrics, order matches above
    fm.top = fmCache[fmCacheIndex * 4 + 0];
    fm.bottom = fmCache[fmCacheIndex * 4 + 1];
    fm.ascent = fmCache[fmCacheIndex * 4 + 2];
    fm.descent = fmCache[fmCacheIndex * 4 + 3];
    fmCacheIndex++;

    if (fm.top < fmTop) {
        fmTop = fm.top;
    }
    if (fm.ascent < fmAscent) {
        fmAscent = fm.ascent;
    }
    if (fm.descent > fmDescent) {
        fmDescent = fm.descent;
    }
    if (fm.bottom > fmBottom) {
        fmBottom = fm.bottom;
    }

    ....

Once the value reach the max, it will never decrease and this is why every following line has an increased line space.