Android
Creación de vistas personalizadas
Buscar..
Creación de vistas personalizadas
Si necesita una vista completamente personalizada, tendrá que hacer una subclase de View
(la superclase de todas las vistas de Android) y proporcionar los onMeasure(...)
tamaño personalizado ( onMeasure(...)
) y drawing ( onDraw(...)
):
Crea tu esqueleto de vista personalizada: esto es básicamente el mismo para cada vista personalizada. Aquí creamos el esqueleto para una vista personalizada que puede dibujar un emoticono, llamado
SmileyView
:public class SmileyView extends View { private Paint mCirclePaint; private Paint mEyeAndMouthPaint; private float mCenterX; private float mCenterY; private float mRadius; private RectF mArcBounds = new RectF(); public SmileyView(Context context) { this(context, null, 0); } public SmileyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaints(); } private void initPaints() {/* ... */} @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {/* ... */} @Override protected void onDraw(Canvas canvas) {/* ... */} }
Inicialice sus pinturas: los objetos de
Paint
son los pinceles de su lienzo virtual que definen cómo se representan los objetos geométricos (por ejemplo, color, estilo de relleno y trazo, etc.). Aquí creamos dosPaint
s, una pintura llenado amarillo para el círculo y una pintura de trazo negro para los ojos y la boca:private void initPaints() { mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setColor(Color.YELLOW); mEyeAndMouthPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mEyeAndMouthPaint.setStyle(Paint.Style.STROKE); mEyeAndMouthPaint.setStrokeWidth(16 * getResources().getDisplayMetrics().density); mEyeAndMouthPaint.setStrokeCap(Paint.Cap.ROUND); mEyeAndMouthPaint.setColor(Color.BLACK); }
Implemente su propio
onMeasure(...)
: esto es necesario para que los diseños principales (por ejemplo,FrameLayout
) puedan alinear correctamente su vista personalizada. Proporciona un conjunto demeasureSpecs
demeasureSpecs
que puede usar para determinar la altura y el ancho de su vista. Aquí creamos un cuadrado asegurándonos de que la altura y el ancho son iguales:@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); int size = Math.min(w, h); setMeasuredDimension(size, size); }
Tenga en cuenta que
onMeasure(...)
debe contener al menos una llamada asetMeasuredDimension(..)
o, de lo contrario, su vista personalizada se bloqueará con unaIllegalStateException
.Implemente su propio
onSizeChanged(...)
: esto le permite capturar la altura y el ancho actuales de su vista personalizada para ajustar adecuadamente su código de representación. Aquí solo calculamos nuestro centro y nuestro radio:@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mCenterX = w / 2f; mCenterY = h / 2f; mRadius = Math.min(w, h) / 2f; }
Implemente su propio
onDraw(...)
: aquí es donde implementa la representación real de su vista. Proporciona un objetoCanvas
que puede dibujar (consulte la documentación oficial deCanvas
para conocer todos los métodos de dibujo disponibles).@Override protected void onDraw(Canvas canvas) { // draw face canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint); // draw eyes float eyeRadius = mRadius / 5f; float eyeOffsetX = mRadius / 3f; float eyeOffsetY = mRadius / 3f; canvas.drawCircle(mCenterX - eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); canvas.drawCircle(mCenterX + eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint); // draw mouth float mouthInset = mRadius /3f; mArcBounds.set(mouthInset, mouthInset, mRadius * 2 - mouthInset, mRadius * 2 - mouthInset); canvas.drawArc(mArcBounds, 45f, 90f, false, mEyeAndMouthPaint); }
Agregue su vista personalizada a un diseño: la vista personalizada ahora se puede incluir en cualquier archivo de diseño que tenga. Aquí simplemente lo
FrameLayout
dentro de unFrameLayout
:<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.example.app.SmileyView android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
Tenga en cuenta que se recomienda compilar su proyecto una vez finalizado el código de vista. Sin construirlo, no podrá ver la vista en una pantalla de vista previa en Android Studio.
Después de poner todo junto, debe recibir la siguiente pantalla después de iniciar la actividad que contiene el diseño anterior:
Agregando atributos a las vistas
Las vistas personalizadas también pueden tomar atributos personalizados que pueden usarse en archivos de recursos de diseño de Android. Para agregar atributos a su vista personalizada, debe hacer lo siguiente:
Defina el nombre y el tipo de sus atributos: esto se hace dentro de
res/values/attrs.xml
(res/values/attrs.xml
si es necesario). El siguiente archivo define un atributo de color para el color de la cara de nuestro smiley y un atributo de enumeración para la expresión del smiley:<resources> <declare-styleable name="SmileyView"> <attr name="smileyColor" format="color" /> <attr name="smileyExpression" format="enum"> <enum name="happy" value="0"/> <enum name="sad" value="1"/> </attr> </declare-styleable> <!-- attributes for other views --> </resources>
Use sus atributos dentro de su diseño: esto se puede hacer dentro de cualquier archivo de diseño que use su vista personalizada. El siguiente archivo de diseño crea una pantalla con un smiley amarillo feliz:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="match_parent" android:layout_width="match_parent"> <com.example.app.SmileyView android:layout_height="56dp" android:layout_width="56dp" app:smileyColor="#ffff00" app:smileyExpression="happy" /> </FrameLayout>
Consejo: los atributos personalizados no funcionan con las
tools:
prefijo en Android Studio 2.1 y versiones anteriores (y posiblemente en versiones futuras). En este ejemplo, reemplazar laapp:smileyColor
contools:smileyColor
resultaría en quesmileyColor
no se establezca durante el tiempo de ejecución ni en el momento del diseño.Lea sus atributos: esto se hace dentro del código fuente de su vista personalizada. El siguiente fragmento de
SmileyView
demuestra cómo se pueden extraer los atributos:public class SmileyView extends View { // ... public SmileyView(Context context) { this(context, null); } public SmileyView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, 0); mFaceColor = a.getColor(R.styleable.SmileyView_smileyColor, Color.TRANSPARENT); mFaceExpression = a.getInteger(R.styleable.SmileyView_smileyExpression, Expression.HAPPY); // Important: always recycle the TypedArray a.recycle(); // initPaints(); ... } }
(Opcional) Agregar estilo predeterminado: esto se hace agregando un estilo con los valores predeterminados y cargándolo dentro de su vista personalizada. El siguiente estilo de emoticono predeterminado representa un color amarillo feliz:
<!-- styles.xml --> <style name="DefaultSmileyStyle"> <item name="smileyColor">#ffff00</item> <item name="smileyExpression">happy</item> </style>
Que se aplica en nuestro
SmileyView
al agregarlo como el último parámetro de la llamada para obtenerobtainStyledAttributes
deobtainStyledAttributes
(vea el código en el paso 3):TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, R.style.DefaultSmileyViewStyle);
Tenga en cuenta que cualquier valor de atributo establecido en el archivo de diseño inflado (ver código en el paso 2) anulará los valores correspondientes del estilo predeterminado.
(Opcional) Proporcione estilos dentro de los temas: esto se hace agregando un nuevo atributo de referencia de estilo que puede usarse dentro de sus temas y proporcionando un estilo para ese atributo. Aquí simplemente
smileyStyle
nuestro atributo de referenciasmileyStyle
:<!-- attrs.xml --> <attr name="smileyStyle" format="reference" />
A continuación, proporcionamos un estilo en el tema de nuestra aplicación (aquí solo reutilizamos el estilo predeterminado del paso 4):
<!-- themes.xml --> <style name="AppTheme" parent="AppBaseTheme"> <item name="smileyStyle">@style/DefaultSmileyStyle</item> </style>
Creando una vista compuesta
Una vista compuesto es una costumbre ViewGroup
que se trata como una única vista por el código del programa circundante. Tal ViewGroup puede ser realmente útil en diseño similar a DDD , ya que puede corresponder a un agregado, en este ejemplo, un Contacto. Se puede reutilizar en cualquier lugar donde se muestre el contacto.
Esto significa que el código del controlador circundante, una Actividad, un Fragmento o un Adaptador, simplemente puede pasar el objeto de datos a la vista sin separarlo en una serie de widgets de IU diferentes.
Esto facilita la reutilización del código y permite un mejor diseño de acuerdo con los principios de SOLID .
El diseño XML
Esto suele ser donde empiezas. Tiene un bit de XML existente que reutiliza, tal vez como <include/>
. Extráigalo en un archivo XML separado y envuelva la etiqueta raíz en un elemento <merge>
:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/photo"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/photo" />
<TextView
android:id="@+id/phone_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toLeftOf="@id/photo" />
</merge>
Este archivo XML sigue funcionando perfectamente en el editor de diseño en Android Studio. Puedes tratarlo como cualquier otro diseño.
El compuesto ViewGroup
Una vez que tenga el archivo XML, cree el grupo de vista personalizado.
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ImageView;
import android.widget.TextView;
import myapp.R;
/**
* A compound view to show contacts.
*
* This class can be put into an XML layout or instantiated programmatically, it
* will work correctly either way.
*/
public class ContactView extends RelativeLayout {
// This class extends RelativeLayout because that comes with an automatic
// (MATCH_PARENT, MATCH_PARENT) layout for its child item. You can extend
// the raw android.view.ViewGroup class if you want more control. See the
// note in the layout XML why you wouldn't want to extend a complex view
// such as RelativeLayout.
// 1. Implement superclass constructors.
public ContactView(Context context) {
super(context);
init(context, null);
}
// two extra constructors left out to keep the example shorter
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ContactView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
// 2. Initialize the view by inflating an XML using `this` as parent
private TextView mName;
private TextView mPhoneNumber;
private ImageView mPhoto;
private void init(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.contact_view, this, true);
mName = (TextView) findViewById(R.id.name);
mPhoneNumber = (TextView) findViewById(R.id.phone_number);
mPhoto = (ImageView) findViewById(R.id.photo);
}
// 3. Define a setter that's expressed in your domain model. This is what the example is
// all about. All controller code can just invoke this setter instead of fiddling with
// lots of strings, visibility options, colors, animations, etc. If you don't use a
// custom view, this code will usually end up in a static helper method (bad) or copies
// of this code will be copy-pasted all over the place (worse).
public void setContact(Contact contact) {
mName.setText(contact.getName());
mPhoneNumber.setText(contact.getPhoneNumber());
if (contact.hasPhoto()) {
mPhoto.setVisibility(View.VISIBLE);
mPhoto.setImageBitmap(contact.getPhoto());
} else {
mPhoto.setVisibility(View.GONE);
}
}
}
El método init(Context, AttributeSet)
es donde leería cualquier atributo XML personalizado tal como se explica en Agregar atributos a las vistas .
Con estas piezas en su lugar, puedes usarlo en tu aplicación.
Uso en XML
Aquí hay un ejemplo de fragment_contact_info.xml
que ilustra cómo pondría un único ContactView encima de una lista de mensajes:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- The compound view becomes like any other view XML element -->
<myapp.ContactView
android:id="@+id/contact"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/message_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
Uso en Código
Aquí hay un ejemplo de RecyclerView.Adapter
que muestra una lista de contactos. Este ejemplo ilustra cuánto más limpio está el código del controlador cuando está completamente libre de manipulación de vistas.
package myapp;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
public class ContactsAdapter extends RecyclerView.Adapter<ContactsViewHolder> {
private final Context context;
public ContactsAdapter(final Context context) {
this.context = context;
}
@Override
public ContactsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ContactView v = new ContactView(context); // <--- this
return new ContactsViewHolder(v);
}
@Override
public void onBindViewHolder(ContactsViewHolder holder, int position) {
Contact contact = this.getItem(position);
holder.setContact(contact); // <--- this
}
static class ContactsViewHolder extends RecyclerView.ViewHolder {
public ContactsViewHolder(ContactView itemView) {
super(itemView);
}
public void setContact(Contact contact) {
((ContactView) itemView).setContact(contact); // <--- this
}
}
}
Consejos de rendimiento de CustomView
No asignar nuevos objetos en onDraw
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint(); //Do not allocate here
}
En lugar de dibujar dibujables en lienzo ...
drawable.setBounds(boundsRect);
drawable.draw(canvas);
Use un mapa de bits para un dibujo más rápido:
canvas.drawBitmap(bitmap, srcRect, boundsRect, paint);
No vuelva a dibujar la vista completa para actualizar solo una pequeña parte de ella. En su lugar, vuelva a dibujar la parte específica de la vista.
invalidate(boundToBeRefreshed);
Si su vista está haciendo una animación continua, por ejemplo, una onStop()
muestra cada segundo, al menos detenga la animación en onStop()
de la actividad y comience de nuevo en onStart()
de la actividad.
No realice ningún cálculo dentro del método onDraw
de una vista, en lugar de eso, debe terminar de dibujar antes de llamar a invalidate()
. Al utilizar esta técnica, puede evitar que el cuadro se caiga en su vista.
Rotaciones
Las operaciones básicas de una vista son traducir, rotar, etc. Casi todos los desarrolladores se han enfrentado a este problema cuando usan mapas de bits o degradados en su vista personalizada. Si la vista va a mostrar una vista girada y el mapa de bits debe girarse en esa vista personalizada, muchos de nosotros pensamos que será caro. Muchos piensan que rotar un mapa de bits es muy costoso porque para hacer eso, es necesario traducir la matriz de píxeles del mapa de bits. Pero la verdad es que no es tan difícil! En lugar de rotar el mapa de bits, ¡simplemente gire el lienzo!
// Save the canvas state
int save = canvas.save();
// Rotate the canvas by providing the center point as pivot and angle
canvas.rotate(pivotX, pivotY, angle);
// Draw whatever you want
// Basically whatever you draw here will be drawn as per the angle you rotated the canvas
canvas.drawBitmap(...);
// Now restore your your canvas to its original state
canvas.restore(save);
// Unless canvas is restored to its original state, further draw will also be rotated.
Vista compuesta para SVG / VectorDrawable as drawableRight
El motivo principal para desarrollar esta vista compuesta es que, por debajo de 5.0, los dispositivos no son compatibles con svg en drawable dentro de TextView / EditText. Uno más es pros, podemos establecer height
y width
de drawableRight
dentro EditText
. Lo he separado de mi proyecto y lo he creado en un módulo separado.
Nombre del módulo: custom_edit_drawable (nombre corto para prefijo-c_d_e)
Se utiliza el prefijo "c_d_e_" para que los recursos del módulo de la aplicación no los anulen por error. Ejemplo: Google utiliza el prefijo "abc" en la biblioteca de soporte.
construir.gradle
dependencies {
compile 'com.android.support:appcompat-v7:25.3.1'
}
utilizar AppCompat> = 23
Archivo de diseño: c_e_d_compound_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/edt_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:paddingEnd="40dp"
android:paddingLeft="5dp"
android:paddingRight="40dp"
android:paddingStart="5dp" />
<!--make sure you are not using ImageView instead of this-->
<android.support.v7.widget.AppCompatImageView
android:id="@+id/drawbleRight_search"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="right|center_vertical"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp" />
</FrameLayout>
Atributos personalizados: attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="EditTextWithDrawable">
<attr name="c_e_d_drawableRightSVG" format="reference" />
<attr name="c_e_d_hint" format="string" />
<attr name="c_e_d_textSize" format="dimension" />
<attr name="c_e_d_textColor" format="color" />
</declare-styleable>
</resources>
Código: EditTextWithDrawable.java
public class EditTextWithDrawable extends FrameLayout {
public AppCompatImageView mDrawableRight;
public EditText mEditText;
public EditTextWithDrawable(Context context) {
super(context);
init(null);
}
public EditTextWithDrawable(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(attrs);
}
private void init(AttributeSet attrs) {
if (attrs != null && !isInEditMode()) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.c_e_d_compound_view, this, true);
mDrawableRight = (AppCompatImageView) ((FrameLayout) getChildAt(0)).getChildAt(1);
mEditText = (EditText) ((FrameLayout) getChildAt(0)).getChildAt(0);
TypedArray attributeArray = getContext().obtainStyledAttributes(
attrs,
R.styleable.EditTextWithDrawable);
int drawableRes =
attributeArray.getResourceId(
R.styleable.EditTextWithDrawable_c_e_d_drawableRightSVG, -1);
if (drawableRes != -1) {
mDrawableRight.setImageResource(drawableRes);
}
mEditText.setHint(attributeArray.getString(
R.styleable.EditTextWithDrawable_c_e_d_hint));
mEditText.setTextColor(attributeArray.getColor(
R.styleable.EditTextWithDrawable_c_e_d_textColor, Color.BLACK));
int textSize = attributeArray.getDimensionPixelSize(R.styleable.EditTextWithDrawable_c_e_d_textSize, 15);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
android.view.ViewGroup.LayoutParams layoutParams = mDrawableRight.getLayoutParams();
layoutParams.width = (textSize * 3) / 2;
layoutParams.height = (textSize * 3) / 2;
mDrawableRight.setLayoutParams(layoutParams);
attributeArray.recycle();
}
}
}
Ejemplo: Cómo usar la vista superior
Diseño: activity_main.xml
<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="wrap_content"
android:orientation="vertical">
<com.customeditdrawable.AppEditTextWithDrawable
android:id="@+id/edt_search_emp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:c_e_d_drawableRightSVG="@drawable/ic_svg_search"
app:c_e_d_hint="@string/hint_search_here"
app:c_e_d_textColor="@color/text_color_dark_on_light_bg"
app:c_e_d_textSize="@dimen/text_size_small" />
</LinearLayout>
Actividad: MainActivity.java
public class MainActivity extends AppCompatActivity {
EditTextWithDrawable mEditTextWithDrawable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditTextWithDrawable= (EditTextWithDrawable) findViewById(R.id.edt_search_emp);
}
}
Respondiendo a los eventos táctiles
Muchas vistas personalizadas deben aceptar la interacción del usuario en forma de eventos táctiles. Puede obtener acceso a eventos táctiles anulando onTouchEvent
. Hay una serie de acciones que puedes filtrar. Los principales son
-
ACTION_DOWN
: Esto se activa una vez cuando su dedo toca la vista por primera vez. -
ACTION_MOVE
: se llama cada vez que su dedo se mueve un poco en la vista. Se llama muchas veces. -
ACTION_UP
: esta es la última acción a la que se llama cuando levanta el dedo de la pantalla.
Puede agregar el siguiente método a su vista y luego observar la salida del registro cuando toca y mueve su dedo alrededor de su vista.
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i("CustomView", "onTouchEvent: ACTION_DOWN: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_MOVE:
Log.i("CustomView", "onTouchEvent: ACTION_MOVE: x = " + x + ", y = " + y);
break;
case MotionEvent.ACTION_UP:
Log.i("CustomView", "onTouchEvent: ACTION_UP: x = " + x + ", y = " + y);
break;
}
return true;
}
Otras lecturas: