java android textview drawing mapsforge

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 los TextView .

  • 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í: