是否有 API 可以检测操作系统正在使用哪个主题 - 深色或浅色(或其他)?

Posted

技术标签:

【中文标题】是否有 API 可以检测操作系统正在使用哪个主题 - 深色或浅色(或其他)?【英文标题】:Is there an API to detect which theme the OS is using - dark or light (or other)? 【发布时间】:2019-09-11 05:04:46 【问题描述】:

背景

在最近的 android 版本上,从 Android 8.1 开始,操作系统对主题的支持越来越多。更具体地说是深色主题。

问题

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

我发现了什么

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

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

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

现在在 Android Q 上,似乎更进一步,但还不清楚到什么程度。不知何故,一个名为“Smart Launcher”的启动器骑在上面,提供直接使用该主题(文章here)。因此,如果您启用暗模式(手动,如 here 所写),您将获得应用程序的设置屏幕:

到目前为止,我发现的唯一的东西就是上面的文章,而且我正在关注这种主题。

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

问题

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

    是否有任何 API 来获取操作系统的主题?从哪个版本开始?

    新 API 是否也与夜间模式有关?它们如何协同工作?

    是否有一个不错的 API 可供应用程序处理所选主题?这意味着如果操作系统处于某个主题中,那么当前的应用程序也是如此吗?

【问题讨论】:

【参考方案1】:

Google 刚刚在 I/O 2019 结束时发布了关于黑暗主题的文档,here。

为了管理黑暗主题,你必须首先使用最新版本的材料组件库:"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 一起启动。

结果

【讨论】:

为什么叫“昼夜”?他们是否返回了根据时间更改主题的功能?你试过这个 API 吗?有用吗? 我不知道名字,我也觉得很奇怪。我没有在 Android Q 设置中找到随时间更改主题的设置,但这与您在应用程序中的实现无关。如果 Q 中存在此功能,您的应用程序将使用我刚刚提供的解决方案来适应它。我已经在我目前正在开发的应用程序中尝试了这个 API,它运行良好! 编辑:我刚刚添加了一个获得结果的示例。 我是从带有 Android Q beta 3 的 Android Studio 模拟器中获取的。自从 Android Q 的第一个 beta 版本以来,可以修改系统的一些美学方面,例如瓷砖的形状.见这里:androidpolice.com/2019/04/04/… 在我之前提供的文档中,Google 提供了 AppCompat DayNight 文档的以下链接:medium.com/androiddevelopers/…。据说每次更改都会重新创建活动。我对其进行了测试,正在重新创建活动。实际上,没有动画,因为在更改主题时过渡可能会被系统覆盖。最后,是的,这就是我正在使用的。你试过我的解决方案吗?这不适合你吗?【参考方案2】:

Charles Annic 回答的更简单的 Kotlin 方法:

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

【讨论】:

Kotlin 是最好的! @ArtemMostyaev C# 更短 Resources.Configuration.UiMode.HasFlag(UiMode.NightYes) :P【参考方案3】:

好的,所以我知道了这通常是如何工作的,无论是在最新版本的 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;

然后搜索WallpaperColors.java有的getColorHints,在StatusBar.java中找到了updateTheme函数:

    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

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

【讨论】:

MAX_DARK_AREA、DARK_PIXEL_LUMINANCE、BRIGHT_IMAGE_MEAN_LUMINANCE、HINT_SUPPORTS_DARK_TEXT HINT_SUPPORTS_DARK_THEME 值是多少? @YusufEkaSayogana 我没有复制所有代码。这不是我的,也无关紧要。如果您希望可以像我一样在线阅读整个代码。它也在 Android 的源代码中,可在 SDK 上找到。搜索“calculateDarkHints”的代码。我认为它也改变了 Android 版本。 为什么这个答案不完整而被接受 @eriknyk 缺少什么? @androiddeveloper 在 calculateDarkHints() 方法中有许多已知常量,例如:MAX_DARK_AREA、DARK_PIXEL_LUMINANCE、BRIGHT_IMAGE_MEAN_LUMINANCE、DARK_THEME_MEAN_LUMINANCE。 getIsOsDarkTheme() 中其他未知的道具是:DarkThemeCheckResult.UNKNOWN、DarkThemeCheckResult.UNKNOWN_MAYBE_DARK、DarkThemeCheckResult.UNKNOWN_MAYBE_LIGHT、DarkThemeCheckResult.MOST_PROBABLY_DARK,否则为 DarkThemeCheckResult.MOST_PROBABLY_LIGHT。在这种情况下,我们可以添加它们,但不确定为什么我们需要所有这些变体,但在第一种情况下,我不知道。【参考方案4】:

我认为 Google 在 Android Q 中应用深色和浅色主题是基于电池电量。

也许DayNight theme?

然后您需要在您的应用中启用该功能。你这样做 调用 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。根据一天中的时间在昼夜之间变化。

【讨论】:

获取颜色怎么样?可能吗?为什么这篇文章会提到 Android Q?它有什么特别之处?我已经测试了代码以检查我处于哪种模式,即使现在是晚上,它也会显示“夜间模式未激活,我们处于白天”。在安卓 9 上测试。怎么会 ?或者可能是针对 Android Q 的? @androiddeveloper 这篇文章对 Android Q (2016) 只字未提。它只有助于让您根据设备设置更改主题(如智能启动器的行为)。 明天我会亲自尝试多个 API 版本。 实际上我的设备有些奇怪。操作系统的设置让我可以选择使用哪个主题,但所有这些设置都让它保持在深色主题上,包括当我选择浅色主题时。我记得我已经报告过这个问题,但我认为它已经解决了......无论如何,有没有办法同时检查颜色,而不仅仅是暗与亮? 我正在对其进行测试,在我的 Android PIE 模拟器上,昼夜主题不起作用(甚至每次都试图强制使用它)并且 Android Studio 不允许我下载 AndroidQ。我要试试安卓奥利奥...【参考方案5】:

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

resources.configuration.isNightModeActive

resources configuration isNightModeActive

【讨论】:

请注意,此方法仅适用于 API 级别 30 及更高级别。【参考方案6】:

我根据所有可能来源的可用信息制作了这段代码,它对我有用!!!希望它也对其他人有所帮助。我为其创建此代码的应用程序适用于 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);
    

【讨论】:

以上是关于是否有 API 可以检测操作系统正在使用哪个主题 - 深色或浅色(或其他)?的主要内容,如果未能解决你的问题,请参考以下文章

是否有 API 来检测 iOS 上的 CPU 功能?

是否有任何私有 api 可以在 iphone 上监控键盘访问(哪个应用程序正在使用键盘,它的访问时间等)?

是否可以检测到哪个管道抛出了 SIGPIPE?

检测按下了哪个按钮

使用“词袋”方法进行主题检测的朴素贝叶斯

使用“词袋”方法进行主题检测的朴素贝叶斯