¿Existe una API para detectar qué tema está utilizando el sistema operativo: oscuro o claro (u otro)?

11 minutos de lectura

Fondo

En las versiones recientes de Android, desde Android 8.1, el sistema operativo obtuvo cada vez más soporte para temas. Más específicamente tema oscuro.

El problema

Aunque se habla mucho sobre el modo oscuro desde el punto de vista de los usuarios, no hay casi nada escrito para los desarrolladores.

lo que he encontrado

A partir de Android 8.1, Google proporcionó algún tipo de tema oscuro. Si el usuario elige tener un fondo de pantalla oscuro, algunos componentes de la interfaz de usuario del sistema operativo se volverán negros (artículo aquí).

Además, si desarrolló una aplicación de fondo de pantalla en vivo, podría decirle al sistema operativo qué colores tiene (3 tipos de colores), lo que también afectó los colores del sistema operativo (al menos en las ROM basadas en Vanilla y en los dispositivos de Google). Es por eso que incluso creé una aplicación que te permite tener cualquier fondo de pantalla sin dejar de elegir los colores (aquí). Esto se hace llamando notificarColorsChanged y luego proporcionarlos usando onComputeColors

A partir de Android 9.0, ahora es posible elegir qué tema tener: claro, oscuro o automático (según el fondo de pantalla):

ingrese la descripción de la imagen aquí

Y ahora en el cercano Android Q, parece que fue más allá, aunque todavía no está claro hasta qué punto. De alguna manera, un lanzador llamado “Smart Launcher” se ha instalado en él, ofreciendo usar el tema directamente sobre sí mismo (artículo aquí). Entonces, si habilita el modo oscuro (manualmente, como está escrito aquí), obtendrá la pantalla de configuración de la aplicación como tal:

ingrese la descripción de la imagen aquí

Lo único que he encontrado hasta ahora son los artículos anteriores y que estoy siguiendo este tipo de tema.

También sé cómo solicitar que el sistema operativo cambie de color usando el fondo de pantalla en vivo, pero esto parece estar cambiando en Android Q, al menos según lo que he visto al probarlo (creo que se basa más en la hora del día , pero no estoy seguro).

Las preguntas

  1. ¿Hay una API para obtener qué colores está configurado para usar el sistema operativo?

  2. ¿Hay algún tipo de API para obtener el tema del sistema operativo? ¿De qué versión?

  3. ¿La nueva API también está relacionada con el modo nocturno? ¿Cómo funcionan esos juntos?

  4. ¿Existe una buena API para que las aplicaciones manejen el tema elegido? Lo que significa que si el sistema operativo está en cierto tema, ¿también lo estará la aplicación actual?

Avatar de usuario de Charles Annic
Carlos Annic

Google acaba de publicar la documentación sobre el tema oscuro a finales de I/O 2019, aquí.

Para administrar el tema oscuro, primero debe usar la última versión de la biblioteca Material Components: "com.google.android.material:material:1.1.0-alpha06".

Cambiar el tema de la aplicación según el tema del sistema

Para que la aplicación cambie al tema oscuro según el sistema, solo se requiere un tema. Para hacer esto, el tema debe tener Theme.MaterialComponents.DayNight como padre.

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
    ...
</style>

Determinar el tema del sistema actual

Para saber si el sistema está actualmente en tema oscuro o no, puede implementar el siguiente código:

switch (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) {
    case Configuration.UI_MODE_NIGHT_YES:
        …
        break;
    case Configuration.UI_MODE_NIGHT_NO:
        …
        break; 
}

Ser notificado de un cambio en el tema

No creo que sea posible implementar una devolución de llamada para recibir una notificación cada vez que cambie el tema, pero eso no es un problema. De hecho, cuando el sistema cambia de tema, la actividad se recrea automáticamente. Entonces es suficiente colocar el código anterior al inicio de la actividad.

¿Desde qué versión del SDK de Android funciona?

No pude hacer que esto funcionara en Android Pie con la versión 28 del SDK de Android. Así que supongo que esto solo funciona a partir de la próxima versión del SDK, que se lanzará con Q, versión 29.

Resultado

