6

I'm currently working on extending a TextView, adding an outline around the text. Thus far, the only problem I've been having is my inability to position the "outline" correctly behind a text. If I code the extended class like the one portrayed below, I get a label that looks like this:

Note: in the above screenshot, I set the fill color to white, and the stroke color to black.

What am I doing wrong?

public class OutlinedTextView extends TextView {
    /* ===========================================================
     * Constants
     * =========================================================== */
    private static final float OUTLINE_PROPORTION = 0.1f;

    /* ===========================================================
     * Members
     * =========================================================== */
    private final Paint mStrokePaint = new Paint();
    private int mOutlineColor = Color.TRANSPARENT;

    /* ===========================================================
     * Constructors
     * =========================================================== */
    public OutlinedTextView(Context context) {
        super(context);
        this.setupPaint();
    }
    public OutlinedTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setupPaint();
        this.setupAttributes(context, attrs);
    }
    public OutlinedTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.setupPaint();
        this.setupAttributes(context, attrs);
    }

    /* ===========================================================
     * Overrides
     * =========================================================== */
    @Override
    protected void onDraw(Canvas canvas) {
        // Get the text to print
        final float textSize = super.getTextSize();
        final String text = super.getText().toString();

        // setup stroke
        mStrokePaint.setColor(mOutlineColor);
        mStrokePaint.setStrokeWidth(textSize * OUTLINE_PROPORTION);
        mStrokePaint.setTextSize(textSize);
        mStrokePaint.setFlags(super.getPaintFlags());
        mStrokePaint.setTypeface(super.getTypeface());

        // Figure out the drawing coordinates
        //mStrokePaint.getTextBounds(text, 0, text.length(), mTextBounds);

        // draw everything
        canvas.drawText(text,
                super.getWidth() * 0.5f, super.getBottom() * 0.5f,
                mStrokePaint);
        super.onDraw(canvas);
    }

    /* ===========================================================
     * Private/Protected Methods
     * =========================================================== */
    private final void setupPaint() {
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setTextAlign(Paint.Align.CENTER);
    }
    private final void setupAttributes(Context context, AttributeSet attrs) {
        final TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.OutlinedTextView);
        mOutlineColor = array.getColor(
                R.styleable.OutlinedTextView_outlineColor, 0x00000000);
        array.recycle(); 

        // Force this text label to be centered
        super.setGravity(Gravity.CENTER_HORIZONTAL);
    }
}
Octavian Helm
  • 39,405
  • 19
  • 98
  • 102
Japtar
  • 1,125
  • 1
  • 14
  • 24
  • I forgot to mention the current XML layout right now. While I don't have it on me yet, basically I'm attempting to center align the TextView both vertically and horizontally on top of an ImageButton. – Japtar Dec 03 '10 at 15:52

4 Answers4

5

Bah, that was stupid of me. I just needed to change-up that commented-out line:

super.getPaint().getTextBounds(text, 0, text.length(), mTextBounds);

In addition, for actually rendering the text, I need to average this view's height and the text's height:

// draw everything
canvas.drawText(text,
    super.getWidth() * 0.5f, (super.getHeight() + mTextBounds.height()) * 0.5f,
    mStrokePaint);

The entire code now reads as follows:

public class OutlinedTextView extends TextView {
    /* ===========================================================
     * Constants
     * =========================================================== */
    private static final float OUTLINE_PROPORTION = 0.1f;

    /* ===========================================================
     * Members
     * =========================================================== */
    private final Paint mStrokePaint = new Paint();
    private final Rect mTextBounds = new Rect();
    private int mOutlineColor = Color.TRANSPARENT;

    /* ===========================================================
     * Constructors
     * =========================================================== */
    public OutlinedTextView(Context context) {
        super(context);
        this.setupPaint();
    }
    public OutlinedTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.setupPaint();
        this.setupAttributes(context, attrs);
    }
    public OutlinedTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        this.setupPaint();
        this.setupAttributes(context, attrs);
    }

    /* ===========================================================
     * Overrides
     * =========================================================== */
    @Override
    protected void onDraw(Canvas canvas) {
        // Get the text to print
        final float textSize = super.getTextSize();
        final String text = super.getText().toString();

        // setup stroke
        mStrokePaint.setColor(mOutlineColor);
        mStrokePaint.setStrokeWidth(textSize * OUTLINE_PROPORTION);
        mStrokePaint.setTextSize(textSize);
        mStrokePaint.setFlags(super.getPaintFlags());
        mStrokePaint.setTypeface(super.getTypeface());

        // Figure out the drawing coordinates
        super.getPaint().getTextBounds(text, 0, text.length(), mTextBounds);

        // draw everything
        canvas.drawText(text,
                super.getWidth() * 0.5f, (super.getHeight() + mTextBounds.height()) * 0.5f,
                mStrokePaint);
        super.onDraw(canvas);
    }

    /* ===========================================================
     * Private/Protected Methods
     * =========================================================== */
    private final void setupPaint() {
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setTextAlign(Paint.Align.CENTER);
    }
    private final void setupAttributes(Context context, AttributeSet attrs) {
        final TypedArray array = context.obtainStyledAttributes(attrs,
                R.styleable.OutlinedTextView);
        mOutlineColor = array.getColor(
                R.styleable.OutlinedTextView_outlineColor, 0x00000000);
        array.recycle(); 

        // Force this text label to be centered
        super.setGravity(Gravity.CENTER_HORIZONTAL);
    }
}
Japtar
  • 1,125
  • 1
  • 14
  • 24
  • In hopes that this would help anyone, you actually don't have to calculate the absolute screen position to determine where to draw the text. OnDraw will actually draw relative to the boundaries of the view it's extending. Thus, to get a text stroke to center to a view, all I needed was the horizontal center of the view and the vertical center, offset by the text's height (for some reason, canvas.drawText has an option to horizontally center-align text, but not for vertical align). – Japtar Dec 04 '10 at 02:21
