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):
- Cómo almacenar texto largo en Android?
- Android: última línea de texto cortada
- ¿Cuál es una buena manera de limitar el número de palabras que se pueden introducir en una vista de edición de Android?
- Agregar una fuente personalizada para la aplicación completa de Android
- Obtener id de TextView en Fragmento de FragmentActivity en ViewPager
Pero se ve así:
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" />
- ¿Cómo hacer la escritura de texto de animación?
- Vista de texto de Android sobre imagen con fondo transparente
- Envoltura de texto mediante simple_list_item_multiple_choice
- Android-TextView setText en Html.fromHtml para mostrar la imagen y el texto
- Cómo quitar la etiqueta de HTML en android?
- Desplazamiento por Android en la lista
- ¿Cómo crear una animación cambiante de color? (Androide)
- TextView y color de fondo
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:
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 laTextView
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
sTextView
. -
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í: