40

背景

在最近的 Android 版本中,自 Android 8.1 以来,操作系统对主题的支持越来越多。更具体地说是黑暗主题。

问题

尽管从用户的角度来看有很多关于暗模式的讨论,但几乎没有为开发人员编写的内容。

我发现了什么

从 Android 8.1 开始,Google 提供了某种深色主题。如果用户选择深色壁纸,操作系统的某些 UI 组件会变黑(此处的文章)。

此外,如果您开发了一个动态壁纸应用程序,您可以告诉操作系统它有哪些颜色(3 种颜色),这也会影响操作系统的颜色(至少在基于 Vanilla 的 ROM 和 Google 设备上)。这就是为什么我什至制作了一个应用程序,它可以让你拥有任何壁纸,同时仍然可以选择颜色(这里)。这是通过调用notifyColorsChanged然后使用 onComputeColors 提供它们来完成的

从 Android 9.0 开始,现在可以选择要拥有的主题:浅色、深色或自动(基于壁纸):

在此处输入图像描述

而现在在临近的 Android Q 上,似乎又走得更远了,但还不清楚到什么程度。不知何故,一个名为“Smart Launcher”的启动器已经骑在它上面,提供直接使用主题本身(这里的文章)。因此,如果您启用暗模式(手动,如此所写),您将获得应用程序的设置屏幕,如下所示:

在此处输入图像描述

到目前为止,我唯一发现的是上述文章,并且我正在关注此类主题。

我也知道如何请求操作系统使用动态壁纸更改颜色,但这似乎在 Android Q 上发生了变化,至少根据我在尝试时看到的情况(我认为它更多基于时间,但不确定)。

问题

  1. 是否有 API 可以获取操作系统设置使用的颜色?

  2. 是否有任何类型的 API 来获取操作系统的主题?来自哪个版本?

  3. 新的 API 是否也与夜间模式有关?这些如何协同工作?

  4. 应用程序是否有一个很好的 API 来处理所选主题?这意味着如果操作系统处于某个主题中,那么当前的应用程序也会如此吗?

4

6 回答 6

40

谷歌刚刚在 I/O 2019 结束时发布了关于黑暗主题的文档,请点击此处

为了管理黑暗主题,您必须首先使用最新版本的 Material Components 库:"com.google.android.material:material:1.1.0-alpha06".

根据系统主题更改应用程序主题

应用程序根据系统切换到深色主题,只需要一个主题。为此,主题必须有 Theme.MaterialComponents.DayNight 作为父级。

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

确定当前系统主题

要知道系统当前是否处于黑暗主题,您可以实现以下代码:

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

收到主题更改通知

我认为不可能实现回调以在主题更改时收到通知,但这不是问题。实际上,当系统更改主题时,活动会自动重新创建。将前面的代码放在活动的开头就足够了。

它适用于哪个版本的 Android SDK?

我无法让它在带有 Android SDK 版本 28 的 Android Pie 上工作。所以我假设这只适用于 SDK 的下一个版本,它将与 Q 版本 29 一起启动。

结果

结果

于 2019-05-08T08:40:00.790 回答
27

一个更简单的 Kotlin 方法来解决 Charles Annic 的答案:

fun Context.isDarkThemeOn(): Boolean {
    return resources.configuration.uiMode and 
            Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
于 2020-03-19T16:23:25.270 回答
10

好的,所以我知道这通常是如何工作的,在最新版本的 Android (Q) 和之前的版本上。

似乎当操作系统创建 WallpaperColors 时,它也会生成颜色提示。在函数WallpaperColors.fromBitmap中,有一个调用int hints = calculateDarkHints(bitmap);,这是代码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;
}

然后搜索getColorHints那个WallpaperColors.java,我在以下位置找到updateTheme了函数StatusBar.java

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

这仅适用于 Android 8.1 ,因为那时主题仅基于壁纸的颜色。在 Android 9.0 上,用户可以在不连接壁纸的情况下进行设置。

根据我在 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
}

我已经设置了各种可能的值,因为在大多数情况下,没有任何保证。

于 2019-06-09T15:36:00.107 回答
5

我认为谷歌基于电池级别在 Android Q 中应用深色和浅色主题。

也许是昼夜主题

然后,您需要在您的应用中启用该功能。您可以通过调用 AppCompatDelegate.setDefaultNightMode() 来做到这一点,它采用以下值之一:

  • MODE_NIGHT_NO。始终使用白天(浅色)主题。
  • MODE_NIGHT_YES。始终使用夜间(黑暗)主题。
  • MODE_NIGHT_FOLLOW_SYSTEM(默认)。此设置遵循系统设置,在 Android Pie 及更高版本上是系统设置(更多内容见下文)。
  • MODE_NIGHT_AUTO_BATTERY。当设备启用“省电模式”功能时变为暗,否则变为亮。✨v1.1.0-alpha03 中的新功能。
  • MODE_NIGHT_AUTO_TIME & MODE_NIGHT_AUTO。根据一天中的时间在白天/黑夜之间变化。
于 2019-04-21T22:15:28.590 回答
1

我想添加到 Vitor Hugo Schwaab 的答案中,您可以进一步分解代码并使用isNightModeActive.

resources.configuration.isNightModeActive

资源
配置
是NightModeActive

于 2021-07-29T05:50:21.390 回答
0

我根据所有可能来源的可用信息制作了这段代码,它对我有用!!!希望它也对其他人有所帮助。我为其创建此代码的应用程序适用于 API 级别 21(Android Lollipop 5.0),因此请相应地使用它。

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);
    }
}
于 2022-02-22T07:03:00.490 回答