4

I've been plugging away with some of these examples for a while, as none seemed to line up quite right, and once I finally got a handle on what is happening with text and put my maths hat on I changed my onDraw for this to be the following and it sits perfectly no matter the size of the text or what size and shape it's containing view is...

@Override
protected void onDraw(Canvas canvas) {
    if (!isInEditMode()){
        // Get the text to print
        final float textSize = super.getTextSize();
        final String text = super.getText().toString();

        // setup stroke
        mStrokePaint.setColor(mOutlineColor);
        mStrokePaint.setStrokeWidth(textSize * mOutlineSize);
        mStrokePaint.setTextSize(textSize);
        mStrokePaint.setFlags(super.getPaintFlags());
        mStrokePaint.setTypeface(super.getTypeface());

        // draw everything
        canvas.drawText(text,
                (this.getWidth()-mStrokePaint.measureText(text))/2, this.getBaseline(),
                mStrokePaint);
    }
    super.onDraw(canvas);
}

Turned out to be far less maths and Rect calculating than a lot of solutions used too.

Edit: Forgot to mention that I copy the textalign of the super in the initialisation and DON'T force it to center. The drawText position calculated here is always going to be the properly centered position for the stroked text.

Zulaxia
  • 2,702
  • 2
  • 22
  • 23
  • Do you know if this will position correctly if the text is right-aligned, center-aligned, top-aligned, etc.? – Japtar Apr 27 '11 at 22:15
  • It will certainly cope with top and bottom aligned, since getBaseline is the proper baseline position as calculated for the 'real' text. The x position here is always going to be centered, however you could easy do a switch on the alignment and fiddle the maths since it's actually pretty simple once you find the right values to read. Left aligned would (ignoring padding) be 0 and right aligned would be getWidth()-measureText(text). You can read the padding from the text view for the left and right too if it needs that. I'm sure I'll make it cope with all this one day soon for another project. – Zulaxia Apr 27 '11 at 22:25
  • Makes sense. I can definitely cope with the lack of horizontal alignment, since I want center-alignment, anyways. I was aware of `getBaseline()`, but never thought about using it for some reason. Thanks for reminding me. – Japtar Apr 29 '11 at 20:53
2

I've been trying to make it work for some time and I have a solution, but it's for a special case only! It's possible to get Layout object that is used inside the TextView for drawing text. You can create a copy of this object and use it inside the onDraw(Canvas) method.

    final Layout originalLayout = super.getLayout();
    final Layout layout = new StaticLayout(text, mStrokePaint,
    originalLayout.getWidth(), originalLayout.getAlignment(),
    originalLayout.getSpacingMultiplier(), originalLayout.getSpacingAdd(), true);

    canvas.save();
    canvas.translate( layout.getLineWidth(0) * 0.5f, 0.0f );
    layout.draw(canvas);
    canvas.restore();

But I'm sure that it's not a good way for drawing outlines. I don't know how to track changes in a TextView.getLayout() object. Also it doesn't work for multiline TextViews and different gravities. And eventually this code has very poor performance because it allocates a Layout object on every draw. I don't understand exactly how it works, so I'd prefer not to use it.

Michael
  • 53,859
  • 22
  • 133
  • 139
0

There're a few attributes in the TextView class, like android:shadowColor, android:shadowDx, android:shadowDy and android:shadowRadius. It's seems to me that they do the same thing you want to implement. So maybe you should try a simple TextView at first.

Michael
  • 53,859
  • 22
  • 133
  • 139
  • The text shadow's not going to be the same thing Japtar's trying to accomplish. I'm actually quite interested in a solution for this as well. Text stroking (along with a drop shadow, actually) would be pretty useful! – Kevin Coppock Dec 03 '10 at 14:59
  • Yes, I have tried adding the shadows in before. As kcoppck said, however, it didn't achieve what I wanted. Great answer, nonetheless. – Japtar Dec 03 '10 at 15:58
  • I use shadows for outlining in my project. It's a pity it doesn't help you. – Michael Dec 03 '10 at 18:39