为Android TextView添加不透明的“阴影”(轮廓)

我有一个TextView在我想要添加阴影的活动。 它应该看起来像OsmAnd (100%不透明):

我想要的是

但看起来像这样:

我拥有的

你可以看到当前的阴影变得模糊并消失。 我想要一个坚实,不透明的阴影。 但是,如何?

我目前的代码是:

 <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" /> 

Solutions Collecting From Web of "为Android TextView添加不透明的“阴影”(轮廓)"

我想我可能会提供替代的覆盖TextView的解决scheme。 这个解决scheme实现了一个自定义的TextView子类,该子类处理其TextPaint对象的属性以首先绘制轮廓,然后在其上绘制文本。

使用这个,你只需要一次处理一个View ,所以在运行时改变一些东西不需要调用两个独立的TextView 。 这也使得更容易利用TextView其他细节 – 比如复合可绘制 – 并且保持一切正常,没有多余的设置。

reflection是用来避免调用TextViewsetTextColor()方法,这将无效的View ,并会导致无限的绘制循环,我相信, 这很可能为什么这样的解决scheme不适合你。 由于TextView在其onDraw()方法中如何处理它,因此直接在Paint对象上设置颜色不起作用。

 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]; } }; } } 

如果使用自定义的XML属性,需要在<resources>包含以下内容,只需将该文件粘贴到res/values/文件夹中,或者添加到已经存在的文件中即可。 如果你不想使用自定义属性,你应该从View的第三个构造函数中删除相关的属性处理。

attrs.xml

 <resources> <declare-styleable name="OutlineTextView" > <attr name="outlineColor" format="color" /> <attr name="outlineWidth" format="dimension" /> </declare-styleable> </resources> 

使用自定义属性,可以在布局XML中设置所有内容。 请注意额外的XML命名空间,这里命名为app ,并在根LinearLayout元素上指定。

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

结果:

截图


笔记:

  • 目前我有一个非常有限的testing平台,但通过源代码版本,我认为这应该从API 8(Froyo)一直到至lessAPI 23(Marshmallow)。

  • 如果轮廓宽度与文本大小相比相对较大,则可能需要在“ View上设置额外的填充以将事物限制在边界内,特别是在包裹宽度和/或高度时。 这也是重叠的TextView关心的问题。

  • 由于笔画风格的原因,相对较大的轮廓宽度也会对某些字符(如“A”和“2”)造成不良的尖angular效果。 这也会发生在叠加的TextView

  • 只是为了好玩:我会指出,你可以使用半透明的颜色为文本和/或轮廓,并使用填充/笔画/填充和笔画风格玩一些漂亮漂亮的效果。 当然,使用覆盖的TextView解决scheme也是可以的。

我在这里 , 这里和这里尝试所有的黑客,技巧和窍门在其他职位。

他们中的任何一个都不是很好,

现在,这是你真正做到这一点(在OsmAnd应用程序的源代码中find):

你使用一个FrameLayout(它具有相互放置组件的特性),并把2个TextView放在同一个位置。

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> 

在你的活动的onCreate方法中,设置阴影TextView的笔画宽度,并将其从FILL更改为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); } } 

结果如下所示:

结果截图