2023年3月15日更新:
AppCompat 1.6.1已发布,但似乎没有破坏任何东西。下面的解决方法仍然有效,耶!
2022年9月5日更新:
发布了AppCompat 1.5.0,其中包括这些更改,具体如下:
此稳定版本包括夜间模式稳定性的改进
[...]
修复了一个问题,即AppCompat的上下文包装器重用应用程序上下文的支持资源实现,导致在应用程序上下文上覆盖了uiMode
。(Idf9d5)
深入代码后发现几个关键的更改,这意味着,与以前类似,如果您没有使用
ContextWrapper
或
ContextThemeWrapper
并且
不提供用户设置与设备不同的夜间模式,那么所有上下文包装和主题和语言环境更新
应该正常工作,您
应该可以安全地删除下面解释的所有解决方法或任何其他已经实施的黑客行为。
不幸的是,如果您正在使用任何类型的
ContextWrapper
或允许用户手动设置应用程序的夜间模式,则仍然存在问题,我不确定为什么Google在修复此问题方面有困难。我遇到的问题是:如果
uiMode
不再被覆盖,那么当您:
- 将设备设置为暗模式,
- 将应用程序设置为亮模式(夜间模式已禁用),
- 将手机旋转到横向模式,
- 锁定屏幕,
- 在仍处于横向模式的情况下再次解锁屏幕,
然后根据您的设备和可能的Android版本,您可能会看到应用程序上下文的uiMode
过时,并最终导致黑色与黑色和白色与白色主题的故障。对于处于浅色模式的设备和处于夜间模式的应用程序也会发生这种情况。此外,您的应用程序的区域设置可能会重置为设备的区域设置。在这些情况下,您需要使用下面描述的解决方案。
当遇到区域设置或日/夜模式问题时,在AppCompat 1.2.0-1.6.1中的有效解决方案:
如果您在AppCompat 1.2.0-1.4.2中的attachBaseContext
中使用ContextWrapper
或ContextThemeWrapper
,区域设置的更改将会中断,因为当您将封装的上下文传递给super时,
- 在1.2.0-1.4.2版本中,
AppCompatActivity
会进行内部调用,将您的ContextWrapper
包装在另一个ContextThemeWrapper
和AppCompatDelegateImpl
中,导致您的语言环境被忽略。
- 或者如果您使用了
ContextThemeWrapper
,则会覆盖其配置为空白配置,类似于1.1.0版本中发生的情况。
无论您在1.5.0版本中是否使用上下文包装器(如我最新更新中所述),仍可能出现主题故障和应用程序语言环境重置。解决方案始终相同,没有其他方法适用于我。最大的障碍是,与1.1.0版本不同,applyOverrideConfiguration
是在您的基本上下文中调用的,而不是在您的宿主活动中调用,因此您不能像在1.1.0版本中那样在活动中覆盖该方法并修复语言环境(或uiMode
)。我知道的唯一有效的解决方案是通过覆盖getDelegate()
来反转包装顺序,以确保您的包装和/或语言环境覆盖最后应用。首先,您需要添加以下类:
Kotlin示例(请注意,类必须位于androidx.appcompat.app
包内,因为唯一存在的AppCompatDelegate
构造函数是包私有的)
package androidx.appcompat.app
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.AttributeSet
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar
class BaseContextWrappingDelegate(private val superDelegate: AppCompatDelegate) : AppCompatDelegate() {
override fun getSupportActionBar() = superDelegate.supportActionBar
override fun setSupportActionBar(toolbar: Toolbar?) = superDelegate.setSupportActionBar(toolbar)
override fun getMenuInflater(): MenuInflater? = superDelegate.menuInflater
override fun onCreate(savedInstanceState: Bundle?) {
superDelegate.onCreate(savedInstanceState)
removeActivityDelegate(superDelegate)
addActiveDelegate(this)
}
override fun onPostCreate(savedInstanceState: Bundle?) = superDelegate.onPostCreate(savedInstanceState)
override fun onConfigurationChanged(newConfig: Configuration?) = superDelegate.onConfigurationChanged(newConfig)
override fun onStart() = superDelegate.onStart()
override fun onStop() = superDelegate.onStop()
override fun onPostResume() = superDelegate.onPostResume()
override fun setTheme(themeResId: Int) = superDelegate.setTheme(themeResId)
override fun <T : View?> findViewById(id: Int) = superDelegate.findViewById<T>(id)
override fun setContentView(v: View?) = superDelegate.setContentView(v)
override fun setContentView(resId: Int) = superDelegate.setContentView(resId)
override fun setContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.setContentView(v, lp)
override fun addContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.addContentView(v, lp)
override fun attachBaseContext2(context: Context) = wrap(superDelegate.attachBaseContext2(super.attachBaseContext2(context)))
override fun setTitle(title: CharSequence?) = superDelegate.setTitle(title)
override fun invalidateOptionsMenu() = superDelegate.invalidateOptionsMenu()
override fun onDestroy() {
superDelegate.onDestroy()
removeActivityDelegate(this)
}
override fun getDrawerToggleDelegate() = superDelegate.drawerToggleDelegate
override fun requestWindowFeature(featureId: Int) = superDelegate.requestWindowFeature(featureId)
override fun hasWindowFeature(featureId: Int) = superDelegate.hasWindowFeature(featureId)
override fun startSupportActionMode(callback: ActionMode.Callback) = superDelegate.startSupportActionMode(callback)
override fun installViewFactory() = superDelegate.installViewFactory()
override fun createView(parent: View?, name: String?, context: Context, attrs: AttributeSet): View? = superDelegate.createView(parent, name, context, attrs)
override fun setHandleNativeActionModesEnabled(enabled: Boolean) {
superDelegate.isHandleNativeActionModesEnabled = enabled
}
override fun isHandleNativeActionModesEnabled() = superDelegate.isHandleNativeActionModesEnabled
override fun onSaveInstanceState(outState: Bundle?) = superDelegate.onSaveInstanceState(outState)
override fun applyDayNight() = superDelegate.applyDayNight()
override fun setLocalNightMode(mode: Int) {
superDelegate.localNightMode = mode
}
override fun getLocalNightMode() = superDelegate.localNightMode
private fun wrap(context: Context): Context {
TODO("your wrapping implementation here")
}
}
然后在我们的基础活动类中(确保您首先删除任何其他以前的解决方法),添加此代码:
private var baseContextWrappingDelegate: AppCompatDelegate? = null
override fun getDelegate() = baseContextWrappingDelegate ?: BaseContextWrappingDelegate(super.getDelegate()).apply {
baseContextWrappingDelegate = this
}
override fun createConfigurationContext(overrideConfiguration: Configuration) : Context {
val context = super.createConfigurationContext(overrideConfiguration)
TODO("your wrapping implementation here")
}
private fun fixStaleConfiguration() {
if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
applicationContext?.configuration?.uiMode = resources.configuration.uiMode
TODO("your locale updating implementation here")
}
override fun onRestart() {
fixStaleConfiguration()
super.onRestart()
}
override fun onConfigurationChanged(newConfig: Configuration) {
fixStaleConfiguration()
super.onConfigurationChanged(newConfig)
}
关于如何成功地“清除过期资源”的示例,这在你的情况下可能需要也可能不需要:
(context as? ContextThemeWrapper)?.run {
if (mContextThemeWrapperResources == null) {
mContextThemeWrapperResources = ContextThemeWrapper::class.java.getDeclaredField("mResources")
mContextThemeWrapperResources!!.isAccessible = true
}
mContextThemeWrapperResources!!.set(this, null)
} ?: (context as? androidx.appcompat.view.ContextThemeWrapper)?.run {
if (mAppCompatContextThemeWrapperResources == null) {
mAppCompatContextThemeWrapperResources = androidx.appcompat.view.ContextThemeWrapper::class.java.getDeclaredField("mResources")
mAppCompatContextThemeWrapperResources!!.isAccessible = true
}
mAppCompatContextThemeWrapperResources!!.set(this, null)
}
(context as? AppCompatActivity)?.run {
if (mAppCompatActivityResources == null) {
mAppCompatActivityResources = AppCompatActivity::class.java.getDeclaredField("mResources")
mAppCompatActivityResources!!.isAccessible = true
}
mAppCompatActivityResources!!.set(this, null)
}
APPCOMPAT 1.1.0 的旧答案和已确认可行解决方案:
基本上在后台发生的情况是,在您正确设置了attachBaseContext
中的配置之后,AppCompatDelegateImpl
然后会覆盖配置为一个完全新鲜的没有区域设置的配置:
final Configuration conf = new Configuration();
conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
try {
...
((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
handled = true;
} catch (IllegalStateException e) {
...
}
在Chris Banes的未发布提交中,这个问题实际上已经修复了:新的配置是基本上下文配置的深拷贝。
final Configuration conf = new Configuration(baseConfiguration);
conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
try {
...
((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
handled = true;
} catch (IllegalStateException e) {
...
}
在此版本发布���前,手动执行完全相同的操作是可能的。要继续使用版本1.1.0,请将以下内容添加到attachBaseContext
下方:
Kotlin解决方案
override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
if (overrideConfiguration != null) {
val uiMode = overrideConfiguration.uiMode
overrideConfiguration.setTo(baseContext.resources.configuration)
overrideConfiguration.uiMode = uiMode
}
super.applyOverrideConfiguration(overrideConfiguration)
}
Java解决方案
@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
if (overrideConfiguration != null) {
int uiMode = overrideConfiguration.uiMode;
overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration());
overrideConfiguration.uiMode = uiMode;
}
super.applyOverrideConfiguration(overrideConfiguration);
}
这段代码实际上与底层的Configuration(baseConfiguration)
做了一样的事情,但由于我们是在AppCompatDelegate
已经设置正确的uiMode
之后执行的,因此我们必须确保在修复之后将被覆盖的uiMode
带回去,以便不会丢失暗/亮模式设置。
请注意,如果您在清单文件中指定了configChanges="uiMode"
,则此方法仅适用于自身。否则,还有另一个错误:在onConfigurationChanged
中,newConfig.uiMode
不会被AppCompatDelegateImpl
的onConfigurationChanged
设置。如果您在您的基本活动代码中复制AppCompatDelegateImpl
用于计算当前夜间模式的所有代码并在super.onConfigurationChanged
调用之前进行覆盖,则可以解决此问题。在Kotlin中,它应该是这个样子的:
private var activityHandlesUiMode = false
private var activityHandlesUiModeChecked = false
private val isActivityManifestHandlingUiMode: Boolean
get() {
if (!activityHandlesUiModeChecked) {
val pm = packageManager ?: return false
activityHandlesUiMode = try {
val info = pm.getActivityInfo(ComponentName(this, javaClass), 0)
info.configChanges and ActivityInfo.CONFIG_UI_MODE != 0
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
activityHandlesUiModeChecked = true
return activityHandlesUiMode
}
override fun onConfigurationChanged(newConfig: Configuration) {
if (isActivityManifestHandlingUiMode) {
val nightMode = if (delegate.localNightMode != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED)
delegate.localNightMode
else
AppCompatDelegate.getDefaultNightMode()
val configNightMode = when (nightMode) {
AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES
AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO
else -> applicationContext.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
}
newConfig.uiMode = configNightMode or (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv())
}
super.onConfigurationChanged(newConfig)
}
android:configChanges="uiMode"
可以解决这个问题,但是相应地,当在浅色和深色主题之间切换时,我们会得到颜色伪影。 - 0101100101