Android中context.getResources.updateConfiguration()已被弃用。

127

最近,在Android API 25中,context.getResources().updateConfiguration()已被弃用,并建议使用context.createConfigurationContext()代替。

有人知道如何使用createConfigurationContext来覆盖android系统语言环境吗?

在此之前,这是通过以下方式完成的:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.getResources().updateConfiguration(config,
                                 context.getResources().getDisplayMetrics());

那么 applyOverrideConfiguration 怎么样(未经测试)? - user1480019
这里也有一个简单的解决方案,与此非常相似 https://dev59.com/C1kS5IYBdhLWcg3wiXbI#40849142 - Thanasis Saxanidis
[updateConfiguration在API级别25中已被弃用] https://developer.android.com/reference/android/content/res/Resources - Md Sifatul Islam
要以最新的方式更改应用程序语言环境,请查看此链接-https://dev59.com/xpPfa4cB1Zd3GeqPADSs#75388129 - Shakil Ahmed Shaj
10个回答

151
受到Calligraphy的启发,我最终创建了一个上下文包装器。在我的情况下,我需要覆盖系统语言,以为我的应用程序用户提供更改应用程序语言的选项,但可以根据您需要实现的任何逻辑进行自定义。
    import android.annotation.TargetApi;
    import android.content.Context;
    import android.content.ContextWrapper;
    import android.content.res.Configuration;
    import android.os.Build;
    
    import java.util.Locale;
    
    public class MyContextWrapper extends ContextWrapper {

    public MyContextWrapper(Context base) {
        super(base);
    }

    @SuppressWarnings("deprecation")
    public static ContextWrapper wrap(Context context, String language) {
        Configuration config = context.getResources().getConfiguration();
        Locale sysLocale = null;
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
            sysLocale = getSystemLocale(config);
        } else {
            sysLocale = getSystemLocaleLegacy(config);
        }
        if (!language.equals("") && !sysLocale.getLanguage().equals(language)) {
            Locale locale = new Locale(language);
            Locale.setDefault(locale);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                setSystemLocale(config, locale);
            } else {
                setSystemLocaleLegacy(config, locale);
            }
            
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
             context = context.createConfigurationContext(config);
        } else {
             context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
            }
        return new MyContextWrapper(context);
    }

    @SuppressWarnings("deprecation")
    public static Locale getSystemLocaleLegacy(Configuration config){
        return config.locale;
    }

    @TargetApi(Build.VERSION_CODES.N)
    public static Locale getSystemLocale(Configuration config){
        return config.getLocales().get(0);
    }

    @SuppressWarnings("deprecation")
    public static void setSystemLocaleLegacy(Configuration config, Locale locale){
        config.locale = locale;
    }

    @TargetApi(Build.VERSION_CODES.N)
    public static void setSystemLocale(Configuration config, Locale locale){
        config.setLocale(locale);
    }
}

并注入您的包装器,在每个活动中添加以下代码:

@Override
protected void attachBaseContext(Context newBase) {
    super.attachBaseContext(MyContextWrapper.wrap(newBase,"fr"));
}

更新 2020年12月22日 在 Android Material 库实现 ContextThemeWrapper 来支持暗模式后,语言设置会出现问题并且语言设置会丢失。经过数月的头痛,问题通过在 Activity 和 Fragment 的 onCreate 方法中添加以下代码得以解决。

Context context = MyContextWrapper.wrap(this/*in fragment use getContext() instead of this*/, "fr");
   getResources().updateConfiguration(context.getResources().getConfiguration(), context.getResources().getDisplayMetrics());

更新于2018年10月19日 有时在屏幕方向改变或活动暂停/恢复后,系统配置对象会重置为默认值,导致应用程序显示英文“en”文本,即使我们使用了法语“fr”语言环境包装上下文。因此,作为良好的实践,在活动或片段中永远不要保留Context/Activity对象的全局变量。

此外,请创建并在MyBaseFragment或MyBaseActivity中使用以下内容:

public Context getMyContext(){
    return MyContextWrapper.wrap(getContext(),"fr");
}

这种做法将为您提供100%无bug的解决方案。


7
我对这种方法有一个顾虑……目前仅应用于活动而非整个应用程序。对于可能不从活动开始的应用程序组件,例如服务,会发生什么? - rfgamaral
8
为什么要扩展ContextWrapper?你里面并没有任何内容,只有静态方法? - vladimir123
8
我需要将createConfigurationContext/updateConfiguration方法从if-else分支中移出,并添加到其下方,否则在第一个Activity中一切正常,但打开第二个Activity后,语言会更改回设备默认设置。我找不到原因。 - kroky
3
我已经添加了所需的代码行,并将其作为此Gist发布:https://gist.github.com/muhammad-naderi/0ff264e6cc07df904bc88a9f7efbe57d。 - Muhammad Naderi
2
我遇到了一个问题:在第二个活动中使用attachBaseContext后,语言环境无法再更改。解决方案:我删除了检查当前语言环境的条件。因此,get方法变得无用。这就是诀窍。 - Mahdi-Malv
显示剩余45条评论

46

可能是这样的:

Configuration overrideConfiguration = getBaseContext().getResources().getConfiguration();
overrideConfiguration.setLocales(LocaleList);
Context context  = createConfigurationContext(overrideConfiguration);
Resources resources = context.getResources();

奖励: 一篇使用createConfigurationContext()的博客文章


83
API 24+...可恶的谷歌,他们难道不能给我们提供一个简单的方式吗? - Ali Bdeir
@Ab_ 是的,谷歌已经为您提供了简单的方法setLocale,该方法可以追溯到API 17。如果您需要更早的版本,请参考Mourjan的完整解决方案。 - click_whir
7
“如果你只针对这些设备,那么它就很简单”的说法并不一定意味着这很简单。 - Vlad
1
@Vlad 如果您不需要支持2012年之前制造的设备,那么有一种简单的方法。欢迎来到应用程序开发! - click_whir
1
你从哪里获取到了 LocaleList - EdgeDev
显示剩余4条评论

22

我已经解决了这个问题,而不需要创建任何自定义的ContextWrapper

首先,我创建了一个扩展函数。

fun Context.setAppLocale(language: String): Context {
    val locale = Locale(language)
    Locale.setDefault(locale)
    val config = resources.configuration
    config.setLocale(locale)
    config.setLayoutDirection(locale)
    return createConfigurationContext(config)
}

然后在活动的attachBaseContext方法中,仅用新的上下文替换上下文即可。

override fun attachBaseContext(newBase: Context) {
  super.attachBaseContext(ContextWrapper(newBase.setAppLocale("bn")))
}

3
这应该是2022年最佳答案。谢谢。 - Md Imran Choudhury
如果您无法拥有基本活动,则必须在每个活动中执行此操作。 - S Haque
嗨。我很好奇。你是如何通知你的活动有关配置更改的?我的意思是,您使用该扩展方法获取具有新配置的新上下文实例,但您的活动如何获取该新上下文? - Aytaj
这个能和androidx.appcompat:appcompat:1.4.0一起使用吗? - Alex
@MdImranChoudhury在我的真机Android 11上无法工作,但在Android Studio上的API v.30上可以工作。您不知道可能是什么问题吗? - Alex
显示剩余2条评论

6

没有 100% 的有效解决方案。您需要同时使用 createConfigurationContextapplyOverrideConfiguration。否则,即使您在每个活动中替换了baseContext,活动仍将使用旧语言环境的 ContextThemeWrapper 中的 Resources

这是我的解决方案,在 API 29 及以下版本中可行:

从以下类中创建 MainApplication 子类:

abstract class LocalApplication : Application() {

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(
            base.toLangIfDiff(
                PreferenceManager
                    .getDefaultSharedPreferences(base)
                    .getString("langPref", "sys")!!
             )
        )
    }
}

此外,每个Activity从以下方面进行:
abstract class LocalActivity : AppCompatActivity() {

    override fun attachBaseContext(newBase: Context) {
        super.attachBaseContext(            
            PreferenceManager
                .getDefaultSharedPreferences(base)
                    .getString("langPref", "sys")!!
        )
    }

    override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
        super.applyOverrideConfiguration(baseContext.resources.configuration)
    }
}

新增以下扩展函数到 LocaleExt.kt:

const val SYSTEM_LANG = "sys"
const val ZH_LANG = "zh"
const val SIMPLIFIED_CHINESE_SUFFIX = "rCN"


private fun Context.isAppLangDiff(prefLang: String): Boolean {
    val appConfig: Configuration = this.resources.configuration
    val sysConfig: Configuration = Resources.getSystem().configuration

    val appLang: String = appConfig.localeCompat.language
    val sysLang: String = sysConfig.localeCompat.language

    return if (SYSTEM_LANG == prefLang) {
        appLang != sysLang
    } else {
        appLang != prefLang
                || ZH_LANG == prefLang
    }
}

fun Context.toLangIfDiff(lang: String): Context =
    if (this.isAppLangDiff(lang)) {
        this.toLang(lang)
    } else {
        this
    }

@Suppress("DEPRECATION")
fun Context.toLang(toLang: String): Context {
    val config = Configuration()

    val toLocale = langToLocale(toLang)

    Locale.setDefault(toLocale)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        config.setLocale(toLocale)

        val localeList = LocaleList(toLocale)
        LocaleList.setDefault(localeList)
        config.setLocales(localeList)
    } else {
        config.locale = toLocale
    }

    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        config.setLayoutDirection(toLocale)
        this.createConfigurationContext(config)
    } else {
        this.resources.updateConfiguration(config, this.resources.displayMetrics)
        this
    }
}

/**
 * @param toLang - two character representation of language, could be "sys" - which represents system's locale
 */
fun langToLocale(toLang: String): Locale =
    when {
        toLang == SYSTEM_LANG ->
            Resources.getSystem().configuration.localeCompat

        toLang.contains(ZH_LANG) -> when {
            toLang.contains(SIMPLIFIED_CHINESE_SUFFIX) ->
                Locale.SIMPLIFIED_CHINESE
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
                Locale(ZH_LANG, "Hant")
            else ->
                Locale.TRADITIONAL_CHINESE
        }

        else -> Locale(toLang)
    }

@Suppress("DEPRECATION")
private val Configuration.localeCompat: Locale
    get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        this.locales.get(0)
    } else {
        this.locale
    }

res/values/arrays.xml 中的数组里添加您所支持的语言:

<string-array name="lang_values" translatable="false">
    <item>sys</item> <!-- System default -->
    <item>ar</item>
    <item>de</item>
    <item>en</item>
    <item>es</item>
    <item>fa</item>
    ...
    <item>zh</item> <!-- Traditional Chinese -->
    <item>zh-rCN</item> <!-- Simplified Chinese -->
</string-array>

我想提到:

  • 当您使用RTL语言(如阿拉伯语、波斯语等)时,请使用config.setLayoutDirection(toLocale);更改布局方向。
  • 代码中的"sys"是一个值,表示"继承系统默认语言"。
  • 这里的"langPref"是偏好设置的键,您可以在其中放置用户当前的语言。
  • 如果上下文已经使用所需的区域设置,则无需重新创建上下文。
  • 无需使用此处发布的ContextWraper,只需将从createConfigurationContext返回的新上下文设置为baseContext即可。
  • 这非常重要!当您调用createConfigurationContext时,您应该传递从头开始创建的配置,仅设置Locale属性。不应将任何其他属性设置为此配置。因为如果我们为此配置设置了其他一些属性(例如方向),我们将永久覆盖该属性,即使我们旋转屏幕,我们的上下文也不会再次更改此方向属性。
  • recreate活动并不能满足用户选择不同语言的需求,因为applicationContext仍然保留旧的区域设置,可能会导致意外行为。因此,请监听偏好设置的更改并重新启动整个应用程序任务:

fun Context.recreateTask() {
    this.packageManager
        .getLaunchIntentForPackage(context.packageName)
        ?.let { intent ->
            val restartIntent = Intent.makeRestartActivityTask(intent.component)
            this.startActivity(restartIntent)
            Runtime.getRuntime().exit(0)
         }
}

这个不行。同时考虑为所有活动创建一个基础活动,而不是在每个活动中重复代码。此外,recreateTask(Context context) 方法无法正常工作,因为我仍然看到没有任何变化的布局。 - blueware
@blueware 我已经更新了示例。之前有一些错误,但现在应该可以正常工作了,这是我生产应用程序中的代码。函数recreateTask可能无法正常工作,你可以显示一个toast提示"语言将在重启后更改"。 - Oleksandr Albul

5

受到书法、Mourjan和我自己的启发,我创建了这个。

首先,您必须创建一个Application的子类:

public class MyApplication extends Application {
    private Locale locale = null;

    @Override
    public void onCreate() {
        super.onCreate();

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);

        Configuration config = getBaseContext().getResources().getConfiguration();

        String lang = preferences.getString(getString(R.string.pref_locale), "en");
        String systemLocale = getSystemLocale(config).getLanguage();
        if (!"".equals(lang) && !systemLocale.equals(lang)) {
            locale = new Locale(lang);
            Locale.setDefault(locale);
            setSystemLocale(config, locale);
            updateConfiguration(config);
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (locale != null) {
            setSystemLocale(newConfig, locale);
            Locale.setDefault(locale);
            updateConfiguration(newConfig);
        }
    }

    @SuppressWarnings("deprecation")
    private static Locale getSystemLocale(Configuration config) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return config.getLocales().get(0);
        } else {
            return config.locale;
        }
    }

    @SuppressWarnings("deprecation")
    private static void setSystemLocale(Configuration config, Locale locale) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            config.setLocale(locale);
        } else {
            config.locale = locale;
        }
    }

    @SuppressWarnings("deprecation")
    private void updateConfiguration(Configuration config) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            getBaseContext().createConfigurationContext(config);
        } else {
            getBaseContext().getResources().updateConfiguration(config, getBaseContext().getResources().getDisplayMetrics());
        }
    }
}

然后,您需要将此设置为AndroidManifest.xml应用程序标记:

<application
    ...
    android:name="path.to.your.package.MyApplication"
    >

并将以下内容添加到您的AndroidManifest.xml活动标签中。

<activity
    ...
    android:configChanges="locale"
    >

请注意,pref_locale是一个字符串资源,类似于这样:
<string name="pref_locale">fa</string>

如果没有设置pref_locale,那么硬编码的"en"就是默认语言。


这还不够,你还需要在每个活动中覆盖上下文。否则,你的 baseContext 可能会有一个语言环境,而你的应用程序可能会有另一个语言环境。结果就是你的用户界面会出现混合语言。请参阅我的答案。 - Oleksandr Albul

3
这是@bassel-mourjan的解决方案,加入了一些kotlin的好处 :):
import android.annotation.TargetApi
import android.content.ContextWrapper
import android.os.Build
import java.util.*

@Suppress("DEPRECATION")
fun ContextWrapper.wrap(language: String): ContextWrapper {
    val config = baseContext.resources.configuration
    val sysLocale: Locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        this.getSystemLocale()
    } else {
        this.getSystemLocaleLegacy()
    }

    if (!language.isEmpty() && sysLocale.language != language) {
        val locale = Locale(language)
        Locale.setDefault(locale)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            this.setSystemLocale(locale)
        } else {
            this.setSystemLocaleLegacy(locale)
        }
    }

    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        val context = baseContext.createConfigurationContext(config)
        ContextWrapper(context)
    } else {
        baseContext.resources.updateConfiguration(config, baseContext.resources.displayMetrics)
        ContextWrapper(baseContext)
    }

}

@Suppress("DEPRECATION")
fun ContextWrapper.getSystemLocaleLegacy(): Locale {
    val config = baseContext.resources.configuration
    return config.locale
}

@TargetApi(Build.VERSION_CODES.N)
fun ContextWrapper.getSystemLocale(): Locale {
    val config = baseContext.resources.configuration
    return config.locales[0]
}


@Suppress("DEPRECATION")
fun ContextWrapper.setSystemLocaleLegacy(locale: Locale) {
    val config = baseContext.resources.configuration
    config.locale = locale
}

@TargetApi(Build.VERSION_CODES.N)
fun ContextWrapper.setSystemLocale(locale: Locale) {
    val config = baseContext.resources.configuration
    config.setLocale(locale)
}

以下是如何使用它:

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(ContextWrapper(newBase).wrap(defaultLocale.language))
}

这行代码 val config = baseContext.resources.configuration 是非常错误的。由于这个原因,你将会遇到很多的 bug。你需要创建一个新的 configuration。请看我的回答。 - Oleksandr Albul

2
现在是2023年。我决定查看我用来更改主题的方法,并找到了这种更改语言的方法。你应该知道你有哪些语言。
AppCompatDelegate.setApplicationLocales(
    LocaleListCompat.create(Locale.forLanguageTag("ru"))
)

0

1
这个链接很有帮助,是一个不错的参考。我认为最好在这里包含实际答案,而不需要额外点击。 - Marshall Davis
你说得对,我刚刚接触Stack Overflow,我认为把答案的功劳归于自己是不正确的,所以我发布了原作者的链接。 - Thanasis Saxanidis

0
受到Bassel Mourjan(被接受的答案)的启发,我创建了自己的Kotlin静态函数:
fun Context.setCustomAppLanguage(language: String? = null) {
    val candidateLanguage = language ?: PreferenceManager.getDefaultSharedPreferences(this)
        .getString(LOCALE_KEY, Locale.ENGLISH.language)!!
    val config = resources.configuration
    if (candidateLanguage.isNotBlank() && config.locales[0].language != candidateLanguage) {
        val locale = Locale(candidateLanguage)
        Locale.setDefault(locale)
        config.setLocale(locale)
        val wrappedContext = ContextWrapper(createConfigurationContext(config))
        resources.updateConfiguration(
            wrappedContext.resources.configuration,
            wrappedContext.resources.displayMetrics
        )
    }
}

它可以即时更改语言,并适用于SDK 34。您需要为每个上下文调用它,所以我在基础片段(单一活动方法)和应用程序类中的applicationContext中执行它。执行函数后,您需要使UI无效 - 不要忘记这样做。

-1

试试这个:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.createConfigurationContext(config);

1
它只是创建了您需要切换上下文的活动到新的上下文中。 - Ali Karaca

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接