Android N以编程方式更改语言

时间:2016-09-26 14:39:19

标签: java android android-7.0-nougat

我发现非常奇怪的错误仅在Android N设备上重现。

在浏览我的应用程序时,有可能改变语言。这是改变它的代码。

 public void update(Locale locale) {

    Locale.setDefault(locale);

    Configuration configuration = res.getConfiguration();

    if (BuildUtils.isAtLeast24Api()) {
        LocaleList localeList = new LocaleList(locale);

        LocaleList.setDefault(localeList);
        configuration.setLocales(localeList);
        configuration.setLocale(locale);

    } else if (BuildUtils.isAtLeast17Api()){
        configuration.setLocale(locale);

    } else {
        configuration.locale = locale;
    }

    res.updateConfiguration(configuration, res.getDisplayMetrics());
}

此代码在我的巡演活动中非常有效(使用recreate()调用),但在所有下一个活动中,所有String资源都是错误的。屏幕旋转修复它。我该怎么办这个问题?我应该以不同方式更改Android N的区域设置,还是只是系统错误?

P.S。这是我发现的。首次启动MainActivity(在我的巡演之后)Locale.getDefault()是正确的,但资源是错误的。但在其他活动中,它给了我错误的Locale和来自此语言环境的错误资源。旋转屏幕(或者可能是其他一些配置更改)Locale.getDefault()是正确的。

9 个答案:

答案 0 :(得分:86)

确定。最后我设法找到了解决方案。

首先,您应该知道在25 API Resources.updateConfiguration(...)中已弃用。所以你可以这样做:

1)您需要创建自己的ContextWrapper,它将覆盖baseContext中的所有配置参数。例如,这是我的ContextWrapper,可以正确地更改Locale。注意context.createConfigurationContext(configuration)方法。

public class ContextWrapper extends android.content.ContextWrapper {

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

public static ContextWrapper wrap(Context context, Locale newLocale) {

    Resources res = context.getResources();
    Configuration configuration = res.getConfiguration();

    if (BuildUtils.isAtLeast24Api()) {
        configuration.setLocale(newLocale);

        LocaleList localeList = new LocaleList(newLocale);
        LocaleList.setDefault(localeList);
        configuration.setLocales(localeList);

        context = context.createConfigurationContext(configuration);

    } else if (BuildUtils.isAtLeast17Api()) {
        configuration.setLocale(newLocale);
        context = context.createConfigurationContext(configuration);

    } else {
        configuration.locale = newLocale;
        res.updateConfiguration(configuration, res.getDisplayMetrics());
    }

    return new ContextWrapper(context);
}}

2)以下是您在BaseActivity中应该做的事情:

  @Override
protected void attachBaseContext(Context newBase) {

    Locale newLocale;
    // .. create or get your new Locale object here.

    Context context = ContextWrapper.wrap(newBase, newLocale);
    super.attachBaseContext(context);
}

注意:

  

如果要更改区域设置,请记住重新创建活动   你的应用程序在某处。您可以覆盖所需的任何配置   这个解决方案。

答案 1 :(得分:16)

受到各种代码的启发(即:我们的Stackoverflow团队(大声喊叫)),我制作了一个更简单的版本。 <{1}}扩展名是不必要的。

首先,我们假设您有两个语言的按钮,EN和KH。在按钮的onClick中将语言代码保存到ContextWrapper,然后调用活动SharedPreferences方法。

示例:

recreate()

然后创建一个返回@Override public void onClick(View v) { switch(v.getId()) { case R.id.btn_lang_en: //save "en" to SharedPref here break; case R.id.btn_lang_kh: //save "kh" to SharedPref here break; default: break; } getActivity().recreate(); } 的静态方法,也许是在Utils类中(因为我做了什么,lul)。

ContextWrapper

最后,在 ALL ACTIVITY&#39; S public static ContextWrapper changeLang(Context context, String lang_code){ Locale sysLocale; Resources rs = context.getResources(); Configuration config = rs.getConfiguration(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { sysLocale = config.getLocales().get(0); } else { sysLocale = config.locale; } if (!lang_code.equals("") && !sysLocale.getLanguage().equals(lang_code)) { Locale locale = new Locale(lang_code); Locale.setDefault(locale); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { config.setLocale(locale); } else { config.locale = locale; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { context = context.createConfigurationContext(config); } else { context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics()); } } return new ContextWrapper(context); } 方法中加载SharedPreferences的语言代码。

attachBaseContext(Context newBase)

奖励:为了节省键盘上的手掌汗,我创建了一个@Override protected void attachBaseContext(Context newBase) { String lang_code = "en"; //load it from SharedPref Context context = Utils.changeLang(newBase, lang_code); super.attachBaseContext(context); } 类,扩展了LangSupportBaseActivity并使用那里的最后一块代码。我所有其他活动都延伸Activity

示例:

LangSupportBaseActivity

答案 2 :(得分:3)

自Android 7.0以来,我的应用程序的某些部分不再更改其语言。即使使用上面提出的新方法。应用程序和活动上下文的更新对我有所帮助。这是Activity子类覆盖的Kotlin示例:

private fun setApplicationLanguage(newLanguage: String) {
    val activityRes = resources
    val activityConf = activityRes.configuration
    val newLocale = Locale(newLanguage)
    activityConf.setLocale(newLocale)
    activityRes.updateConfiguration(activityConf, activityRes.displayMetrics)

    val applicationRes = applicationContext.resources
    val applicationConf = applicationRes.configuration
    applicationConf.setLocale(newLocale)
    applicationRes.updateConfiguration(applicationConf,
            applicationRes.displayMetrics)
}

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(newBase)

    setApplicationLanguage("fa");
}

注意:不建议使用updateConfiguration,但是无论如何,对于每个Activity,createConfigurationContext都保留了一些字符串。

答案 3 :(得分:3)

在Android应用中以编程方式更改语言环境是很痛苦的。我花了很多时间找到目前可以在生产环境中工作的解决方案。

您需要覆盖每个Activity中的上下文,也需要覆盖Application类中的上下文,否则最终会在ui中使用混合语言。

这是适用于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>

这是关键点:

  • 使用config.setLayoutDirection(toLocale);更改布局方向 当您使用阿拉伯语,波斯语等RTL语言环境时。
  • 代码中的
  • "sys"是一个值,表示“继承系统默认语言”。
  • “ langPref”是您输入用户当前语言的首选键。
  • 如果已经使用了上下文,则无需重新创建上下文 语言环境。
  • 这里没有必要ContextWraper,只需将createConfigurationContext返回的新上下文设置为baseContext
  • 这很重要!调用createConfigurationContext时,您应该通过配置从头开始创建,并且仅设置了Locale属性。不应为此配置设置任何其他属性。因为如果我们为此配置设置其他一些属性(例如 orientation ),我们将永远覆盖该属性,即使我们旋转了该属性,我们的上下文也不再更改此 orientation 属性。屏幕。
  • 仅当用户选择其他语言时,仅进行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)
         }
}

答案 4 :(得分:2)

这是我的代码,可以正常工作! 如果有问题,请告诉我

protected void attachBaseContext(Context newBase) {
    String lang = "en"; // your language or load from SharedPref
    Locale locale = new Locale(lang);
    Configuration config = new Configuration(newBase.getResources().getConfiguration());
    Locale.setDefault(locale);
    config.setLocale(locale);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        newBase = newBase.createConfigurationContext(config);
    } else {
        newBase.getResources().updateConfiguration(config, newBase.getResources().getDisplayMetrics());
    }
    super.attachBaseContext(newBase);
}

答案 5 :(得分:1)

这对我有用,我正在使用 androidx.appcompat:appcompat:1.2.0

 override fun attachBaseContext(newBase: Context?) {
            val sp = PreferenceManager.getDefaultSharedPreferences(newBase)
            val locale = when(sp.getString("app_language", "")) {
                "en" -> { Locale("en") }
                "hu" -> { Locale("hu") }
                else -> {
                    if (Build.VERSION.SDK_INT >= 24) {
                        Resources.getSystem().configuration.locales.get(0);
                    }
                    else {
                        Resources.getSystem().configuration.locale
                    }
                }
            }
            if(newBase != null) {
                Locale.setDefault(locale)
                newBase.resources.configuration.setLocale(locale)
                applyOverrideConfiguration(newBase.resources.configuration)
            }
            super.attachBaseContext(newBase)
        }

答案 6 :(得分:0)

以上答案使我走上了正确的轨道,但留下了一些问题

  1. 在android 7和android 9上,我可以愉快地更改为除应用默认设置以外的任何语言。当我改回应用程序默认语言时,它显示了最后选择的语言-不足为奇,因为它已经覆盖了默认语言(尽管有趣的是,这在Android 8上不是问题!)。
  2. 对于RTL语言,它没有将布局更新为RTL

要解决第一个问题,我在应用启动时存储了默认语言环境。

注意:如果您的默认语言设置为“ en”,则“ enGB”或“ enUS”的语言环境都需要匹配默认语言环境(除非您为其提供了单独的语言环境)。同样,在下面的示例中,如果用户的电话区域设置为arLY(阿拉伯利比亚),则defLanguage必须为“ ar”而不是“ arLY”

private Locale defLocale = Locale.getDefault();
private Locale locale = Locale.getDefault();
public static myApplication myApp;
public static Resources res;
private static String defLanguage = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
private static sLanguage = "en";
private static final Set<String> SUPPORTEDLANGUAGES = new HashSet<>(Arrays.asList(new String[]{"en", "ar", "arEG"})); 

@Override
protected void attachBaseContext(Context base) {
  if (myApp == null) myApp = this;
  if (base == null) super.attachBaseContext(this);
  else super.attachBaseContext(setLocale(base));
}

@Override
public void onCreate() {
  myApp = this;

  if (!SUPPORTEDLANGUAGES.contains(test)) {
    // The default locale (eg enUS) is not in the supported list - lets see if the language is
    if (SUPPORTEDLANGUAGES.contains(defLanguage.substring(0,2))) {
      defLanguage = defLanguage.substring(0,2);
    }
  }
}

private static void setLanguage(String sLang) {
  Configuration baseCfg = myApp.getBaseContext().getResources().getConfiguration();
  if ( sLang.length() > 2 ) {
    String s[] = sLang.split("_");
    myApp.locale = new Locale(s[0],s[1]);
    sLanguage = s[0] + s[1];
  }
  else {
    myApp.locale = new Locale(sLang);
    sLanguage = sLang;
  }
}

public static Context setLocale(Context ctx) {
  Locale.setDefault(myApp.locale);
  Resources tempRes = ctx.getResources();
  Configuration config = tempRes.getConfiguration();

  if (Build.VERSION.SDK_INT >= 24) {
    // If changing to the app default language, set locale to the default locale
    if (sLanguage.equals(myApp.defLanguage)) {
      config.setLocale(myApp.defLocale);
      // restored the default locale as well
      Locale.setDefault(myApp.defLocale);
    }
    else config.setLocale(myApp.locale);

    ctx = ctx.createConfigurationContext(config);

    // update the resources object to point to the current localisation
    res = ctx.getResources();
  } else {
    config.locale = myApp.locale;
    tempRes.updateConfiguration(config, tempRes.getDisplayMetrics());
  }

  return ctx;
}

为解决RTL问题,我根据answer

中的Fragments注释扩展了AppCompatActivity。
public class myCompatActivity extends AppCompatActivity {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(myApplication.setLocale(base));
  }

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (Build.VERSION.SDK_INT >= 17) {
      getWindow().getDecorView().setLayoutDirection(myApplication.isRTL() ?
              View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
    }
  }
}

答案 7 :(得分:0)

UPDATE SEP 2020

对于最新的Androidx Appcombat稳定版1.2.0,请删除1.1.0的所有替代方法并添加

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")
}
}

将您的语言环境逻辑添加到函数包装中(您可以在上面接受的答案中添加ContextWrapper)。此类必须在androidx.appcompat.app包内,因为唯一现有的AppCompatDelegate构造函数是包私有

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")
}

就是这样。您最好使用最新的1.2.0 appCombat

答案 8 :(得分:0)

更新2020年11月

大家好,我只想分享我的经验。几天前,我开始收到有关Android N设备中的语言未从我的应用程序设置更改不变的问题的报告。我进行了大量搜索,尝试对代码进行多次更改后,发现它们在我的代码中没有问题,并且问题是由于androidx约束Layout gradle依赖版本 2.0.0 以及降级后导致更改为 1.1.3 ,语言问题已解决。 使用此版本的ConstraintLayout库可以解决我的问题。

implementation 'androidx.constraintlayout:constraintlayout:1.1.3'