Android N меняет язык программно
Я обнаружил действительно странную ошибку, которая воспроизводится только на устройствах 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()
звоните), но во всех следующих действиях все строковые ресурсы неверны. Поворот экрана исправляет это. Что я могу сделать с этой проблемой? Должен ли я изменить локаль для Android N по-другому, или это просто системная ошибка?
PS Вот что я нашел. При первом запуске MainActivity (после моего тура) Locale.getDefault()
это правильно, но ресурсы не так. Но в других действиях это дает мне неправильную локаль и неправильные ресурсы из этой локали. После поворота экрана (или, возможно, другого изменения конфигурации) Locale.getDefault()
верно.
10 ответов
Хорошо. Наконец мне удалось найти решение.
Сначала вы должны знать, что в 25 API Resources.updateConfiguration(...)
устарела. Так что вместо этого вы можете сделать что-то вроде этого:
1) Вам нужно создать свой собственный ContextWrapper, который будет переопределять все параметры конфигурации в baseContext. Например, это мой ContextWrapper, который корректно меняет локаль. Обратите внимание на 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);
}
Замечания:
Не забудьте воссоздать свою деятельность, если вы хотите где-то изменить язык в вашем приложении. Вы можете переопределить любую конфигурацию, которую хотите, используя это решение.
Вдохновленный различными кодами (т.е. нашими братьями из Stackru (кричите, ребята)), я создал гораздо более простую версию. ContextWrapper
расширение не нужно.
Во-первых, скажем, у вас есть 2 кнопки для 2 языков, EN и KH. В onClick для кнопок сохранить код языка в 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();
}
Затем создайте статический метод, который возвращает ContextWrapper
возможно в классе Utils (потому что это то, что я сделал, lul).
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
,
Пример:
public class LangSupportBaseActivity extends Activity{
...blab blab blab so on and so forth lines of neccessary code
@Override
protected void attachBaseContext(Context newBase) {
String lang_code = "en"; //load it from SharedPref
Context context = Utils.changeLang(newBase, lang_code);
super.attachBaseContext(context);
}
}
public class HomeActivity extends LangSupportBaseActivity{
...blab blab blab
}
Начиная с Android 7.0+, некоторые части моего приложения больше не меняли свой язык. Даже с новыми методами, предложенными выше. Мне помогло обновление контекста приложения и активности. Вот пример Kotlin переопределений подкласса Activity:
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 устарел, но в любом случае createConfigurationContext для каждого Activity оставил некоторые строки без изменений.
Программно изменить языковой стандарт в приложении для Android довольно сложно. Я потратил много времени, чтобы найти рабочее решение, которое сейчас работает в продакшене.
Вам нужно переопределить контекст в каждом Activity
но и в вашем Application
class, иначе вы получите смешанные языки в 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
набор свойств. Для этой конфигурации не должно быть никаких других свойств. Потому что, если мы установим некоторые другие свойства для этой конфигурации (например,ориентацию), мы переопределим это свойство навсегда, и наш контекст больше не изменит это свойство ориентации, даже если мы повернем экран. - Недостаточно только
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)
}
}
Это мой код, и он работает! Пожалуйста, дайте мне знать, если возникнут проблемы:
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);
}
Приведенные выше ответы поставили меня на правильный путь, но оставили пару вопросов
- На Android 7 и 9 я мог бы с радостью перейти на любой язык, кроме приложения по умолчанию. Когда я переключился обратно на язык приложения по умолчанию, он показывал последний выбранный язык - не удивительно, так как это переопределило значение по умолчанию (хотя интересно, что это не проблема для Android 8!).
- Для языков 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, я расширил 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);
}
}
}
Это работает для меня, я использую 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)
}
ОБНОВЛЕНИЕ СЕНТЯБРЬ 2020
Для последней версии Androidx Appcombat Stable 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.0Combat
В моем случае Xamarin.Android:
Создайте контекстную оболочку:
public class LanguageContextWrapper : Android.Content.ContextWrapper
{
public LanguageContextWrapper(Context @base) : base(@base)
{
}
public static ContextWrapper Wrap(Context context, string newLocale)
{
Locale.Default = new Locale(newLocale);
Configuration config = new Configuration();
config.SetLocale(Locale.Default);
context = context.CreateConfigurationContext(config);
return new ContextWrapper(context);
}
}
и использовать во всех действиях:
protected override void AttachBaseContext(Context newBase)
{
Context context = LanguageContextWrapper.Wrap(newBase, "en"); //need use short name of locale language
base.AttachBaseContext(context);
}
А работу в андроиде 10,11,12 я ниже не проверял.
ОБНОВЛЕНИЕ НОЯБРЬ 2020
Всем привет, просто хочу поделиться своим опытом. Пару дней назад я начал получать сообщения о проблеме на устройствах Android N, когда язык не меняется в настройках моего приложения. Я много искал и, попробовав несколько изменений в своем коде, я узнал, что в моем коде не было проблем, и проблема была вызвана из-за ограничения androidx. Layout gradle dependency version 2.0.0 и после понижения до 1.1.3, язык проблема решена. Я решил проблему с помощью этой версии библиотеки ConstraintLayout.
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'