是否有 API 来检测 OS 使用的主题是深色还是浅色(或其他)?
Is there an API to detect which theme the OS is using - dark or light (or other)?
背景
在最近的 Android 版本中,自 Android 8.1 以来,OS 获得了越来越多的主题支持。更具体地说是深色主题。
问题
尽管从用户的角度来看有很多关于深色模式的讨论,但几乎没有为开发人员编写的内容。
我发现了什么
从 Android 8.1 开始,Google 提供了某种深色主题。如果用户选择深色墙纸,OS 的某些 UI 组件会变黑(文章 here)。
此外,如果您开发了动态壁纸应用程序,您可以告诉 OS 它有哪些颜色(3 种颜色),这也会影响 OS 颜色(至少在基于香草的 ROM 和 Google 设备)。这就是为什么我什至制作了一个应用程序,让你拥有任何壁纸,同时仍然能够选择颜色(here). This is done by calling notifyColorsChanged and then provide them using onComputeColors
从 Android 9.0 开始,现在可以选择要使用的主题:浅色、深色或自动(基于壁纸):
而现在近AndroidQ,好像更进一步了,具体到什么程度还不清楚。不知何故,一个名为 "Smart Launcher" 的启动器安装在它上面,提供直接使用主题(文章 here). So, if you enable dark mode (manually, as written here),您会看到应用程序的设置屏幕:
目前我只找到了上面的文章,我正在关注这种话题。
我也知道如何使用动态壁纸请求 OS 更改颜色,但这似乎在 Android Q 上发生了变化,至少根据我在尝试时看到的情况它(我认为它更多地基于一天中的时间,但不确定)。
问题
是否有 API 来获取 OS 设置使用的颜色?
有什么API可以得到OS的主题吗?来自哪个版本?
新的 API 是否也与夜间模式有关?它们如何协同工作?
是否有适合 API 的应用程序来处理所选主题?这意味着如果 OS 是某个主题,那么当前的应用程序也是?
我认为 Google 在 Android Q 中应用深色和浅色主题是基于电池电量。
You then need to enable the feature in your app. You do that by
calling AppCompatDelegate.setDefaultNightMode(), which takes one of
the follow values:
- MODE_NIGHT_NO. Always use the day (light) theme.
- MODE_NIGHT_YES. Always use the night (dark) theme.
- MODE_NIGHT_FOLLOW_SYSTEM (default). This setting follows the system’s setting, which on Android Pie and above is a system setting
(more on this below).
- MODE_NIGHT_AUTO_BATTERY. Changes to dark when the device has its ‘Battery Saver’ feature enabled, light otherwise. ✨New in
v1.1.0-alpha03.
- MODE_NIGHT_AUTO_TIME & MODE_NIGHT_AUTO. Changes between day/night based on the time of day.
Google 刚刚在 I/O 2019 年底发布了黑暗主题的文档,here.
为了管理深色主题,您必须首先使用最新版本的Material组件库:"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;
}
主题更改时收到通知
我认为不可能在主题更改时实现回调以得到通知,但这不是问题。实际上,当系统更改主题时,activity 会自动重新创建。将前面的代码放在 activity 的开头就足够了。
它适用于哪个版本的 Android SDK?
我无法在使用 Android SDK 版本 28 的 Android Pie 上使用它。所以我假设这只适用于下一版本的 SDK,它将与 Q 版本 29 一起发布。
结果
好的,所以我知道这通常是如何工作的,在最新版本的 Android (Q) 和之前。
似乎当 OS 创建 WallpaperColors 时,它也会生成 color-hints。在函数 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
}
我已经设置了各种可能的值,因为在大多数情况下,没有什么是可以保证的。
Charles Annic 回答的更简单的 Kotlin 方法:
fun Context.isDarkThemeOn(): Boolean {
return resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
我想补充 Vitor Hugo Schwaab 的答案,您可以进一步分解代码并使用 isNightModeActive
。
resources.configuration.isNightModeActive
我根据所有可能来源的可用信息制作了这段代码,它对我有用!!!希望它也能帮助别人。我为其创建此代码的应用程序适用于 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);
}
}
背景
在最近的 Android 版本中,自 Android 8.1 以来,OS 获得了越来越多的主题支持。更具体地说是深色主题。
问题
尽管从用户的角度来看有很多关于深色模式的讨论,但几乎没有为开发人员编写的内容。
我发现了什么
从 Android 8.1 开始,Google 提供了某种深色主题。如果用户选择深色墙纸,OS 的某些 UI 组件会变黑(文章 here)。
此外,如果您开发了动态壁纸应用程序,您可以告诉 OS 它有哪些颜色(3 种颜色),这也会影响 OS 颜色(至少在基于香草的 ROM 和 Google 设备)。这就是为什么我什至制作了一个应用程序,让你拥有任何壁纸,同时仍然能够选择颜色(here). This is done by calling notifyColorsChanged and then provide them using onComputeColors
从 Android 9.0 开始,现在可以选择要使用的主题:浅色、深色或自动(基于壁纸):
而现在近AndroidQ,好像更进一步了,具体到什么程度还不清楚。不知何故,一个名为 "Smart Launcher" 的启动器安装在它上面,提供直接使用主题(文章 here). So, if you enable dark mode (manually, as written here),您会看到应用程序的设置屏幕:
目前我只找到了上面的文章,我正在关注这种话题。
我也知道如何使用动态壁纸请求 OS 更改颜色,但这似乎在 Android Q 上发生了变化,至少根据我在尝试时看到的情况它(我认为它更多地基于一天中的时间,但不确定)。
问题
是否有 API 来获取 OS 设置使用的颜色?
有什么API可以得到OS的主题吗?来自哪个版本?
新的 API 是否也与夜间模式有关?它们如何协同工作?
是否有适合 API 的应用程序来处理所选主题?这意味着如果 OS 是某个主题,那么当前的应用程序也是?
我认为 Google 在 Android Q 中应用深色和浅色主题是基于电池电量。
You then need to enable the feature in your app. You do that by calling AppCompatDelegate.setDefaultNightMode(), which takes one of the follow values:
- MODE_NIGHT_NO. Always use the day (light) theme.
- MODE_NIGHT_YES. Always use the night (dark) theme.
- MODE_NIGHT_FOLLOW_SYSTEM (default). This setting follows the system’s setting, which on Android Pie and above is a system setting (more on this below).
- MODE_NIGHT_AUTO_BATTERY. Changes to dark when the device has its ‘Battery Saver’ feature enabled, light otherwise. ✨New in v1.1.0-alpha03.
- MODE_NIGHT_AUTO_TIME & MODE_NIGHT_AUTO. Changes between day/night based on the time of day.
Google 刚刚在 I/O 2019 年底发布了黑暗主题的文档,here.
为了管理深色主题,您必须首先使用最新版本的Material组件库:"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;
}
主题更改时收到通知
我认为不可能在主题更改时实现回调以得到通知,但这不是问题。实际上,当系统更改主题时,activity 会自动重新创建。将前面的代码放在 activity 的开头就足够了。
它适用于哪个版本的 Android SDK?
我无法在使用 Android SDK 版本 28 的 Android Pie 上使用它。所以我假设这只适用于下一版本的 SDK,它将与 Q 版本 29 一起发布。
结果
好的,所以我知道这通常是如何工作的,在最新版本的 Android (Q) 和之前。
似乎当 OS 创建 WallpaperColors 时,它也会生成 color-hints。在函数 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
}
我已经设置了各种可能的值,因为在大多数情况下,没有什么是可以保证的。
Charles Annic 回答的更简单的 Kotlin 方法:
fun Context.isDarkThemeOn(): Boolean {
return resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}
我想补充 Vitor Hugo Schwaab 的答案,您可以进一步分解代码并使用 isNightModeActive
。
resources.configuration.isNightModeActive
我根据所有可能来源的可用信息制作了这段代码,它对我有用!!!希望它也能帮助别人。我为其创建此代码的应用程序适用于 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);
}
}