resultado

  • ¿Por qué se llama “DíaNoche”? ¿Han vuelto la característica que cambia el tema según el tiempo? ¿Has probado esta API? Funciona ?

    – desarrollador de Android

    8 mayo 2019 a las 17:37


  • No tengo ni idea del nombre, a mí también me parece raro. No encontré la configuración para cambiar el tema con el tiempo en la configuración de Android Q, pero esto es independiente de su implementación en su aplicación. Si esta característica está presente en Q, su aplicación se adaptará a ella con la solución que acabo de dar. ¡He probado esta API en la aplicación que estoy desarrollando actualmente y funciona perfectamente!

    – Charles Annic

    8 mayo 2019 a las 18:22

  • Editar: acabo de agregar un ejemplo del resultado obtenido.

    – Charles Annic

    8 mayo 2019 a las 18:30

  • Lo tomé del emulador de Android Studio con Android Q beta 3. Desde la primera beta de Android Q, es posible modificar algunos aspectos estéticos del sistema, como la forma de los mosaicos. Mira aquí: androidpolice.com/2019/04/04/…

    – Charles Annic

    9 de mayo de 2019 a las 7:10

  • En la documentación que proporcioné anteriormente, Google proporciona el siguiente enlace para la documentación de AppCompat DayNight: medium.com/androiddevelopers/…. Aquí es donde se dice que la actividad se recrea con cada cambio. Lo probé, se está recreando la actividad. De hecho, no hay animación porque es probable que el sistema anule la transición al cambiar de tema. Y finalmente, sí, eso es lo que estoy usando. ¿Has probado mi solución? ¿No te funciona eso?

    – Charles Annic

    9 de mayo de 2019 a las 7:10


Avatar de usuario de Vitor Hugo Schwaab
Vítor Hugo Schwaab

Un enfoque más simple de Kotlin para la respuesta de Charles Annic:

fun Context.isDarkThemeOn(): Boolean {
    return resources.configuration.uiMode and 
            Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}

  • ¡Kotlin es el mejor!

    – Artem Mostyaev

    10 de febrero de 2021 a las 10:10

  • @ArtemMostyaev C# es aún más corto Resources.Configuration.UiMode.HasFlag(UiMode.NightYes) :PAG

    – Iván Mir

    12 mayo 2021 a las 23:36

avatar de usuario del desarrollador de Android
desarrollador de Android

Bien, pude saber cómo funciona esto normalmente, tanto en la versión más reciente de Android (Q) como en versiones anteriores.

Parece que cuando el sistema operativo crea WallpaperColors, también genera sugerencias de color. en la funcion WallpaperColors.fromBitmap hay una llamada a int hints = calculateDarkHints(bitmap); y este es el código de calculateDarkHints :

/**
 * Checks if image is bright and clean enough to support light text.
 *
 * @param source What to read.
 * @return Whether image supports dark text or not.
 */
private static int calculateDarkHints(Bitmap source) {
    if (source == null) {
        return 0;
    }

    int[] pixels = new int[source.getWidth() * source.getHeight()];
    double totalLuminance = 0;
    final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
    int darkPixels = 0;
    source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
            source.getWidth(), source.getHeight());

    // This bitmap was already resized to fit the maximum allowed area.
    // Let's just loop through the pixels, no sweat!
    float[] tmpHsl = new float[3];
    for (int i = 0; i < pixels.length; i++) {
        ColorUtils.colorToHSL(pixels[i], tmpHsl);
        final float luminance = tmpHsl[2];
        final int alpha = Color.alpha(pixels[i]);
        // Make sure we don't have a dark pixel mass that will
        // make text illegible.
        if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
            darkPixels++;
        }
        totalLuminance += luminance;
    }

    int hints = 0;
    double meanLuminance = totalLuminance / pixels.length;
    if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) {
        hints |= HINT_SUPPORTS_DARK_TEXT;
    }
    if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) {
        hints |= HINT_SUPPORTS_DARK_THEME;
    }

    return hints;
}

