安卓应用的白天/黑夜主题

13

我有一个应用程序需要实现白天/夜晚主题。不幸的是,仅使用样式没有简单的制作主题的方法,我需要能够更新:布局背景、按钮选择器、文本颜色、文本大小、图像、图标、动画。

从我所看到的,我有两个选择:

  1. 为白天/夜晚创建不同的xml布局文件,例如home_day.xml / home_night.xml。应用中大约有30个屏幕,所以最终将有60个xml布局文件。在activity/fragment onCreate上,根据当前时间可以setContentView。这会增加一些xml文件,但避免在活动中添加更多代码。

  2. 只为白天/夜晚创建一个布局,在activity的onCreate中找到每个要进行主题设计的项目,并根据当前的白天/夜晚更新其属性。这可能会产生大量额外的代码,查找视图并为许多视图应用属性。

我选择第二种方案,但我乐于听取你的任何建议。那么,你会选择哪一个?为什么?


1
我会使用-night作为夜间模式的资源集限定符,将您的夜间特定资源放在其中。 - CommonsWare
这实际上只是取决于你是否介意在代码中进行GUI工作。就我个人而言,我认为单独为夜间和白天设置不同的布局会更清晰、更容易。可能还有第三种选择,即使用可绘制对象来设置背景、文本颜色和其他需要更改的内容,并为夜间/白天创建自定义状态。然后您可以循环遍历所有视图并更改状态。不过,我不知道这与您的选项1有多大差异。 - mpellegr
1
@CommonsWare,您能在回答中详细解释一下吗?谢谢。 - Alin
6个回答

19

我会使用-night作为夜间模式的资源集限定符,将您的夜间特定资源放在其中。

Android已经有了夜间模式的概念,根据时间和传感器切换夜间和白天模式。因此,您可以考虑使用它。

例如,要基于模式使用不同的主题,请创建res/values/styles.xmlres/values-night/styles.xml。在每个文件中都有相同名称的主题(例如AppTheme),但根据您想要在白天和晚上模式之间具有的任何差异来调整主题。当您按名称引用您的主题(例如,在清单中),Android将自动加载正确的资源,并且如果在这些活动运行时模式发生更改,则Android将自动销毁并重新创建您的活动。

现在,如果您希望手动控制是否使用夜间主题UI,则-night将无法帮助。


感谢您的回复。我需要在代码中设置夜间模式出现的时刻。在夏季,它可能是晚上22点,而在冬季则可能是晚上19点。因此,我猜 -night 不适用,因为设置夜间模式会显示“只有在设备启用汽车或桌面模式时,夜间模式才有效”。 - Alin
1
@Alin 你可以使用UiModeManager.enableCarMode()自己启用车载模式,尽管我必须说将设备置于车载模式的效果并不完全清楚。 - spaaarky21
2
从支持库23.2开始,有API可用于在应用程序中包含主题,根据白天和黑夜自动切换。http://android-developers.blogspot.com/2016/02/android-support-library-232.html - Damien Diehl
您好,抱歉打扰了这个帖子,不过能否与我分享如何制作夜间模式呢?非常感谢! - Ticherhaz FreePalestine
1
@Zuhrain:抱歉,我不知道你所说的“制作夜间模式”是什么意思。我建议你提出一个单独的Stack Overflow问题,在那里你可以更详细地解释你的需求。 - CommonsWare
嗨,当我们从代码中设置颜色时它不起作用?它总是从浅色资源中获取颜色。 - hallz12

17

点击这里查看完整的分步教程:点击这里

使用Appcompat v23.2支持库添加自动切换日夜主题

在你的build.gradle文件中添加以下行:

compile 'com.android.support:appcompat-v7:23.2.0'

请按照以下方式在styles.xml中设置主题样式

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:textColorPrimary">@color/textColorPrimary</item>
        <item name="android:textColorSecondary">@color/textColorSecondary</item>
    </style>
</resources>

现在在onCreate()方法中添加以下行代码,以为整个应用程序设置主题。

要设置默认的自动切换夜间模式,请使用:

AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);

要设置默认的夜间模式,请使用

AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);

设置默认的日间模式,请使用

AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);

输入图像描述


1
添加 MODE_NIGHT_FOLLOW_SYSTEM(默认)。此设置将跟随系统的设置,仅适用于 Android P。 - Codelaby
1
这个如何适应两种背景颜色?<item name="android:textColorPrimary">@color/textColorPrimary</item> - behelit
4
链接:http://blog.nkdroidsolutions.com/android-daynight-theme-example-using-appcompat-v23-2/ 看起来无法访问。 - Bruno Bieri

5

这是我的解决方案:

  • 我想要自动的日夜模式功能,但不想启用安卓中繁琐的车载模式。

-> 从NOAA网页中可以找到算法,根据给定的位置和日期计算太阳在地平线上的高度。

