0

I want to have a textview with a double stroke in Android, which is not natively supported (not even a single stroke). Following Android textview outline text it can be easily achieved.

public class StrokedTextView extends androidx.appcompat.widget.AppCompatTextView {

// fields
private int strokeColorW, strokeColorB;
private float strokeWidthW, strokeWidthB;

// constructors
public StrokedTextView(Context context) {
    this(context, null, 0);
}

public StrokedTextView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public StrokedTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    strokeColorW = context.getColor(R.color.white);
    strokeWidthW = dpToPx(context, 2);
    strokeColorB = context.getColor(R.color.main_color_dark);
    strokeWidthB = dpToPx(context, 3);
}

// overridden methods
@Override
protected void onDraw(Canvas canvas) {
    //set paint to fill mode
    Paint p = getPaint();
    p.setStyle(Paint.Style.FILL);
    //draw the fill part of text
    super.onDraw(canvas);
    //save the text color
    int currentTextColor = getCurrentTextColor();
    //set paint to stroke mode and specify
    //stroke 1 color and width
    p.setStyle(Paint.Style.STROKE);
    p.setStrokeWidth(strokeWidthB);
    setTextColor(strokeColorB);
    //draw text stroke
    super.onDraw(canvas);
    //set paint to stroke mode and specify
    //stroke 2 color and width
    p.setStyle(Paint.Style.STROKE);
    p.setStrokeWidth(strokeWidthW);
    setTextColor(strokeColorW);
    //draw text stroke
    super.onDraw(canvas);
    //revert the color back to the one
    //initially specified
    setTextColor(currentTextColor);
}

public static int dpToPx(Context context, float dp)
{
    final float scale= context.getResources().getDisplayMetrics().density;
    return (int) (dp * scale + 0.5f);
}

}

The problem is that setTextColor calls invalidate, so this falls into infinite calls of onDraw. As some suggest, I've tried to control it with a flag, that indicates whether the invalidate is caused by the setTextColor or not. But it still calls onDraw infinitely

private boolean isDrawing = false;

@Override
public void invalidate() {
    if(isDrawing) return;
    super.invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
    isDrawing = true;

    Paint p = getPaint();
    p.setStyle(Paint.Style.FILL);
    super.onDraw(canvas);
    int currentTextColor = getCurrentTextColor();

    p.setStyle(Paint.Style.STROKE);
    p.setStrokeWidth(strokeWidthB);
    setTextColor(strokeColorB);
    super.onDraw(canvas);

    p.setStyle(Paint.Style.STROKE);
    p.setStrokeWidth(strokeWidthW);
    setTextColor(strokeColorW);
    super.onDraw(canvas);

    setTextColor(currentTextColor);

    isDrawing = false;
}

As other user states in that question, I've also tried using Reflection to access the private field of the TextView and brute-forcely set it:

private void setColor(int color){
    Field field = TextView.class.getDeclaredField("mCurTextColor");
    field.setAccessible(true);
    field.set(this, color);
}

However, it states Reflective access to mCurTextColor will throw an Exception when targeting API 33 and above

So I'm asking if someone sees a way to overcome this issue different from the ones I've already tried and failed at.

Josemafuen
  • 682
  • 2
  • 16
  • 41

1 Answers1

0

The solution would be to create your own custom View from scratch.

public class StrokedTextView extends View {
private final Paint TEXT_PAINT;
private final Paint WHITE_BORDER_PAINT;
private final Paint BROWN_BORDER_PAINT;

private String text;

private int desiredWidth, desiredHeight;
private final int bigBorderSize, halfMargin;

// constructors
public StrokedTextView(Context context) {
    this(context, null, 0);
}

public StrokedTextView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public StrokedTextView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);

    float textSize = 0;
    int textColor = context.getColor(R.color.main_color);
    text = "";
    if(attrs != null) {
        @SuppressLint("CustomViewStyleable") TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.StrokedTextAttrs);
        textSize = a.getDimensionPixelSize(R.styleable.StickerTextAttrs_textSize, 0);
        textColor = a.getColor(R.styleable.StrokedTextAttrs_textColor, textColor);
        text = a.getString(R.styleable.StrokedTextAttrs_text);
        a.recycle();
    }

    TEXT_PAINT = new Paint();
    TEXT_PAINT.setTextSize(textSize);
    TEXT_PAINT.setStyle(Paint.Style.FILL);
    TEXT_PAINT.setColor(textColor);

    int smallBorderSize = dpToPx(context, 2);
    bigBorderSize = smallBorderSize * 3;
    halfMargin = bigBorderSize / 2;

    WHITE_BORDER_PAINT = new Paint();
    WHITE_BORDER_PAINT.setTextSize(textSize);
    WHITE_BORDER_PAINT.setStyle(Paint.Style.STROKE);
    WHITE_BORDER_PAINT.setStrokeWidth(bigBorderSize);
    WHITE_BORDER_PAINT.setColor(context.getColor(R.color.white));

    BROWN_BORDER_PAINT = new Paint();
    BROWN_BORDER_PAINT.setTextSize(textSize);
    BROWN_BORDER_PAINT.setStyle(Paint.Style.STROKE);
    BROWN_BORDER_PAINT.setStrokeWidth(smallBorderSize);
    BROWN_BORDER_PAINT.setColor(context.getColor(R.color.main_color_dark));
    measure();
}

public void setText(String t){
    text = t;
    measure();
    invalidate();
    requestLayout();
}

private void measure(){
    Rect bounds = new Rect();
    TEXT_PAINT.getTextBounds(text, 0, text.length(), bounds);
    desiredHeight = bounds.height() + bigBorderSize;
    desiredWidth = bounds.width() + bigBorderSize;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int bottom = getHeight() - halfMargin;
    canvas.drawText(text, halfMargin, bottom, WHITE_BORDER_PAINT);
    canvas.drawText(text, halfMargin, bottom, BROWN_BORDER_PAINT);
    canvas.drawText(text, halfMargin, bottom, TEXT_PAINT);
}

public static int dpToPx(Context context, float dp)
{
    final float scale = context.getResources().getDisplayMetrics().density;
    return (int) (dp * scale + 0.5f);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width;
    int height;

    //Measure Width
    if (widthMode == MeasureSpec.EXACTLY) {
        //Must be this size
        width = widthSize;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        width = Math.min(desiredWidth, widthSize);
    } else {
        //Be whatever you want
        width = desiredWidth;
    }

    //Measure Height
    if (heightMode == MeasureSpec.EXACTLY) {
        //Must be this size
        height = heightSize;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        //Can't be bigger than...
        height = Math.min(desiredHeight, heightSize);
    } else {
        //Be whatever you want
        height = desiredHeight;
    }

    setMeasuredDimension(width, height);
}

and in the attr.xml:

<declare-styleable name="StickerTextAttrs">
    <attr name="textColor" format="color"/>
    <attr name="textSize" format="dimension"/>
    <attr name="text" format="string"/>
</declare-styleable>
Josemafuen
  • 682
  • 2
  • 16
  • 41