Añadir opaco "sombra" (esquema) a Android TextView

Tengo un TextView en mi actividad a la que quiero agregar una sombra. Se supone que parece en OsmAnd (100% opaco):

lo que quiero

Pero se ve así:

Lo que tengo

Puede 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 del objeto TextPaint para dibujar primero el contorno y, a continuación, dibujar el texto encima de él.

Usando esto, solo necesitas tratar con una View a la vez, así que cambiar algo en tiempo de ejecución no requerirá llamadas en dos TextView diferentes. Esto también debería hacer que sea más fácil utilizar otras sutilezas de TextView -como compuestos TextView y mantener todo cuadrado, sin ajustes redundantes.

Reflection se usa para evitar llamar al TextView setTextColor() , que invalida la View , y causaría un bucle de dibujo infinito, lo cual, creo, es muy probable que las soluciones como ésta no funcionaran 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í la reflexión.

 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 utiliza los atributos XML personalizados, lo siguiente debe estar en su <resources> , que puede hacer simplemente pegando este archivo en su carpeta res/values/ o agregando a la que ya está allí. Si no desea utilizar los atributos personalizados, debe quitar el procesamiento de atributos relevante 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 XML de diseño. Anote el espacio de nombres XML adicional, aquí denominado app , y se especifica 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:

captura de pantalla


Notas:

  • Tengo un testbed muy limitado en este momento, pero mirando a través de las versiones de origen, creo que esto debería funcionar desde todo el camino de vuelta en la API 8 (Froyo), hasta por lo menos API 23 (Marshmallow).

  • Si el ancho del contorno 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 sería una preocupación con la TextView s, también.

  • Anchos de contorno relativamente grandes también pueden resultar en efectos de ángulo agudo indeseables en ciertos caracteres – como "A" y "2" – debido al estilo de trazo. Esto también ocurriría con el TextView s TextView .

  • Sólo por diversión: Quisiera señalar que se pueden obtener algunos efectos bastante ingeniosos usando colores translúcidos para el texto y / o esquema, y ​​jugando con los estilos de relleno / trazo / relleno y trazo. Esto, por supuesto, sería posible con la solución TextView , también.

He probado todos los hacks, consejos y trucos en los otros puestos como aquí , aquí y aquí .

Ninguno de ellos funciona tan bien o se ve tan bien.

Ahora esto es lo que realmente lo haces (se encuentra en la Fuente de la aplicación OsmAnd ):

Se utiliza un FrameLayout (que tiene la característica de poner sus componentes unos sobre otros) y poner 2 TextViews dentro de 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, se onCreate el ancho de trazo de la sombra TextView y se 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 se ve así:

Captura de pantalla del resultado

  • Android sola línea TextView sin los puntos
  • Nueva línea no deseada en TextView utilizando HTML
  • Mostrar el menú contextual cuando se pulsa el enlace por largo tiempo en TextView
  • Desvanecimiento del borde TextView de múltiples líneas
  • Conversión de TextView a String (tipo de) Android
  • Android: LayoutParams para TextView hace que la vista desaparezca, programáticamente
  • Convertir el número en textview en int
  • Cómo establecer el color del texto en una vista de texto mediante programación
  • Android 2.2: ¿Cómo actualizar las vistas de texto automáticamente con un temporizador?
  • Android define el color de fondo en TextView en styles.xml
  • Finalizar el problema de la elipse con TextViews
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.