Entonces buscando getColorHints que el WallpaperColors.java tiene, he encontrado updateTheme función en StatusBar.java :

    WallpaperColors systemColors = mColorExtractor
            .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
    final boolean useDarkTheme = systemColors != null
            && (systemColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0;

Esto funcionaría solo en Android 8.1, porque entonces el tema se basaba solo en los colores del fondo de pantalla. En Android 9.0, el usuario puede configurarlo sin ninguna conexión con el fondo de pantalla.

Esto es lo que he hecho, según lo que he visto en Android:

enum class DarkThemeCheckResult {
    DEFAULT_BEFORE_THEMES, LIGHT, DARK, PROBABLY_DARK, PROBABLY_LIGHT, USER_CHOSEN
}

@JvmStatic
fun getIsOsDarkTheme(context: Context): DarkThemeCheckResult {
    when {
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.O -> return DarkThemeCheckResult.DEFAULT_BEFORE_THEMES
        Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> {
            val wallpaperManager = WallpaperManager.getInstance(context)
            val wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
                    ?: return DarkThemeCheckResult.UNKNOWN
            val primaryColor = wallpaperColors.primaryColor.toArgb()
            val secondaryColor = wallpaperColors.secondaryColor?.toArgb() ?: primaryColor
            val tertiaryColor = wallpaperColors.tertiaryColor?.toArgb() ?: secondaryColor
            val bitmap = generateBitmapFromColors(primaryColor, secondaryColor, tertiaryColor)
            val darkHints = calculateDarkHints(bitmap)
            //taken from StatusBar.java , in updateTheme :
            val HINT_SUPPORTS_DARK_THEME = 1 shl 1
            val useDarkTheme = darkHints and HINT_SUPPORTS_DARK_THEME != 0
            if (Build.VERSION.SDK_INT == VERSION_CODES.O_MR1)
                return if (useDarkTheme)
                    DarkThemeCheckResult.UNKNOWN_MAYBE_DARK
                else DarkThemeCheckResult.UNKNOWN_MAYBE_LIGHT
            return if (useDarkTheme)
                DarkThemeCheckResult.MOST_PROBABLY_DARK
            else DarkThemeCheckResult.MOST_PROBABLY_LIGHT
        }
        else -> {
            return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
                Configuration.UI_MODE_NIGHT_YES -> DarkThemeCheckResult.DARK
                Configuration.UI_MODE_NIGHT_NO -> DarkThemeCheckResult.LIGHT
                else -> DarkThemeCheckResult.MOST_PROBABLY_LIGHT
            }
        }
    }
}

fun generateBitmapFromColors(@ColorInt primaryColor: Int, @ColorInt secondaryColor: Int, @ColorInt tertiaryColor: Int): Bitmap {
    val colors = intArrayOf(primaryColor, secondaryColor, tertiaryColor)
    val imageSize = 6
    val bitmap = Bitmap.createBitmap(imageSize, 1, Bitmap.Config.ARGB_8888)
    for (i in 0 until imageSize / 2)
        bitmap.setPixel(i, 0, colors[0])
    for (i in imageSize / 2 until imageSize / 2 + imageSize / 3)
        bitmap.setPixel(i, 0, colors[1])
    for (i in imageSize / 2 + imageSize / 3 until imageSize)
        bitmap.setPixel(i, 0, colors[2])
    return bitmap
}

He establecido los distintos valores posibles, porque en la mayoría de esos casos nada está garantizado.

  • ¿Cuáles son los valores MAX_DARK_AREA,DARK_PIXEL_LUMINANCE, BRIGHT_IMAGE_MEAN_LUMINANCE, HINT_SUPPORTS_DARK_TEXT HINT_SUPPORTS_DARK_THEME?

    – Yusuf Eka Sayogana

    20 de abril de 2021 a las 9:01

  • @YusufEkaSayogana No copié todo el código. No es mío y es irrelevante. Si lo desea, puede leer el código completo en línea en algún lugar, tal como lo hice yo. También está en el código fuente de Android, disponible en el SDK. Busque el código de “calculateDarkHints”. Creo que también cambió con las versiones de Android.

    – desarrollador de Android

    20 de abril de 2021 a las 9:18

  • por qué se acepta esta respuesta ya que no está completa

    – erik.aortiz

    24 de febrero de 2022 a las 1:22

  • @eriknyk ¿Qué falta?

    – desarrollador de Android

    25 de febrero de 2022 a las 7:59

  • @androiddeveloper dentro de los métodos de computeDarkHints() hay muchas constantes conocidas como: MAX_DARK_AREA, DARK_PIXEL_LUMINANCE, BRIGHT_IMAGE_MEAN_LUMINANCE, DARK_THEME_MEAN_LUMINANCE. Y otros accesorios desconocidos en getIsOsDarkTheme() son: DarkThemeCheckResult.UNKNOWN, DarkThemeCheckResult.UNKNOWN_MAYBE_DARK, DarkThemeCheckResult.UNKNOWN_MAYBE_LIGHT, DarkThemeCheckResult.MOST_PROBABLE_DARK, de lo contrario DarkThemeCheckResult.MOST_PROBABLY_LIGHT. en este segundo caso, podemos simplemente agregarlos, sin embargo, no estoy seguro de por qué necesitamos todas esas variantes, pero en el primer caso no tengo idea.

    – erik.aortiz

    2 de marzo de 2022 a las 21:44


