Personalización de Android: cómo crear un componente de interfaz de usuario que haga lo que usted quiere

Publicado: 2022-03-11

No es raro que los desarrolladores se encuentren en la necesidad de un componente de interfaz de usuario que no se proporciona en la plataforma a la que se dirigen o que, de hecho, se proporciona, pero carece de una determinada propiedad o comportamiento. La respuesta a ambos escenarios es un componente de interfaz de usuario personalizado.

El modelo de interfaz de usuario de Android es inherentemente personalizable y ofrece los medios de personalización y prueba de Android y la capacidad de crear componentes de interfaz de usuario personalizados de varias maneras:

  • Heredar un componente existente (es decir, TextView , ImageView , etc.) y agregar/anular la funcionalidad necesaria. Por ejemplo, un CircleImageView que hereda ImageView , anulando la función onDraw() para restringir la imagen mostrada a un círculo y agregando una función loadFromFile() para cargar una imagen desde la memoria externa.

  • Cree un componente compuesto a partir de varios componentes. Este enfoque generalmente aprovecha los diseños para controlar cómo se organizan los componentes en la pantalla. Por ejemplo, un LabeledEditText que hereda LinearLayout con orientación horizontal y contiene un TextView que actúa como una etiqueta y un EditText que actúa como un campo de entrada de texto.

    Este enfoque también podría hacer uso del anterior, es decir, los componentes internos podrían ser nativos o personalizados.

  • El enfoque más versátil y más complejo es crear un componente autodibujado . En este caso, el componente heredaría la clase View genérica y anularía funciones como onMeasure() para determinar su diseño, onDraw() para mostrar su contenido, etc. Los componentes creados de esta manera generalmente dependen en gran medida de la API de dibujo 2D de Android.

Estudio de caso de personalización de Android: CalendarView

Android proporciona un componente CalendarView nativo. Funciona bien y proporciona la funcionalidad mínima esperada de cualquier componente de calendario, mostrando un mes completo y resaltando el día actual. Algunos podrían decir que también se ve bien, pero solo si busca una apariencia nativa y no tiene interés en personalizar su apariencia.

Por ejemplo, el componente CalendarView no proporciona ninguna forma de cambiar cómo se marca un día determinado o qué color de fondo usar. Tampoco hay forma de agregar texto o gráficos personalizados, para marcar una ocasión especial, por ejemplo. En resumen, el componente se ve así, y casi nada se puede cambiar:

Captura de pantalla
CalendarView en el tema AppCompact.Light .

Haz lo tuyo

Entonces, ¿cómo hace uno para crear su propia vista de calendario? Cualquiera de los enfoques anteriores funcionaría. Sin embargo, la practicidad generalmente descartará la tercera opción (gráficos 2D) y nos dejará con los otros dos métodos, y emplearemos una combinación de ambos en este artículo.

Para seguir, puede encontrar el código fuente aquí.

1. El diseño de componentes

Primero, comencemos con el aspecto del componente. Para simplificar las cosas, mostremos los días en una cuadrícula y, en la parte superior, el nombre del mes junto con los botones "mes siguiente" y "mes anterior".

Captura de pantalla
Vista de calendario personalizada.

Este diseño se define en el archivo control_calendar.xml , de la siguiente manera. Tenga en cuenta que algunas marcas repetitivas se han abreviado con ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. La clase de componente

El diseño anterior se puede incluir tal cual en una Activity o un Fragment y funcionará bien. Pero encapsularlo como un componente de interfaz de usuario independiente evitará la repetición de código y permitirá un diseño modular, donde cada módulo maneja una responsabilidad.

Nuestro componente de interfaz de usuario será un LinearLayout , para que coincida con la raíz del archivo de diseño XML. Tenga en cuenta que solo se muestran las partes importantes del código. La implementación del componente reside en CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

El código es bastante sencillo. Tras la creación, el componente infla el diseño XML y, una vez hecho esto, asigna los controles internos a las variables locales para facilitar el acceso más adelante.

3. Se necesita algo de lógica

Para hacer que este componente se comporte realmente como una vista de calendario, es necesaria cierta lógica comercial. Puede parecer complicado al principio, pero en realidad no hay mucho que hacer. Vamos a desglosarlo:

  1. La vista del calendario tiene siete días de ancho y se garantiza que todos los meses comenzarán en algún lugar de la primera fila.

  2. Primero, debemos averiguar en qué posición comienza el mes, luego llenar todas las posiciones anteriores con los números del mes anterior (30, 29, 28, etc.) hasta llegar a la posición 0.

  3. Luego, llenamos los días del mes actual (1, 2, 3…etc).

  4. Después vienen los días del próximo mes (nuevamente, 1, 2, 3, etc.), pero esta vez solo llenamos las posiciones restantes en la(s) última(s) fila(s) de la cuadrícula.

