Diseño Android: Tema claro y oscuro con Material Components

android

Sin duda alguna, los temas oscuros están pegando fuerte en el mundo del software en general, y en el de las aplicaciones móviles en particular. Aunque muchas aplicaciones ya utilizaban este tipo de diseño desde hace años como Spotify, a nivel de sistema operativo Apple abrió camino en 2018 con el lanzamiento de Mojave y le han seguido iOS (13), Android (10) y Windows.

Independientemente de los gustos personales, lo cierto es que el elevado contraste del modo oscuro hace más visible el contenido mostrado por la aplicación en condiciones de poca luminosidad por lo que pantalla necesita menos brillo lo que parece causar una menor fatiga visual. Este menor brillo también permite reducir el consumo de batería en los dispositivos portátiles. De hecho, en modo de ahorro de energía Android 10+ activa automáticamente el modo oscuro.

android ejemplos tema oscuro

El modo oscuro de Android 10, disponible también en Android 9 en las opciones del desarrollador, aplica automáticamente un estilo oscuro a todas las apps y hace un trabajo realmente bueno adaptando los colores, aunque he notado pequeñas diferencias en los colores aplicados por Android puro en el emulador y por MIUI 12 de Xiaomi.

Este comportamiento se puede configurar con la propiedad forceDarkAllowed que definiremos en el tema principal de la aplicación. Por defecto, su valor es true si nuestro tema hereda de un tema claro, y false si lo hacemos de un tema oscuro o de tipo DayNight que, como veremos en breve, ya aplica a los widgets de nuestra app un tema claro u oscuro según se solicite. Por tanto, si sólo tenemos un tema claro y no queremos que Android pueda forzar un tema oscuro porque, por ejemplo, hemos comprobado que queda mal, pondremos forceDarkAllowed a false.

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
   <item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
   <item name="colorPrimary">@color/color_primary</item>
...

Si queremos ofrecer la mejor experiencia posible al usuario generalmente tendremos que diseñar un tema oscuro para nuestra app, suponiendo, claro está, que queramos proporcionar tanto un tema claro como otro oscuro tal y como ya están haciendo las aplicaciones de referencia. Nosotros no vamos a ser menos y con la inestimable ayuda de los temas y widgets de Material Components for Android en este tutorial vamos a ver los fundamentos necesarios para ponernos manos a la obra. Lo haremos desde un punto de vista meramente técnico, esto es, en lo que respecta al programador y no al diseño gráfico.

App de ejemplo con tema DayNight

Vamos a hacer una app muy sencilla con la que poder “jugar”. Tendrá una única pantalla con una ActionBar implementada con una Toolbar, un campo de entrada de texto de tipo TextInputLayout, un Switch y una BottomAppBar con un FAB (Floating Action Buttom). Todos estos componentes se van presentar dentro de un ConstraintLayout.

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/text_input_layout_email"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/activity_horizontal_margin"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:layout_marginEnd="@dimen/activity_horizontal_margin"
        android:hint="@string/field_email"
        app:layout_constraintTop_toBottomOf="@+id/toolbar">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/editTextEmail"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress" />

    </com.google.android.material.textfield.TextInputLayout>


    <com.google.android.material.switchmaterial.SwitchMaterial
        android:id="@+id/switch_notifications"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:text="@string/switch_notification"
        app:layout_constraintEnd_toEndOf="@+id/text_input_layout_email"
        app:layout_constraintStart_toStartOf="@+id/text_input_layout_email"
        app:layout_constraintTop_toBottomOf="@+id/text_input_layout_email" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:onClick="validate"
        android:text="@string/button_validate"
        app:layout_constraintEnd_toEndOf="@+id/text_input_layout_email"
        app:layout_constraintStart_toStartOf="@+id/text_input_layout_email"
        app:layout_constraintTop_toBottomOf="@+id/switch_notifications" />

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/coordinator"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.google.android.material.bottomappbar.BottomAppBar
            android:id="@+id/bottomBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            app:fabAlignmentMode="center"
            app:menu="@menu/bottom_app_bar_menu"
            app:navigationIcon="@drawable/ic_menu">

        </com.google.android.material.bottomappbar.BottomAppBar>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_anchor="@id/bottomBar"
            android:contentDescription="@string/fab_add"
            app:srcCompat="@drawable/ic_add" />


    </androidx.coordinatorlayout.widget.CoordinatorLayout>


</androidx.constraintlayout.widget.ConstraintLayout>

En la Activity correspondiente se configura el layout, la Actionbar, el menú, y la lógica del botón. Su código no es relevante para este tutorial.

package com.danielme.android.dark;

import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;

import com.google.android.material.textfield.TextInputEditText;
import com.google.android.material.textfield.TextInputLayout;

public class MainActivity extends AppCompatActivity {

  private TextInputEditText editTextEmail;
  private TextInputLayout textInputEmail;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    editTextEmail = findViewById(R.id.editTextEmail);
    textInputEmail = findViewById(R.id.text_input_layout_email);
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.toolbar_menu, menu);
    return super.onCreateOptionsMenu(menu);
  }

  public void validate(View view) {
    if (TextUtils.isEmpty(editTextEmail.getText())) {
      textInputEmail.setError(getString(R.string.field_mandatory));
      textInputEmail.setErrorEnabled(true);
    } else {
      textInputEmail.setError(null);
      textInputEmail.setErrorEnabled(false);
    }

    clearFocus();
  }


  private void clearFocus() {
    View view = this.getCurrentFocus();
    if (view instanceof EditText) {
      InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context
              .INPUT_METHOD_SERVICE);
      inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
      view.clearFocus();
    }
  }

}

Y ahora viene lo más interesante: la configuración del tema principal de la app. Vamos a heredar del tema Theme.MaterialComponents.DayNight.NoActionBar ya que con los temas DayNight se seleccionará automáticamente como tema base de la app un tema de Material Components de tipo Light o Dark según ordene Android o bien configuremos nosotros en la app. Esto último lo veremos con detalle en la segunda parte del tutorial.

<resources>

    <style name="AppTheme.LightDark" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_900</item>
        <item name="colorSecondary">@color/red_500</item>
        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>        
        <item name="colorOnPrimary">@color/white_50</item>
        <item name="colorOnSecondary">@color/white_50</item>
        <item name="colorError">@color/red_300</item>
        <item name="bottomAppBarStyle">@style/Widget.MaterialComponents.BottomAppBar.PrimarySurface</item>
        <!-- toolbar -->
        <item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.PrimarySurface</item>
        <item name="actionOverflowButtonStyle">@style/ToolbarStyle.Overflow</item>
        <item name="toolbarNavigationButtonStyle">@style/Toolbar.Button.Navigation.Tinted</item>
        <!-- menús -->
        <item name="popupTheme">@style/Theme.MaterialComponents.DayNight</item>
    </style>

    <style name="ToolbarStyle.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
        <item name="android:tint">@color/white_50</item>
    </style>

    <style name="Toolbar.Button.Navigation.Tinted" parent="Widget.AppCompat.Toolbar.Button.Navigation">
        <item name="tint">@color/white_50</item>
    </style>

</resources>

Indicaremos en el manifiesto de la app que AppTheme.LightDark será el tema base.

    <application
        android:name=".LightDarkApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme.LightDark">

