迁移到 Androidx 后更改语言环境不起作用

我有一个旧的项目,支持多语言。我想升级支持库和目标平台,在迁移到 Androidx之前一切都很好,但现在改变语言不工作!

我使用这个代码来更改 App 的默认语言环境

private static Context updateResources(Context context, String language)
{
Locale locale = new Locale(language);
Locale.setDefault(locale);


Configuration configuration = context.getResources().getConfiguration();
configuration.setLocale(locale);


return context.createConfigurationContext(configuration);
}

通过覆盖 attachBaseContext对每个活动调用这个方法,如下所示:

@Override
protected void attachBaseContext(Context newBase)
{
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
String language = preferences.getString(SELECTED_LANGUAGE, "fa");
super.attachBaseContext(updateResources(newBase, language));
}

我尝试其他方法获得字符串,我注意到 getActivity().getBaseContext().getString工作和 getActivity().getString不工作。即使下面的代码也不起作用,并且始终在默认资源 string.xml 中显示 app_name值。

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"/>

我在 https://github.com/Freydoonk/LanguageTest中共享一个示例代码

getActivity()..getResources().getIdentifier不工作,总是返回0!

20563 次浏览

1. 在 AttachBaseContext ()中可以使用的方法

private void setLanguage(Context mContext, String localeName) {
Locale myLocale = new Locale(localeName);
Resources res = mContext.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
Configuration conf = res.getConfiguration();
conf.locale = myLocale;
res.updateConfiguration(conf, dm);
}

2. 重写活动

@Override
protected void attachBaseContext(Context newBase) {
setLanguage(newBase, "your language");
super.attachBaseContext(newBase);
}

注意: 在我重新创建了这个活动之后,这个方法对我来说非常有效

最后,我在我的应用程序中发现了问题。当将项目迁移到我的项目的 Androidx依赖项时,依赖项变化如下:

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0-alpha04'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-alpha02'
}

正如我们所看到的,当我将 androidx.appcompat:appcompat的版本改为最新的稳定版本 1.0.2时,我的问题得到了解决,并且改变的语言工作正常。

我在 Maven 仓库中找到了 appcompat库的最新稳定版本。我还将其他库更改为最新的稳定版本。

现在,我的应用程序依赖部分如下:

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

现在有一个更新的版本也可以用:

implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'

正如@Fred 提到的,appcompat:1.1.0-alpha03有一个小故障,尽管在他们的 发布版本日志上没有提到

最后我得到了定位的解决方案,在我的情况下,实际问题是与 bundle apk,因为它分裂了定位文件。在 bundle apk中,默认情况下将生成所有拆分。但是在您的 build.gradle文件的 android 块中,您可以声明将生成哪些拆分。

bundle {
language {
// Specifies that the app bundle should not support
// configuration APKs for language resources. These
// resources are instead packaged with each base and
// dynamic feature APK.
enableSplit = false
}
}

将这段代码添加到 build.gradle的仿真块文件后,问题就解决了。

androidx.appcompat:appcompat:1.1.0上有同样的错误。切换到 androidx.appcompat:appcompat:1.1.0-rc01,现在在 Android 5-6.上改变朗斯

有一个问题在新的应用程序压缩库相关的夜间模式,导致覆盖的配置在 android 21至25。 这个问题可以通过在调用这个公共函数时应用配置来解决:

Public void applicyOverrideConfiguration (Configuration overrideConfiguration

对我来说,这个小技巧通过将设置从覆盖的配置复制到我的配置来工作,但是您可以做任何您想做的事情。最好将语言逻辑重新应用于新配置,以尽量减少错误

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
if (Build.VERSION.SDK_INT >= 21&& Build.VERSION.SDK_INT <= 25) {
//Use you logic to update overrideConfiguration locale
Locale locale = getLocale()//your own implementation here;
overrideConfiguration.setLocale(locale);
}
super.applyOverrideConfiguration(overrideConfiguration);
}

更新日期: 2022年9月5日:

AppCompat 1.5.0发布了,其中包括 这些变化,特别是:

这个稳定的版本包括对夜间模式稳定性的改进

[...]

修正了 AppCompat 的上下文包装器重用应用程序上下文的支持资源实现,导致在应用程序上下文上覆盖 uiMode的问题。(Idf9d5)

深入挖掘代码揭示了几个关键的变化,这基本上意味着,类似于以前,如果你没有使用 ContextWrapperContextThemeWrapper不要提供给用户设置与他们在设备上设置的不同的夜间模式然后所有上下文包装,主题和本地化更新 应该现在正常工作,你 应该能够安全地删除所有解决方案下面解释或任何其他黑客可能有的地方。