El siguiente diagrama ilustra esos pasos:

Captura de pantalla
Lógica empresarial de vista de calendario personalizada.

El ancho de la cuadrícula ya está especificado en siete celdas, lo que indica un calendario semanal, pero ¿qué tal la altura? El tamaño más grande de la cuadrícula se puede determinar en el peor de los casos de un mes de 31 días que comience un sábado, que es la última celda de la primera fila, y necesitará 5 filas más para mostrarse en su totalidad. Por lo tanto, configurar el calendario para que muestre seis filas (un total de 42 días) será suficiente para manejar todos los casos.

¡Pero no todos los meses tienen 31 días! Podemos evitar las complicaciones derivadas de eso mediante el uso de la funcionalidad de fecha integrada de Android, evitando la necesidad de averiguar la cantidad de días nosotros mismos.

Como se mencionó anteriormente, las funcionalidades de fecha proporcionadas por la clase Calendar hacen que la implementación sea bastante sencilla. En nuestro componente, la función updateCalendar() implementa esta lógica:

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. Personalizable de corazón

Dado que el componente responsable de mostrar los días individuales es GridView , un buen lugar para personalizar la forma en que se muestran los días es el Adapter , ya que es responsable de mantener los datos e inflar las vistas de las celdas de cuadrícula individuales.

Para este ejemplo, necesitaremos lo siguiente de nuestro CalendearView :

  • El día actual debe estar en negrita azul .
  • Los días fuera del mes actual deben estar atenuados .
  • Los días con un evento deben mostrar un icono especial.
  • El encabezado del calendario debe cambiar de color según la temporada (verano, otoño, invierno, primavera).

Los tres primeros requisitos son fáciles de lograr cambiando los atributos de texto y los recursos de fondo. Implementemos un CalendarAdapter para llevar a cabo esta tarea. Es tan simple que puede ser una clase miembro en CalendarView . Al anular la función getView() , podemos lograr los requisitos anteriores:

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

El requisito de diseño final requiere un poco más de trabajo. Primero, agreguemos los colores para las cuatro estaciones en /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

Luego, usemos una matriz para definir la estación de cada mes (asumiendo que es el hemisferio norte, por simplicidad; ¡lo siento, Australia!). En CalendarView agregamos las siguientes variables miembro:

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

