Home » Android » android – Can you explain the behavior of TextView + Gravity + SingleLine and Canvas?

android – Can you explain the behavior of TextView + Gravity + SingleLine and Canvas?

Posted by: admin June 15, 2020 Leave a comment

Questions:

I have a custom TextView with it’s onDraw method overridden to draw a red border around the text view.

When I set the gravity to something else than left AND singleLine=true : the border is not drawn.

  • Is there any reason for that ?
  • Is there any workaround ?

The following screenshot illustrate the problem : the second textView doesn’t have a border
enter image description here

TL means TOP|LEFT ; C means CENTER ; SL means single-line ; T means true ; F means false

Here is the code (can be copy/pasted and run – no need for a layout file)

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.RelativeLayout;
import android.widget.TextView;


public class MyActivity extends Activity {

    private final int TEXT_VIEW_WIDTH_PX = 400;
    private final int TEXT_VIEW_HEIGHT_PX = 60;
    private Paint borderPaint = new Paint();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RelativeLayout rootView = new RelativeLayout(this);
        getWindow().setContentView(rootView);

        borderPaint.setColor(Color.RED);

        MyTextView text1 = makeTextView("Grav=TL ; SL=F",100);
        rootView.addView(text1);

        MyTextView text2 = makeTextView("Grav=C ; SL=T",200);
        //combination of the 2 following attributes cause the issue
        text2.setSingleLine(true);
        text2.setGravity(Gravity.CENTER);
        rootView.addView(text2);

        MyTextView text3 = makeTextView("Grav=C ; SL=F",300);
        text3.setGravity(Gravity.CENTER);
        rootView.addView(text3);

        MyTextView text4 = makeTextView("Grav=TL ; SL=T",400);
        text4.setSingleLine(true);
        rootView.addView(text4);
    }

    /**
     * Custom TextView with red border
     */
    private class MyTextView extends TextView {

        private MyTextView(Context context) {
            super(context);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.drawLine(1,1,1,TEXT_VIEW_HEIGHT_PX-1,borderPaint);
            canvas.drawLine(1,1,TEXT_VIEW_WIDTH_PX-1,1,borderPaint);
            canvas.drawLine(TEXT_VIEW_WIDTH_PX-1,1,TEXT_VIEW_WIDTH_PX-1,TEXT_VIEW_HEIGHT_PX-1,borderPaint);
            canvas.drawLine(1,TEXT_VIEW_HEIGHT_PX-1,TEXT_VIEW_WIDTH_PX-1,TEXT_VIEW_HEIGHT_PX-1,borderPaint);
        }
    }


    /**
     * create a MyTextView with 'text' context and located at x=50 ; y=marginTop
     * (nothing relevant for the issue here)
     */
    private MyTextView makeTextView(String text, int marginTop){
        MyTextView textView = new MyTextView(this);
        textView.setText(text);
        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(TEXT_VIEW_WIDTH_PX,TEXT_VIEW_HEIGHT_PX);
        lp.topMargin = marginTop;
        lp.leftMargin = 50;
        textView.setLayoutParams(lp);
        return textView;
    }

}
How to&Answers:

Thanks to pskink’s thorough research (from the comment on my another answer):

If you want to know why custom onDraw doesn’t draw anything, just Log.d the value of canvas.getMatrix().

[It] seems that canvas is translated/horizontally scrolled (at least in my case) 8159 pixels to the left, so calling canvas.translate(8159, 0) fixes the issue, of course 8159 is not a magic number and can vary.

I found it, see VERY_WIDE constant in TextView, it is set to 16384 (2**14), in my case TextView has width of 66 and now (16384-66)/2 == 8159, voila!

…but VERY_WIDE is private so you cannot access it 🙁

From here, I wonder if the offset can be retrieved programmatically, and sure it does, easily by getScrollX(). This approach translates the canvas instead of “hacking” the single-line by cancelling horizontal scrolled. It displays the single-line more naturally.

Inside custom TextView:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // translate the canvas before drawing onto it, fixing the position
    canvas.translate(getScrollX(), 0);

    canvas.drawLine(1, 1, 1, TEXT_VIEW_HEIGHT_PX - 1, borderPaint);
    canvas.drawLine(1, 1, TEXT_VIEW_WIDTH_PX - 1, 1, borderPaint);
    canvas.drawLine(TEXT_VIEW_WIDTH_PX - 1, 1, TEXT_VIEW_WIDTH_PX - 1,
        TEXT_VIEW_HEIGHT_PX - 1, borderPaint);
    canvas.drawLine(1, TEXT_VIEW_HEIGHT_PX - 1, TEXT_VIEW_WIDTH_PX - 1,
        TEXT_VIEW_HEIGHT_PX - 1, borderPaint);
}

This code hasn’t been tested thoroughly for all cases. I only confirmed the 4 cases provided by the OP

Answer:

TL;DR

The problem seems to be from setSingleLine(). The workaround is to call setHorizontallyScrolling(false) after setSingleLine(true), but this will “wrap” the text if it’s too long. You can’t even truncate it by using setEllipsize(); it doesn’t have any effects.

MyTextView text2 = makeTextView(
    "Grav=C ; SL=T a ab abc abcd abcde abcdef abcdefg abcdefgh abcdefghi",
    200);
text2.setSingleLine(true);
text2.setHorizontallyScrolling(false);
text2.setGravity(Gravity.CENTER);

Original Post

I still couldn’t pinpoint the issue, but my guess is on the single-line.

When calling text2.setSingleLine(true);, Android will do this:

public void setSingleLine(boolean singleLine) {
    setInputTypeSingleLine(singleLine);
    applySingleLine(singleLine, true, true);
}

// no effect, since there is no editor
/*
private void setInputTypeSingleLine(boolean singleLine) {
    if (mEditor != null &&
            (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
        if (singleLine) {
            mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
        } else {
            mEditor.mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
        }
    }
}
*/

private void applySingleLine(boolean singleLine, boolean applyTransformation,
        boolean changeMaxLines) {
    mSingleLine = singleLine;
    if (singleLine) {
        setLines(1);
        setHorizontallyScrolling(true);
        if (applyTransformation) {
            setTransformationMethod(SingleLineTransformationMethod.getInstance());
        }
    } else {
        if (changeMaxLines) {
            setMaxLines(Integer.MAX_VALUE);
        }
        setHorizontallyScrolling(false);
        if (applyTransformation) {
            setTransformationMethod(null);
        }
    }
}

From here, I concluded that somewhere in applySingleLine() was causing this issue. Thus, I tried “simulating” the single-line by changing the code to below, and the issue is still there.

MyTextView text2 = makeTextView("Grav=C ; SL=T",200);
//text2.setSingleLine(true);
text2.setLines(1);
text2.setHorizontallyScrolling(true);
text2.setTransformationMethod(SingleLineTransformationMethod.getInstance());

text2.setGravity(Gravity.CENTER);
rootView.addView(text2);

But when I put setHorizontallyScrolling(true) into comment, the issue was gone! Thus, by calling setHorizontallyScrolling(false) after setSingleLine(true), it negates the effect and solves the issue, but this will “wrap” the text if it’s too long.