不幸的是,如果你使用任何类型的 ContextWrapper或允许用户手动设置你的应用程序的夜间模式,那么似乎仍然存在问题,我不知道为什么谷歌有这样的麻烦修复这一点。我遇到的问题是: 如果 uiMode不再被覆盖,那意味着当你:

  • 把你的设备设置为黑暗模式,
  • 将你的应用程序设置为灯光模式(禁用夜间模式) ,
  • 旋转你的手机到景观,
  • 锁定屏幕,
  • 再次解锁屏幕,同时仍然在横向,

然后取决于你的设备和可能的 Android 版本,你可能会看到应用程序上下文的 uiMode已经过时,最终在黑色和白色主题上出现了可怕的黑色和白色的小故障。这也发生在设备在光模式和应用程序在夜间模式。此外,应用程序的语言环境可能会被重置为设备语言环境。在这些情况下,您需要使用下面描述的解决方案。

AppCompat 1.2.0-1.5.0的工作解决方案,当遇到语言环境或白天/晚上的问题时:

如果在 AppCompat 1.2.0-1.4.2的 attachBaseContext中使用 ContextWrapperContextThemeWrapper,则区域设置更改将中断,因为当您将包装的上下文传递给 super 时,

  1. 1.2.0-1.4.2 AppCompatActivity发出内部调用,将您的 ContextWrapper包装在另一个 ContextThemeWrapperAppCompatDelegateImpl中,因此最终在您的语言环境中被忽略,
  2. 或者如果使用 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
}


// OPTIONAL createConfigurationContext and/or onStart code below may be needed depending on your ContextWrapper implementation to avoid issues with themes


override fun createConfigurationContext(overrideConfiguration: Configuration) : Context {
val context = super.createConfigurationContext(overrideConfiguration)
TODO("your wrapping implementation here")
}


private fun fixStaleConfiguration() {
// we only want to fix the configuration if our app theme is different than the system theme, otherwise it will result in an infinite configuration change loop causing a StackOverflowError and crashing your app
if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
applicationContext?.configuration?.uiMode = resources.configuration.uiMode
TODO("your locale updating implementation here")
// OPTIONAL if you are using a context wrapper, the wrapper might have stale resources with wrong uiMode and/or locale which you need to clear or update at this stage
}


override fun onRestart() {
fixStaleConfiguration()
super.onRestart()
}


// OPTIONAL if you have specified configChanges in your manifest, especially orientation and uiMode
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)
}

AppCOMPAT1.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转移到 uiMode,这样我们就不会丢失黑暗/光明模式设置。

请注意 ,如果您没有在清单中指定 configChanges="uiMode",那么它只能自己工作。如果你这样做,那么还有另一个错误: 在 onConfigurationChangednewConfig.uiMode不会设置由 AppCompatDelegateImplonConfigurationChanged。如果您将 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)
}

来自@0101100101的回答对我很有用。

只知道我用过

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration)
{
if (overrideConfiguration != null) {
int uiMode = overrideConfiguration.uiMode;
overrideConfiguration.setTo(getResources().getConfiguration());
overrideConfiguration.uiMode = uiMode;
}
super.applyOverrideConfiguration(overrideConfiguration);
}

所以只有 getResources()而不是 getBaseContext().getResources()

在我的例子中,我使用重写的 getResources ()来扩展 ContextWrapper。 但是在 appyOverrideConfiguration 被调用之后,我无法访问我的自定义 getResources,我只能使用标准的 getResources。

如果我使用上面的代码一切工作正常。

也可以通过简单地调用 Activity.applyOverrideConfiguration()中的 getResources()来解决 androidx.appcompat:appcompat:1.1.0错误

@Override public void
applyOverrideConfiguration(Configuration cfgOverride)
{
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// add this to fix androidx.appcompat:appcompat 1.1.0 bug
// which happens on Android 6.x ~ 7.x
getResources();
}


super.applyOverrideConfiguration(cfgOverride);
}

回答晚了,不过我觉得可能会有帮助。从 Appcompat: appcompat: 1.2.0-beta01开始,0101100101覆盖 applyOverrideConfiguration的解决方案不再适用于我。相反,在然后重写的 attacheBaseContext中,您必须调用 applyOverrideConfiguration() 而不是重写它

