2020 年 8 月 21 日更新:
AppCompat 1.2.0 终于发布了。如果您根本没有使用 ContextWrapper 或 ContextThemeWrapper,则应该没有其他事情可做,您应该可以删除 1.1.0 中的任何解决方法!
如果您在attachBaseContext 中使用ContextWrapper 或ContextThemeWrapper,语言环境更改将会中断,因为当您将包装的上下文传递给super 时,
- 1.2.0
AppCompatActivity 进行内部调用,将您的ContextWrapper 包装在另一个ContextThemeWrapper 中,
- 或者如果您使用
ContextThemeWrapper,将其配置覆盖为空白,类似于 1.1.0 中发生的情况。
但解决方案总是一样的。对于情况 2,我尝试了多种其他解决方案,但正如 @Kreiri 在 cmets 中指出的那样(感谢您的调查帮助!),AppCompatDelegateImpl 总是最终剥离了语言环境。最大的障碍是,与 1.1.0 不同,applyOverrideConfiguration 是在您的基本上下文而不是您的主机活动中调用的,因此您不能像在 1.1.0 中那样在活动中覆盖该方法并修复语言环境.我知道的唯一可行的解决方案是通过覆盖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")
}
}
然后在我们的基础活动类中删除所有 1.1.0 解决方法并简单地添加以下内容:
private var baseContextWrappingDelegate: AppCompatDelegate? = null
override fun getDelegate() = baseContextWrappingDelegate ?: BaseContextWrappingDelegate(super.getDelegate()).apply {
baseContextWrappingDelegate = this
}
根据您使用的ContextWrapper 实现,配置更改可能会破坏主题或区域设置更改。要解决此问题,请另外添加:
override fun createConfigurationContext(overrideConfiguration: Configuration) : Context {
val context = super.createConfigurationContext(overrideConfiguration)
TODO("your wrapping implementation here")
}
你很好!您可以期待 Google 在 1.3.0 中再次打破这一点。我会去修复它……再见,太空牛仔!
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) {
...
}
In an unreleased commit by 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)
}