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