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.
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>