Creo que Google se basa en el nivel de batería para aplicar temas claros y oscuros en Android Q.

Tal vez Tema de día y noche?

Luego debe habilitar la función en su aplicación. Lo hace llamando a AppCompatDelegate.setDefaultNightMode(), que toma uno de los siguientes valores:

  • MODO_NOCHE_NO. Utilice siempre el tema de día (luz).
  • MODO_NOCHE_SI. Utilice siempre el tema nocturno (oscuro).
  • MODE_NIGHT_FOLLOW_SYSTEM (predeterminado). Esta configuración sigue la configuración del sistema, que en Android Pie y superior es una configuración del sistema (más sobre esto a continuación).
  • MODO_NOCHE_AUTO_BATERÍA. Cambia a oscuro cuando el dispositivo tiene habilitada la función ‘Ahorro de batería’; de lo contrario, se ilumina. ✨Nuevo en v1.1.0-alpha03.
  • MODE_NIGHT_AUTO_TIME & MODE_NIGHT_AUTO. Cambios entre día/noche en función de la hora del día.

Avatar de usuario de Shawn
Shawn

Quería agregar a la respuesta de Vitor Hugo Schwaab, podría desglosar el código aún más y usar isNightModeActive.

resources.configuration.isNightModeActive

recursos
configuración
esModoNocturnoActivo

  • Tenga en cuenta que este método solo está disponible en el nivel de API 30 y superior.

    –Anders Gustafsson

    12 oct 2021 a las 7:21

  • @AndersGustafsson esto es realmente malo

    – Udemir

    15/09/2022 a las 12:00

Avatar de usuario de Shubham Nanche
Shubham Nanché

¡Hice este código basado en la información disponible de todas las fuentes posibles y funcionó para mí! Espero que ayude a otros también. La aplicación para la que creé este código está diseñada para el nivel de API 21 (Android Lollipop 5.0), así que utilícela en consecuencia.

public class MainActivity extends AppCompatActivity{
    public final String[] themes = {"System Default","Light","Dark"};
    public static int checkedTheme;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loadAppTheme(); //always put this before setContentView();
        setContentView(R.layout.activity_main);
        //your other code
    }

    private void showChangeThemeAlertDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Change Theme");
        builder.setSingleChoiceItems(themes, checkedTheme, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                checkedTheme = which;
                switch (which) {
                    case 0:
                        setAppTheme(0);
                        break;
                    case 1:
                        setAppTheme(1);
                        break;
                    case 2:
                        setAppTheme(2);
                        break;
                }
                dialog.dismiss();
            }
        });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();

    }

    private void setAppTheme(int themeNo) {
        switch (themeNo){
            case 0:
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
                break;
            case 1:
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                break;
            case 2:
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                break;
        }
        SharedPreferences.Editor editor = getSharedPreferences("Themes",MODE_PRIVATE).edit();
        editor.putInt("ThemeNo",checkedTheme);
        editor.apply();
    }


    private void loadAppTheme() {
        SharedPreferences themePreference = getSharedPreferences("Themes",Activity.MODE_PRIVATE);
        checkedTheme = themePreference.getInt("ThemeNo",0);
        setAppTheme(checkedTheme);
    }
}

  • Tenga en cuenta que este método solo está disponible en el nivel de API 30 y superior.

    –Anders Gustafsson

    12 oct 2021 a las 7:21

  • @AndersGustafsson esto es realmente malo

    – Udemir

    15/09/2022 a las 12:00

¿Ha sido útil esta solución?