Para el tema se han personalizado algunos de los 12 tipos de colores definimos actualmente por Material Design y utilizados por Material Components y que además modifican y amplían los colores originalmente disponibles en los estilos de tipo AppCompat. Los más relevantes son los siguientes.

  • primaryColor: es el color principal de nuestra marca o “branding” y por tanto el distintivo de la app.
  • primaryVariant: versión del color principal generalmente más oscura.
  • colorSurface: color aplicado a diversas superficies (una especie área con cierta elevación sobre el fondo) y elementos tales como los menú inferiores Bottom Sheet o las tarjetas. Con los temas de tipo DayNight ya toma por defecto un color claro u oscuro según corresponda.
  • colorSecondary: equivale al antiguo colorAccent de Material Design y es un color complementario al primary que se utiliza para destacar o “acentuar” ciertos elementos.
  • colorOnSecondary: es el color de los elementos que aparecen sobre los widgets coloreados con colorSecondary. Todos los colores que tienen en su nombre “on” tienen este comportamiento (colorOnPrimary, colorOnSurface…).
  • colorError: no necesita descripción, pues es lo que parece. En el ejemplo se aplicará al mensaje de error asociado a la entrada de texto.

Los colores los definimos en el fichero /res/values/colors.xml (realmente el nombre da igual) siguiendo una estrategia de nombrado semántico de tal modo que podamos identificar por su nombre claramente cada color y sus posibles variaciones. Esto facilitará enormemente la definición en la app de temas duales, pues la principal diferencia entre el tema claro y el oscuro será casi exclusivamente la gama de colores aplicada en cada caso.

Para conseguirlo, vamos a seguir la praxis que presenta Google en sus apps de ejemplos y tutoriales para la definición de los colores consistente en nombrar el color que consideremos como “base” o “puro” con el sufijo 500. Esto es, el rojo que elijamos se llamará red_500. A partir de ahí, creamos una paleta de rojos de tal modo que el número del sufijo aumenta a medida que oscurecemos el color base, y disminuye si hacemos lo contrario y nos movemos hacia tonos menos saturados que además son los que se suelen utilizar en los temas oscuros. Esta categorización de los colores es fácil de entender viendo la siguiente imagen que he copiado de la herramienta online gratuita Color Tool.

material paleta colores

Para generar la paleta completa de un color, recomiendo utilizar una herramienta online como esta. En cualquier caso, no tenemos que incluir en nuestra app toda la paleta sino los colores que vayamos a usar.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <item name="purple_500" type="color">#9a28a1</item>
    <item name="purple_900" type="color">#6c0f74</item>
    <item name="purple_300" type="color">#b869bd</item>

    <item name="red_500" type="color">#af1541</item>
    <item name="red_300" type="color">#c75b7a</item>

    <color name="white_50">#ffffff</color>

    <color name="black_900">#000000</color>

    <color name="grey_900">#252525</color>

    <color name="cyan_500">#00bcd4</color>

</resources>

Ahora ejecutemos el proyecto.

Tenemos que

  • La Toolbar tiene colorPrimary porque el tema Widget.MaterialComponents.BottomAppBar.PrimarySurface aplica colorPrimary en modo claro y colorSurface en modo oscuro. Esta configuración se ha establecido para todas las toolbar de la app (propiedad toolbarStyle). Otras opciones de nombre bastante descriptivo son Widget.MaterialComponents.Toolbar.Primary y Widget.MaterialComponents.Toolbar.Surface. Para que los iconos del menú y el botón de navegación hacia atrás se muestren en blanco he utilizado las propiedades actionOverflowButtonStyle y toolbarNavigationButtonStyle.
  • El botón y el campo de texto se colorean de forma automática con colorPrimary. El texto del botón toma el valor colorOnPrimary
  • El FAB es de color secundario y su icono colorOnSecondary
  • El switch no seleccionado usa colorSurface, y seleccionado colorSecondary.
  • Para la BottomBar se aplica el tema @style/Widget.MaterialComponents.BottomAppBar.PrimarySurface equivalente al que hemos visto para la Toolbar. Nuevamente se vuelve a aplicar el mismo estilo por defecto para todas las BottomBar de la app (propiedad bottomAppBarStyle del tema).
  • Con el estilo Theme.MaterialComponents.DayNight el menú “overflow” se muestra con colorSurface.

Configurar el tema oscuro