--> 使用这些算法,我创建了一个方法,根据两个双精度经纬度和一个日历计算太阳在地平线上的高度。

public class SolarCalculations {

    /**
     * Calculate height of the sun above horizon for a given position and date
     * @param lat Positive to N
     * @param lon Positive to E
     * @param cal Calendar containing current time, date, timezone, daylight time savings
     * @return height of the sun in degrees, positive if above the horizon
     */
    public static double CalculateSunHeight(double lat, double lon, Calendar cal){

        double adjustedTimeZone = cal.getTimeZone().getRawOffset()/3600000 + cal.getTimeZone().getDSTSavings()/3600000;

        double timeOfDay = (cal.get(Calendar.HOUR_OF_DAY) * 3600 + cal.get(Calendar.MINUTE) * 60 + cal.get(Calendar.SECOND))/(double)86400;

        double julianDay = dateToJulian(cal.getTime()) - adjustedTimeZone/24;

        double julianCentury = (julianDay-2451545)/36525;

        double geomMeanLongSun = (280.46646 + julianCentury * (36000.76983 + julianCentury * 0.0003032)) % 360;

        double geomMeanAnomSun = 357.52911+julianCentury*(35999.05029 - 0.0001537*julianCentury);

        double eccentEarthOrbit = 0.016708634-julianCentury*(0.000042037+0.0000001267*julianCentury);

        double sunEqOfCtr = Math.sin(Math.toRadians(geomMeanAnomSun))*(1.914602-julianCentury*(0.004817+0.000014*julianCentury))+Math.sin(Math.toRadians(2*geomMeanAnomSun))*(0.019993-0.000101*julianCentury)+Math.sin(Math.toRadians(3*geomMeanAnomSun))*0.000289;

        double sunTrueLong = geomMeanLongSun + sunEqOfCtr;

        double sunAppLong = sunTrueLong-0.00569-0.00478*Math.sin(Math.toRadians(125.04-1934.136*julianCentury));

        double meanObliqEcliptic = 23+(26+((21.448-julianCentury*(46.815+julianCentury*(0.00059-julianCentury*0.001813))))/60)/60;

        double obliqueCorr = meanObliqEcliptic+0.00256*Math.cos(Math.toRadians(125.04-1934.136*julianCentury));

        double sunDeclin = Math.toDegrees(Math.asin(Math.sin(Math.toRadians(obliqueCorr))*Math.sin(Math.toRadians(sunAppLong))));

        double varY = Math.tan(Math.toRadians(obliqueCorr/2))*Math.tan(Math.toRadians(obliqueCorr/2));

        double eqOfTime = 4*Math.toDegrees(varY*Math.sin(2*Math.toRadians(geomMeanLongSun))-2*eccentEarthOrbit*Math.sin(Math.toRadians(geomMeanAnomSun))+4*eccentEarthOrbit*varY*Math.sin(Math.toRadians(geomMeanAnomSun))*Math.cos(2*Math.toRadians(geomMeanLongSun))-0.5*varY*varY*Math.sin(4*Math.toRadians(geomMeanLongSun))-1.25*eccentEarthOrbit*eccentEarthOrbit*Math.sin(2*Math.toRadians(geomMeanAnomSun)));

        double trueSolarTime = (timeOfDay*1440+eqOfTime+4*lon-60*adjustedTimeZone) % 1440;

        double hourAngle;
        if(trueSolarTime/4<0)
            hourAngle = trueSolarTime/4+180;
        else
            hourAngle = trueSolarTime/4-180;

        double solarZenithAngle = Math.toDegrees(Math.acos(Math.sin(Math.toRadians(lat))*Math.sin(Math.toRadians(sunDeclin))+Math.cos(Math.toRadians(lat))*Math.cos(Math.toRadians(sunDeclin))*Math.cos(Math.toRadians(hourAngle))));

        double solarElevation = 90 - solarZenithAngle;

        double athmosphericRefraction;
        if(solarElevation>85)
            athmosphericRefraction = 0;
        else if(solarElevation>5)
            athmosphericRefraction = 58.1/Math.tan(Math.toRadians(solarElevation))-0.07/Math.pow(Math.tan(Math.toRadians(solarElevation)),3)+0.000086/Math.pow(Math.tan(Math.toRadians(solarElevation)),5);
        else if(solarElevation>-0.575)
            athmosphericRefraction = 1735+solarElevation*(-518.2+solarElevation*(103.4+solarElevation*(-12.79+solarElevation*0.711)));
        else
            athmosphericRefraction = -20.772/Math.tan(Math.toRadians(solarElevation));
        athmosphericRefraction /= 3600;

        double solarElevationCorrected = solarElevation + athmosphericRefraction;

        return solarElevationCorrected;

    }


    /**
     * Return Julian day from date
     * @param date
     * @return
     */
    public static double dateToJulian(Date date) {

        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);

