Day/Night theme for android app
Check this tutorial for a complete step by step example: click here
Add Auto Switching DayNight Theme using Appcompat v23.2 support library
Add following line in your build.gradle
file
compile 'com.android.support:appcompat-v7:23.2.0'
Make your theme style in styles.xml
as below
<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>
Now add the following line code onCreate()
method for setting theme for entire app.
For Setting Default Auto Switching Night Mode use
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO);
For Setting Default Night Mode use
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
For Setting Default Day Mode use
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
here is my solution:
- I wanted to have automatic day/night features but not have to enable the cumbersome car mode in android.
-> From NOAA webpages one can find algorithms to calculate the sun height over the horizon given a certain position and date.
--> Using these algoritms I created a method that calculate the sun's height over the horizon given two doubles Latitude & Longitude and a Calendar
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;
}
}
Then in the MainActivity I have a method that checks every 5 minutes for the sun's height at the given position:
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();
}
}
This methods sets the current style to be used and I have three of them. Two for daylight and night, one for dusk when sunlight begins to be refracted into the atmosphere
<!-- 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>
This has been working pretty well and take into account daylight saving time correction.
Sources:
NOAA Sunrise Sunset
Julian Day
I'd use -night
as a resource set qualifier for night mode, putting your night-specific resources in there.
Android already has the notion of night mode, switching between night and daytime modes based upon time of day and sensors. Hence, you might consider using it.
For example, to have a different theme based on mode, create res/values/styles.xml
and res/values-night/styles.xml
. Have a theme with the same name in each file (e.g., AppTheme
), but tailor the theme based upon whatever differences you want have between day and night modes. When you reference your theme by name (e.g., in the manifest), Android will automatically load in the right resources, and Android will automatically destroy and recreate your activities if the mode changes while those activities are running.
Now, if you want manual user control over whether to use a night-themed UI, -night
will not help.