¿Y si forzamos el modo oscuro o el ahorro de batería en Android 10+?

El resultado tiene buena pinta y es el esperado para el tema y colores que hemos creado. La toolbar, su menú y BottomBar ahora toman colorSurface, que no hemos personalizado, y que en el caso de la BottomBar se muestra en un tono más claro, mientras el campo de texto y el botón siguen con colorPrimary. Asimismo, el fondo de la aplicación es ahora negro (viene dado por colorBackground).

Tenemos que adaptar, al menos, el color de la status bar. Por fortuna podemos definir recursos, entendiendo como tales colores, estilos, drawable, etc, específicos sólo para el modo oscuro usando el cuantificador night en el nombre de las carpetas que los contienen (values-night, drawables-night, etc). Estos recursos específicos sobrescriben a sus equivalentes por defecto, esto es, los que se aplican en el tema claro y se encuentran en las carpetas sin el cuantificador night. De este modo, es relativamente sencillo afinar nuestro tema oscuro.

Esto se puede hacer de varias maneras. La estrategia que muestran los videos y ejemplos oficiales de Google consiste básicamente en separar el tema principal de la app en tres: un tema base común a todos los temas y los específicos para el tema claro y el oscuro.

Definamos en el fichero de temas “por defecto” (/res/values), tanto los temas y estilos generales como los específicos para el modo light.

<resources>

    <!-- Tema base con estilos comunes para light y dark. El tema de cada tipo debe heredar de este-->
    <style name="Base.AppTheme.LightDark" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
        <item name="colorOnPrimary">@color/white_50</item>
        <item name="colorOnSecondary">@color/white_50</item>
        <item name="colorError">@color/red_300</item>
        <item name="bottomAppBarStyle">@style/Widget.MaterialComponents.BottomAppBar.PrimarySurface</item>
        <!-- toolbar -->
        <item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.PrimarySurface</item>
        <item name="actionOverflowButtonStyle">@style/ToolbarStyle.Overflow</item>
        <item name="toolbarNavigationButtonStyle">@style/Toolbar.Button.Navigation.Tinted</item>
        <!-- menús -->
        <item name="popupTheme">@style/Theme.MaterialComponents.DayNight</item>
    </style>

    <style name="ToolbarStyle.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
        <item name="android:tint">@color/white_50</item>
    </style>

    <style name="Toolbar.Button.Navigation.Tinted" parent="Widget.AppCompat.Toolbar.Button.Navigation">
        <item name="tint">@color/white_50</item>
    </style>

    <!-- Configuración específica para light-->
    <style name="AppTheme.LightDark" parent="Base.AppTheme.LightDark">
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorPrimaryVariant">@color/purple_900</item>
        <item name="colorSecondary">@color/red_500</item>
        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
    </style>

</resources>

Y ahora creamos la versión dark en el fichero para night. Lo que hacemos es sobrescribir todos los temas y estilos que queramos utilizando el mismo nombre y que en nuestro ejemplo sólo es uno (AppTheme.LightDark). Insisto, esto que hacemos para el tema puede hacerse de forma análoga para colores, imágenes, drawables…cualquier elemento en las carpetas dentro /res.

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- configuración específica para dark -->
    <style name="AppTheme.LightDark" parent="Base.AppTheme.LightDark">
        <item name="colorPrimary">@color/purple_300</item>
        <item name="colorPrimaryVariant">@color/purple_500</item>
        <item name="colorSecondary">@color/cyan_500</item>
        <item name="colorSurface">@color/grey_900</item>
        <item name="android:statusBarColor">@color/black_900</item>
    </style>

</resources>

Podemos ver cómo se aplica el nuevo colorPrimary (campo de texto, botón) y colorSecondary (el FAB y el switch). También se ha personalizado colorSurface (Toolbar, BottomBar y menú).

android tema claro y oscuro demo

El siguiente video muestra el ejemplo completo incluyendo la selección de temas de la segunda parte del tutorial.

Código de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .