Reputation: 2696
TL,DR;
ContextCompat.getColor()
does not use the night colors (values-night/colors.xml
) though it should when night mode is enabled.
Here is the problem:
Hi everyone,
So I'm implementing a dark theme for my Android app, I call this to enable it :
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
I have set colors in values/colors.xml
and there dark version in values-night/colors.xml
. The colors changes well depending on the nightMode, BUT :
when I use ContextCompat.getColor(getApplicationContext(), R.id.myColor)
, it uses the normal colors (values/colors.xml
) and not the night colors (values-night/colors.xml
).
In my build.gradle
, I have set these :
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0-beta01'
Could someone please tell me what am I doing wrong ?
PS : I already looked at the following question and it does not answer this problem https://stackoverflow.com/questions/57779661/contextcompat-getcolor-method-ignores-night-mode
Upvotes: 20
Views: 3563
Reputation: 287
For those who find it later. I've tweaked the solution a bit @ygngy
This answer solves the configuration problem at runtime and leaves the actual context on every call. It could also be used with a dagger so as not to generate a million objects each time.
in some cases, this is necessary when the color needs to be taken at runtime, for example, in a mapper when receiving data from the server, and so on
interface ThemeContextProvider {
fun getColor(@ColorRes id: Int): Int
fun getDrawable(@DrawableRes id: Int): Drawable?
}
class ThemeContextProviderImpl @Inject constructor(
private val application: Context
) : ThemedContext {
private val res: Resources = application.resources
private val filter by lazy {
application.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()
}
private lateinit var themedContext: Context
private var configuration = Configuration(res.configuration)
init {
getCurrentContext()
}
private fun getCurrentContext() {
configuration.uiMode = when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO or filter
AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES or filter
else -> res.configuration.uiMode
}
themedContext = application.createConfigurationContext(configuration)
}
override fun getColor(@ColorRes id: Int): Int {
getCurrentContext()
return ContextCompat.getColor(themedContext, id)
}
override fun getDrawable(@DrawableRes id: Int): Drawable? {
getCurrentContext()
return ContextCompat.getDrawable(themedContext, id)
}
}
Upvotes: 1
Reputation: 3979
Application context don't know anything about current theme or day/night so if you get a resource from application context, you will get the resource in default app theme. A solution to this problem is to use activity/fragment context but in some situations you may not have activity or fragment and only have application context. I have created a context wrapper class to add Day & Night theme to Application context:
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
class ThemedContext(application: Context) {
private val themedContext: Context
init {
val res: Resources = application.resources
val configuration = Configuration(res.configuration)
val filter = res.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()
configuration.uiMode = when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO or filter
AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES or filter
else -> res.configuration.uiMode
}
themedContext = application.createConfigurationContext(configuration)
}
fun getColor(@ColorRes id: Int) = ContextCompat.getColor(themedContext, id)
fun getDrawable(@DrawableRes id: Int) = ContextCompat.getDrawable(themedContext, id)
//todo Add other getter methods as needed...
}
This code has been tested and worked.
Upvotes: 6
Reputation: 21
I faced similar question as you, and I also found the core of the issue is that Application don't have a theme wrapper like Activity. so W0rmH0le's answer can resolve this problem. but for me, there are lots of code can't fetch an activity or view's context. so I thought why don't we create one singleton Context wrapper nightMode theme. there is the code, and it works fine for me.
Resources res = getApplication().getResources();
Configuration configuration = new Configuration(res.getConfiguration());
int nightNode = AppCompatDelegate.getDefaultNightMode();
if (nightNode == AppCompatDelegate.MODE_NIGHT_NO) {
configuration.uiMode = Configuration.UI_MODE_NIGHT_NO | (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
} else if (nightNode == AppCompatDelegate.MODE_NIGHT_YES) {
configuration.uiMode = Configuration.UI_MODE_NIGHT_YES| (res.getConfiguration().uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
} else {
configuration.uiMode = res.getConfiguration().uiMode;
}
mThemeContext = getApplication().createConfigurationContext(configuration);
then you can use mThemeContext replace Application, and you can found the right color followed night mode.
Upvotes: 2
Reputation: 18677
I faced similar issues with night mode. Some screens were fine but others were keeping the regular theme. In the end, I found out that I was instantiating some views using the Application's context instead of the current's activity context. For some reason, Application's context does not track this kind of information.
So, update your code to use current's activity context instead of the application context.
For reference for other users. Avoid:
ContextCompat.getColor(getApplicationContext(), R.id.myColor)
And use:
// In a Activity
ContextCompat.getColor(this, R.id.myColor)
// In a View
ContextCompat.getColor(getContext(), R.id.myColor)
// In a Fragment (check against null because getContext can trigger a NPE
Context context = getContext()
if (context != null) {
ContextCompat.getColor(context, R.id.myColor)
}
Upvotes: 29