I have a TextView in my Activity to which I want to add a shadow. It is supposed to look like in OsmAnd (100% opaque):
But it looks like this:
I thought I might offer an alternative to the overlayed TextView
s solution. This solution implements a custom TextView
subclass which manipulates its TextPaint
object's properties to first draw the outline, and then draw the text on top of it.
Using this, you need only deal with one View
at a time, so changing something at runtime won't require calls on two separate TextView
s. This should also make it easier to utilize other niceties of TextView
- like compound drawables - and keep everything square, without redundant settings.
Reflection is used to avoid calling TextView
's setTextColor()
method, which invalidates the View
, and would cause an infinite draw loop, which, I believe, is most likely why solutions like this didn't work for you. Setting the color directly on the Paint
object doesn't work, due to how TextView
handles that in its onDraw()
method, hence the reflection.
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View.BaseSavedState;
import android.widget.TextView;
import java.lang.reflect.Field;
public class OutlineTextView extends TextView {
private Field colorField;
private int textColor;
private int outlineColor;
public OutlineTextView(Context context) {
this(context, null);
}
public OutlineTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public OutlineTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
try {
colorField = TextView.class.getDeclaredField("mCurTextColor");
colorField.setAccessible(true);
// If the reflection fails (which really shouldn't happen), we
// won't need the rest of this stuff, so we keep it in the try-catch
textColor = getTextColors().getDefaultColor();
// These can be changed to hard-coded default
// values if you don't need to use XML attributes
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.OutlineTextView);
outlineColor = a.getColor(R.styleable.OutlineTextView_outlineColor, Color.TRANSPARENT);
setOutlineStrokeWidth(a.getDimensionPixelSize(R.styleable.OutlineTextView_outlineWidth, 0));
a.recycle();
}
catch (NoSuchFieldException e) {
// Optionally catch Exception and remove print after testing
e.printStackTrace();
colorField = null;
}
}
@Override
public void setTextColor(int color) {
// We want to track this ourselves
// The super call will invalidate()
textColor = color;
super.setTextColor(color);
}
public void setOutlineColor(int color) {
outlineColor = color;
invalidate();
}
public void setOutlineWidth(float width) {
setOutlineStrokeWidth(width);
invalidate();
}
private void setOutlineStrokeWidth(float width) {
getPaint().setStrokeWidth(2 * width + 1);
}
@Override
protected void onDraw(Canvas canvas) {
// If we couldn't get the Field, then we
// need to skip this, and just draw as usual
if (colorField != null) {
// Outline
setColorField(outlineColor);
getPaint().setStyle(Paint.Style.STROKE);
super.onDraw(canvas);
// Reset for text
setColorField(textColor);
getPaint().setStyle(Paint.Style.FILL);
}
super.onDraw(canvas);
}
private void setColorField(int color) {
// We did the null check in onDraw()
try {
colorField.setInt(this, color);
}
catch (IllegalAccessException | IllegalArgumentException e) {
// Optionally catch Exception and remove print after testing
e.printStackTrace();
}
}
// Optional saved state stuff
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.textColor = textColor;
ss.outlineColor = outlineColor;
ss.outlineWidth = getPaint().getStrokeWidth();
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
textColor = ss.textColor;
outlineColor = ss.outlineColor;
getPaint().setStrokeWidth(ss.outlineWidth);
}
private static class SavedState extends BaseSavedState {
int textColor;
int outlineColor;
float outlineWidth;
SavedState(Parcelable superState) {
super(superState);
}
private SavedState(Parcel in) {
super(in);
textColor = in.readInt();
outlineColor = in.readInt();
outlineWidth = in.readFloat();
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(textColor);
out.writeInt(outlineColor);
out.writeFloat(outlineWidth);
}
public static final Parcelable.Creator<SavedState>
CREATOR = new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}
If using the custom XML attributes, the following needs to be in your <resources>
, which you can do by just sticking this file in your res/values/
folder, or adding to the one already there. If you don't wish to use the custom attributes, you should remove the relevant attribute processing from the View
's third constructor.
attrs.xml
<resources>
<declare-styleable name="OutlineTextView" >
<attr name="outlineColor" format="color" />
<attr name="outlineWidth" format="dimension" />
</declare-styleable>
</resources>
With the custom attributes, everything can be setup in the layout XML. Note the additional XML namespace, here named app
, and specified on the root LinearLayout
element.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#445566">
<com.example.testapp.OutlineTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="123 ABC"
android:textSize="36sp"
android:textColor="#000000"
app:outlineColor="#ffffff"
app:outlineWidth="2px" />
</LinearLayout>
The results:
Notes:
If you're using the support libraries, your OutlineTextView
class should instead extend AppCompatTextView
, to ensure that the tinting and whatnot are handled appropriately on all versions.
If the outline width is relatively large compared to the text size, it might be necessary to set additional padding on the View
to keep things within their bounds, especially if wrapping the width and/or height. This would be a concern with the overlayed TextView
s, too.
Relatively large outline widths can also result in undesirable sharp corner effects on certain characters - like "A" and "2" - due to the stroke style. This would also occur with the overlayed TextView
s.
This class can easily be converted to the EditText
equivalent, simply by changing the super class to EditText
, and passing android.R.attr.editTextStyle
in place of android.R.attr.textViewStyle
in the three-paramater constructor chain call. For the support libraries, the super class would be AppCompatEditText
, and the constructor argument R.attr.editTextStyle
.
Just for fun: I would point out that you can get some pretty nifty effects using translucent colors for the text and/or outline, and playing with the fill/stroke/fill-and-stroke styles. This, of course, would be possible with the overlayed TextView
s solution, as well.
As of API level 28 (Pie), there are certain Restrictions on non-SDK interfaces, including reflection to access normally inaccessible members in the SDK. Despite that, this solution still works, surprisingly, at least on the available Pie emulators, for both the native TextView
and the support AppCompatTextView
. I will update if that changes in the future.
I tried all the hacks, tips and tricks in the other posts like here, here and here.
None of them works that great or looks so good.
You use a FrameLayout (which has the characteristic of laying its components over each other) and put 2 TextViews inside at the same position.
MainActivity.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:background="#445566">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_weight="1">
<TextView
android:id="@+id/textViewShadowId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:textSize="36sp"
android:text="123 ABC"
android:textColor="#ffffff" />
<TextView
android:id="@+id/textViewId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:textSize="36sp"
android:text="123 ABC"
android:textColor="#000000" />
</FrameLayout>
</LinearLayout>
And in the onCreate
method of your activity you set the stroke width of the shadow TextView and change it from FILL to STROKE:
import android.graphics.Paint;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//here comes the magic
TextView textViewShadow = (TextView) findViewById(R.id.textViewShadowId);
textViewShadow.getPaint().setStrokeWidth(5);
textViewShadow.getPaint().setStyle(Paint.Style.STROKE);
}
}
The result looks like this:
I experienced the same issue, with calling setTextColor
in onDraw
causing infinite draw loop. I wanted to make my custom text view have a different fill color than the outline color when it rendered text. Which is why I was calling setTextColor
multiple times in onDraw
.
I found an alternative solution using an OutlineSpan
see https://github.com/santaevpavel/OutlineSpan. This is better than making the layout hierarchy complicated with multiple TextViews or using reflection and requires minimal changes. See the github page for more details.
Example
val outlineSpan = OutlineSpan(
strokeColor = Color.RED,
strokeWidth = 4F
)
val text = "Outlined text"
val spannable = SpannableString(text)
spannable.setSpan(outlineSpan, 0, 8, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
// Set text of TextView
binding.outlinedText.text = spannable