De esta manera, la selección de un color apropiado se hace seleccionando la temporada apropiada ( monthSeason[currentMonth] ) y luego eligiendo el color correspondiente ( rainbow[monthSeason[currentMonth] ), esto se agrega a updateCalendar() para asegurarse de que se seleccione el color apropiado cada vez que se cambia el calendario.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

Con eso, obtenemos el siguiente resultado:

Personalización de Android
El color de la cabecera cambia según la temporada.

Nota importante Debido a la forma en que HashSet compara los objetos, la verificación anterior eventDays.contains(date) en updateCalendar() no dará como resultado verdadero para los objetos de fecha a menos que sean exactamente idénticos. No realiza ninguna comprobación especial para el tipo de datos Date . Para evitar esto, esta verificación se reemplaza por el siguiente código:

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. Se ve feo en tiempo de diseño

La elección de Android para marcadores de posición en tiempo de diseño puede ser cuestionable. Afortunadamente, Android en realidad crea una instancia de nuestro componente para representarlo en el diseñador de la interfaz de usuario, y podemos aprovechar esto llamando a updateCalendar() en el constructor del componente. De esta manera, el componente realmente tendrá sentido en el tiempo de diseño.

Captura de pantalla

Si inicializar el componente requiere mucho procesamiento o carga muchos datos, puede afectar el rendimiento del IDE. En este caso, Android proporciona una función ingeniosa llamada isInEditMode() que se puede usar para limitar la cantidad de datos utilizados cuando el componente se crea una instancia en el diseñador de la interfaz de usuario. Por ejemplo, si hay muchos eventos para cargar en CalendarView , podemos usar isInEditMode() dentro de la función updateCalendar() para proporcionar una lista de eventos vacía/limitada en el modo de diseño y, de lo contrario, cargar la real.

6. Invocando el Componente

El componente se puede incluir en archivos de diseño XML (se puede encontrar un ejemplo de uso en activity_main.xml ):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

Y recuperado para interactuar una vez que se carga el diseño:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

El código anterior crea un HashSet de eventos, le agrega el día actual y luego lo pasa a CalendarView . Como resultado, CalendarView mostrará el día actual en negrita azul y también colocará el marcador de evento en él:

Captura de pantalla
CalendarView mostrando un evento

7. Agregar atributos

Otra función proporcionada por Android es asignar atributos a un componente personalizado. Esto permite a los desarrolladores de Android usar el componente para seleccionar la configuración a través del diseño XML y ver el resultado inmediatamente en el diseñador de la interfaz de usuario, en lugar de tener que esperar y ver cómo se ve CalendarView en tiempo de ejecución. Agreguemos la capacidad de cambiar la visualización del formato de fecha en el componente, por ejemplo, para deletrear el nombre completo del mes en lugar de la abreviatura de tres letras.

Para hacer esto, se necesitan los siguientes pasos:

  • Declarar el atributo. Llamémoslo formato de fecha y dateFormat un tipo de datos de string . Agréguelo a /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Use el atributo en el diseño que está usando el componente y asígnele el valor "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • Finalmente, haga que el componente haga uso del valor del atributo:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Cree el proyecto y notará que la fecha mostrada cambia en el diseñador de la interfaz de usuario para usar el nombre completo del mes, como "Julio de 2015". Intente proporcionar diferentes valores y vea qué sucede.

Captura de pantalla
Cambiar los atributos de CalendarView .

8. Interactuando con el Componente

¿Has probado a pulsar en un día concreto? Los elementos internos de la interfaz de usuario en nuestro componente aún se comportan de la forma normal esperada y activarán eventos en respuesta a las acciones del usuario. Entonces, ¿cómo manejamos esos eventos?

La respuesta implica dos partes:

  • Capture eventos dentro del componente y
  • Informe de eventos al padre del componente (podría ser un Fragment , una Activity o incluso otro componente).

La primera parte es bastante sencilla. Por ejemplo, para manejar los elementos de cuadrícula que se presionan durante mucho tiempo, asignamos un oyente correspondiente en nuestra clase de componente:

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

Hay varios métodos para reportar eventos. Una directa y simple es copiar la forma en que lo hace Android: proporciona una interfaz para los eventos del componente que implementa el padre del componente ( eventHandler en el fragmento de código anterior).

Las funciones de la interfaz pueden pasar cualquier dato que sea relevante para la aplicación. En nuestro caso, la interfaz necesita exponer un controlador de eventos, que pasa la fecha del día presionado. La siguiente interfaz se define en CalendarView :

 public interface EventHandler { void onDayLongPress(Date date); }

La implementación proporcionada por el padre se puede proporcionar a la vista de calendario a través de setEventHandler() . Aquí hay un ejemplo de uso de `MainActivity.java':

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

Al presionar prolongadamente un día, se activará un evento de presión prolongada que GridView captura y maneja y se informa llamando a onDayLongPress() en la implementación proporcionada, que, a su vez, mostrará la fecha del día presionado en la pantalla:

Captura de pantalla

Otra forma más avanzada de manejar esto es usar Intents y BroadcastReceivers de Android. Esto es particularmente útil cuando varios componentes necesitan ser notificados del evento del calendario. Por ejemplo, si pulsar un día en el calendario requiere que se muestre un texto en una Activity y que un Service descargue un archivo en segundo plano.

El uso del enfoque anterior requerirá que Activity proporcione un EventHandler al componente, controle el evento y luego lo pase al Service . En cambio, hacer que el componente transmita un Intent y que tanto la Activity como el Service lo acepten a través de sus propios BroadcastReceivers no solo facilita la vida, sino que también ayuda a desacoplar la Activity y el Service en cuestión.

Conclusión

¡Contempla el asombroso poder de la personalización de Android!
Pío

Entonces, así es como crea su propio componente personalizado en unos simples pasos:

  • Crea el diseño XML y dale estilo para que se adapte a tus necesidades.
  • Derive su clase de componente del componente principal apropiado, de acuerdo con su diseño XML.
  • Agregue la lógica empresarial de su componente.
  • Utilice atributos para permitir que los usuarios modifiquen el comportamiento del componente.
  • Para facilitar el uso del componente en el diseñador de UI, use la función isInEditMode() de Android.

En este artículo, creamos una vista de calendario como ejemplo, principalmente porque falta la vista de calendario de acciones, en muchos sentidos. Pero, de ninguna manera está limitado en cuanto a qué tipo de componentes puede crear. Puedes usar la misma técnica para crear cualquier cosa que necesites, ¡el cielo es el límite!

¡Gracias por leer esta guía, le deseo la mejor de las suertes en sus esfuerzos de codificación!