override fun attachBaseContext(newBase: Context) {
val newContext = LocaleHelper.getUpdatedContext(newBase)
super.attachBaseContext(newContext)
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1){
applyOverrideConfiguration(newContext.resources.configuration)
}
}

只可惜他的解决方案只能在1.1.0上运行。根据我的研究,这应该已经被正式修复了。只是这只虫子还在这里感觉很奇怪。我知道我使用测试版,但是对于那些想要使用最新版本的人来说,这个解决方案对我来说是有效的。 在模拟器 api 级别21-25上测试。超过这个 api 级别,你就不用担心了。

试试这样:

public class MyActivity extends AppCompatActivity {
public static final float CUSTOM_FONT_SCALE = 4.24f;
public static final Locale CUSTOM_LOCALE = Locale.CANADA_FRENCH; // or whatever


@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(useCustomConfig(newBase));
}


private Context useCustomConfig(Context context) {
Locale.setDefault(CUSTOM_LOCALE);
if (Build.VERSION.SDK_INT >= 17) {
Configuration config = new Configuration();
config.fontScale = CUSTOM_FONT_SCALE;
config.setLocale(CUSTOM_LOCALE);
return context.createConfigurationContext(config);
} else {
Resources res = context.getResources();
Configuration config = new Configuration(res.getConfiguration());
config.fontScale = CUSTOM_FONT_SCALE;
config.locale = CUSTOM_LOCALE;
res.updateConfiguration(config, res.getDisplayMetrics());
return context;
}
}
}

资料来源: 问题追踪者的评论来自问题追踪者评论的第一个样本链接

虽然上面的方法对我来说效果不错,但 从问题追踪者评论链接的第二个样本的另一个选择如下(我个人还没有尝试过) :

@RequiresApi(17)
public class MyActivity extends AppCompatActivity {
public static final float CUSTOM_FONT_SCALE = 4.24f;
public static final Locale CUSTOM_LOCALE = Locale.CANADA_FRENCH; // or whatever


@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);


Configuration config = new Configuration();
config.fontScale = CUSTOM_FONT_SCALE;
applyOverrideConfiguration(config);
}


@Override
public void applyOverrideConfiguration(Configuration newConfig) {
super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig));
}


private Configuration updateConfigurationIfSupported(Configuration config) {
if (Build.VERSION.SDK_INT >= 24) {
if (!config.getLocales().isEmpty()) {
return config;
}
} else {
if (config.locale != null) {
return config;
}
}


Locale locale = CUSTOM_LOCALE;
if (locale != null) {
if (Build.VERSION.SDK_INT >= 17) {
config.setLocale(locale);
} else {
config.locale = locale;
}
}
return config;
}
}

我使用的是“ androidx.appcompat: appcompat: 1.3.0-alpha01”,但我认为它也可以在 版本1.2.0上工作。
下面的代码基于 Android 代码搜索

import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import java.util.*


open class MyBaseActivity :AppCompatActivity(){


override fun attachBaseContext(newBase: Context?) {
super.attachBaseContext(newBase)
val config = Configuration()
applyOverrideConfiguration(config)
}


override fun applyOverrideConfiguration(newConfig: Configuration) {
super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig))
}


open fun updateConfigurationIfSupported(config: Configuration): Configuration? {
// Configuration.getLocales is added after 24 and Configuration.locale is deprecated in 24
if (Build.VERSION.SDK_INT >= 24) {
if (!config.locales.isEmpty) {
return config
}
} else {
if (config.locale != null) {
return config
}
}
// Please Get your language code from some storage like shared preferences
val languageCode = "fa"
val locale = Locale(languageCode)
if (locale != null) {
// Configuration.setLocale is added after 17 and Configuration.locale is deprecated
// after 24
if (Build.VERSION.SDK_INT >= 17) {
config.setLocale(locale)
} else {
config.locale = locale
}
}
return config
}
}

解决方案:

在应用程序级别级别中,我在 android 部门中包含了以下代码,

bundle {
language {
// Specifies that the app bundle should not support
// configuration APKs for language resources. These
// resources are instead packaged with each base and
// dynamic feature APK.
enableSplit = false
}
}

Https://medium.com/dwarsoft/how-to-provide-languages-dynamically-using-app-bundle-567d2ec32be6

现在,这种语言并没有随着这些库而改变: Appcompat: appcompat: 1.1.0, Appcompat: appcompat: 1.2.0

这个问题只有在这个图书馆才能解决: Appcompat: appcompat: 1.3.0-rc01