Android Spannable Line Height

titanic_fanatic picture titanic_fanatic · Oct 16, 2012 · Viewed 10.6k times · Source

I have been trying to figure this one out for the last couple of days now, and have had no success...

I'm learning android right now, and am currently creating a calculator with history as my learning project. I have a TextView that is responsible for displaying all history... I'm using a digital font that looks like a calculator font, but this only looks good for digits and decimals and comma's. I want all operators to be highlighted and in a different font (Arial Narrow at the moment). I have been able to get this to work beautifully using a spannable string where I'm specifying a font color as well as a font using a CustomTypeFaceSpan class to apply my custom fonts.

The problem... When I mix the Typefaces, there seems to be an issue with the line height, so I found this post which demonstrates using another custom defined class to apply a line height to each added line of spannable text:

public class CustomLineHeightSpan implements LineHeightSpan{
    private final int height;

    public CustomLineHeightSpan(int height){
        this.height = height;
    }

    @Override
    public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, FontMetricsInt fm) {
        fm.bottom += height;
        fm.descent += height;
    }

}

This does not seem to work, and I can not figure out why. If I don't apply the different typefaces, then it displays as expected with no space above the first line, and about 5px spacing between lines. When I apply the alternate typefaces, there is a space of about 10 to 15px above the first line of text and the line spacing is about the same 10 to 15px.

There is no difference in the font size, only the typeface. What am I missing. I implemented the CustomLineHeightSpan which implements LineHeightSpan and overrides the chooseHeight method. I call it like so:

WordtoSpan.setSpan(new CustomLineHeightSpan(10), operatorPositions.get(ii), operatorPositions.get(ii) + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

It does not seem to matter what I put in the call to CustomLineHeightSpan. Nothing changes...

Anybody have any idea what I'm missing... I'm sure it's an "I can't believe I missed that" answer, but can't seem to figure it out at the moment.

Thanks for the help guys :-)

Answer

titanic_fanatic picture titanic_fanatic · Oct 17, 2012

I finally found a more in depth example of the use of LineHeightSpan... Actually LineHeightSpan.WithDensity to be more precise... The following is the excerpt that helped me to resolve my issue:

private static class Height implements LineHeightSpan.WithDensity {
    private int mSize;
    private static float sProportion = 0;

    public Height(int size) {
        mSize = size;
    }

    public void chooseHeight(CharSequence text, int start, int end,
                             int spanstartv, int v,
                             Paint.FontMetricsInt fm) {
        // Should not get called, at least not by StaticLayout.
        chooseHeight(text, start, end, spanstartv, v, fm, null);
    }

    public void chooseHeight(CharSequence text, int start, int end,
                             int spanstartv, int v,
                             Paint.FontMetricsInt fm, TextPaint paint) {
        int size = mSize;
        if (paint != null) {
            size *= paint.density;
        }

        if (fm.bottom - fm.top < size) {
            fm.top = fm.bottom - size;
            fm.ascent = fm.ascent - size;
        } else {
            if (sProportion == 0) {
                /*
                 * Calculate what fraction of the nominal ascent
                 * the height of a capital letter actually is,
                 * so that we won't reduce the ascent to less than
                 * that unless we absolutely have to.
                 */

                Paint p = new Paint();
                p.setTextSize(100);
                Rect r = new Rect();
                p.getTextBounds("ABCDEFG", 0, 7, r);

                sProportion = (r.top) / p.ascent();
            }

            int need = (int) Math.ceil(-fm.top * sProportion);

            if (size - fm.descent >= need) {
                /*
                 * It is safe to shrink the ascent this much.
                 */

                fm.top = fm.bottom - size;
                fm.ascent = fm.descent - size;
            } else if (size >= need) {
                /*
                 * We can't show all the descent, but we can at least
                 * show all the ascent.
                 */

                fm.top = fm.ascent = -need;
                fm.bottom = fm.descent = fm.top + size;
            } else {
                /*
                 * Show as much of the ascent as we can, and no descent.
                 */

                fm.top = fm.ascent = -size;
                fm.bottom = fm.descent = 0;
            }
        }
    }
}

This was taken from this example.

What it does is as quoted below:

Forces the text line to be the specified height, shrinking/stretching the ascent if possible, or the descent if shrinking the ascent further will make the text unreadable.

I hope this helps the next person :-)