java - Agregue "sombra" opaca(contorno) a Android TextView
drawing mapsforge (2)
Tengo una TextView
de TextView
en mi actividad a la que deseo agregar una sombra. Se supone que debe verse en OsmAnd (100% opaco):
Pero se ve así:
Puedes ver que la sombra actual está borrosa y se desvanece. Quiero una sombra sólida y opaca. ¿Pero cómo?
Mi código actual es:
<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"
/>
Pensé que podría ofrecer una alternativa a la solución TextView
. Esta solución implementa una subclase TextView
personalizada que manipula las propiedades de su objeto TextPaint
para dibujar primero el contorno y luego dibujar el texto sobre él.
Al usar esto, solo necesita tratar con una View
a la vez, por lo que cambiar algo en tiempo de ejecución no requerirá llamadas en dos TextView
s separados. Esto también debería facilitar el uso de otras sutilezas de TextView
, como compuestos compuestos, y mantener todo en orden, sin configuraciones redundantes.
La reflexión se usa para evitar llamar al TextView
setTextColor()
, que invalida la View
, y causaría un bucle de sorteo infinito, que, creo, es muy probable que las soluciones como esta no funcionen para usted. Establecer el color directamente en el objeto Paint
no funciona, debido a cómo TextView
maneja eso en su método onDraw()
, de ahí el reflejo.
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];
}
};
}
}
Si usa los atributos XML personalizados, lo siguiente debe estar en sus <resources>
, lo cual puede hacer simplemente colocando este archivo en su carpeta res/values/
o agregando uno que ya esté allí. Si no desea utilizar los atributos personalizados, debe eliminar el procesamiento de los atributos relevantes del tercer constructor de View
.
attrs.xml
<resources>
<declare-styleable name="OutlineTextView" >
<attr name="outlineColor" format="color" />
<attr name="outlineWidth" format="dimension" />
</declare-styleable>
</resources>
Con los atributos personalizados, todo se puede configurar en el diseño XML. Tenga en cuenta el espacio de nombres XML adicional, aquí llamada app
, y especificado en el elemento raíz 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>
Los resultados:
Notas:
Tengo un banco de pruebas muy limitado en este momento, pero al revisar las versiones de origen, creo que debería funcionar desde API 8 (Froyo), hasta al menos API 23 (Marshmallow).
Si el ancho del perfil es relativamente grande en comparación con el tamaño del texto, puede ser necesario establecer un relleno adicional en la
View
para mantener las cosas dentro de sus límites, especialmente si se ajusta el ancho y / o la altura. Esto también sería una preocupación con losTextView
.Los anchos de contorno relativamente grandes también pueden dar como resultado efectos de esquina nítidos indeseados en ciertos caracteres, como "A" y "2", debido al estilo de trazo. Esto también ocurriría con los
TextView
.Solo por diversión: señalaría que puedes obtener algunos efectos bastante ingeniosos usando colores translúcidos para el texto y / o contorno, y jugando con los estilos de relleno / trazo / relleno y trazo. Esto, por supuesto, también sería posible con la solución
TextView
.
Probé todos los trucos, consejos y trucos en las otras publicaciones como aquí , aquí y aquí .
Ninguno de ellos funciona tan bien o se ve tan bien.
Ahora así es como realmente lo haces (se encuentra en la aplicación Source of the OsmAnd ):
Usas un FrameLayout (que tiene la característica de colocar sus componentes uno encima del otro) y colocas 2 TextViews en la misma posición.
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>
Y en el método onCreate
de su actividad, establece el ancho de trazo de la sombra de TextView y lo cambia de FILL a 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);
}
}
El resultado es así: