Add opaque "shadow" (outline) to Android TextView

juergen d :

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):

what I want

But it looks like this:

What I have

You can see that the current shadow is blurred and fades away. I want a solid, opaque shadow. But how?

My current code is:

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/speedTextView"
    android:text="25 km/h"

    android:textSize="24sp"
    android:textStyle="bold"
    android:textColor="#000000"
    android:shadowColor="#ffffff"
    android:shadowDx="0"
    android:shadowDy="0"
    android:shadowRadius="6"
/>
Mike M. :

I thought I might offer an alternative to the overlayed TextViews 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 TextViews. 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:

screenshot


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

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

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=451468&siteId=1