        int a = (14-(calendar.get(Calendar.MONTH)+1))/12;
        int y = calendar.get(Calendar.YEAR) + 4800 - a;

        int m =  (calendar.get(Calendar.MONTH)+1) + 12*a;
        m -= 3;

        double jdn = calendar.get(Calendar.DAY_OF_MONTH) + (153.0*m + 2.0)/5.0 + 365.0*y + y/4.0 - y/100.0 + y/400.0 - 32045.0 + calendar.get(Calendar.HOUR_OF_DAY) / 24 + calendar.get(Calendar.MINUTE)/1440 + calendar.get(Calendar.SECOND)/86400;

        return jdn;
    } 
}

然后在MainActivity中,我有一个方法,每5分钟检查给定位置的太阳高度:

 if(displayMode.equals("auto")){
        double sunHeight = SolarCalculations.CalculateSunHeight(lat, lon, cal);
        if(sunHeight > 0 && mThemeId != R.style.AppTheme_Daylight)
        {//daylight mode
            mThemeId = R.style.AppTheme_Daylight;   
            this.recreate();
        }
        else if (sunHeight < 0 && sunHeight >= -6 && mThemeId != R.style.AppTheme_Dusk)
        {//civil dusk
            mThemeId = R.style.AppTheme_Dusk;
            this.recreate();
        }
        else if(sunHeight < -6 && mThemeId != R.style.AppTheme_Night)
        {//night mode
            mThemeId = R.style.AppTheme_Night;
            this.recreate();
        }
    }

这个方法用于设置当前的样式,我有三种不同的样式可供使用。其中两种用于白天和夜晚,另一种用于黄昏时太阳光线开始被大气层折射的情况。
<!-- Application theme. -->
<style name="AppTheme.Daylight" parent="AppBaseTheme">
    <item name="android:background">@color/white</item>
    <item name="android:panelBackground">@color/gray</item>
    <item name="android:textColor">@color/black</item>
</style>

<style name="AppTheme.Dusk" parent="AppBaseTheme">
    <item name="android:background">@color/black</item>
    <item name="android:panelBackground">@color/gray</item>
    <item name="android:textColor">@color/salmon</item>
</style>

<style name="AppTheme.Night" parent="AppBaseTheme">
    <item name="android:background">@color/black</item>
    <item name="android:panelBackground">@color/gray</item>
    <item name="android:textColor">@color/red</item>
</style>

这一功能一直表现不错,并考虑到了夏令时的修正。
来源:

美国国家海洋和大气管理局日出日落时间

儒略日


1
你可以使用时区。 - Shashank Priyadarshi
最好使用时区而不是询问位置。 - Marius Razvan Varvarei

4

实际上,您也可以使用主题来描述自定义可绘制对象。请参阅:如何在Android上在夜间模式和白天模式之间切换主题?。通过使用样式块创建您的主题,然后在xml布局中通过使用?attr指定主题中的某些内容。接下来,您应该能够在下一个活动中调用setTheme(R.styles.DAY_THEME),并且所有内容都应该得到更新。


我发现这种方法存在的问题,例如<item name="menuIconCamera">,我认为它是主题的一部分,因此我可以自定义它。但是在我的情况下,如果我需要为imgLogo使用不同的drawable呢? - Alin
那些不是Android系统属性,它们是自定义的。你需要自己创建它们。这里有一个很好的例子:https://dev59.com/03A75IYBdhLWcg3wKlqM#3441986。 - mpellegr
另外,你也可以直接在xml布局中使用这些自定义样式属性。比如,如果你创建了一个名为“exit_button_background”的属性,你可以在xml中设置该值,例如“myapp:exit_button_background="@drawable/night_background"”,但对于你的情况,似乎你需要创建自定义属性,以便在自定义的日夜主题中使用它们。 - mpellegr
是的,在这种方式下它可以工作,定义自定义属性并在样式中使用它们。非常感谢您的帮助。 - Alin

1
更新答案
  1. enable dark theme:

    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
    
  2. forcefully disable dark theme:

    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
    
  3. set app theme based on mobile settings of dark mode, i.e. if dark mode is enabled then the theme will be set to a dark theme, if not then the default theme, but this will only work in version >= Android version Q

    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
    

注意事项:

  1. 您的应用/活动的基本主题应为

"Theme.AppCompat.DayNight"

喜欢

<style name="DarkTheme" parent="Theme.AppCompat.DayNight">
    <item name="windowActionBar">false</item>
    <item name="windowNoTitle">true</item>
</style>
  1. 你的res文件夹中的名称应以-night结尾,以便为日间和夜间主题设置不同的颜色和图像,例如

drawable & drawable-night,
values & values-night


0
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);

3
请在回答问题时尽量更清晰、更详细地解释。 - Yunus Temurlenk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接