Android context.getResources.updateConfiguration () устарел

92

Совсем недавно context.getResources (). updateConfiguration () устарел в Android API 25, и рекомендуется использовать context. createConfigurationContext () вместо этого.

Кто-нибудь знает, как createConfigurationContext можно использовать для переопределения языкового стандарта системы Android?

прежде, чем это будет сделано:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.getResources().updateConfiguration(config,
                                 context.getResources().getDisplayMetrics());
Бассель Мурджан
источник
Как насчет applyOverrideConfiguration (непроверено)?
user1480019 06
здесь также есть простое решение, очень похожее на это stackoverflow.com/questions/39705739/…
Thanasis Saxanidis
[updateConfiguration устарела на уровне API 25] developer.android.com/reference/android/content/res/Resources
Md Sifatul Islam

Ответы:

126

Вдохновленный каллиграфией , я создал оболочку контекста. В моем случае мне нужно перезаписать системный язык, чтобы предоставить пользователям моего приложения возможность изменить язык приложения, но это можно настроить с помощью любой логики, которую вам нужно реализовать.

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

ОБНОВЛЕНИЕ 22/12/2020 После реализации библиотеки материалов Android 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());

ОБНОВЛЕНИЕ 19.10.2018 Иногда после изменения ориентации или приостановки / возобновления активности объект конфигурации сбрасывается до конфигурации системы по умолчанию, и в результате мы видим, что приложение отображает английский текст «en», даже если мы обернули контекст французской локалью «fr» . Поэтому рекомендуется никогда не сохранять объект Context / Activity в глобальной переменной действий или фрагментов.

кроме того, создайте и используйте следующее в MyBaseFragment или MyBaseActivity:

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

Эта практика предоставит вам 100% -ное решение без ошибок.

Бассель Мурджан
источник
5
У меня есть одна проблема с этим подходом ... В настоящее время он применяется только к действиям, а не ко всему приложению. Что произойдет с компонентами приложения, которые могут не запускаться из действий, например служб?
rfgamaral 05
6
Зачем вам расширять ContextWrapper? В нем ничего нет, только статические методы?
vladimir123
7
Мне пришлось вынуть createConfigurationContext / updateConfiguration из ветки if-else и добавить под ним, иначе в первом Activity все было в порядке, но при открытии во втором язык снова изменился на язык по умолчанию. Не удалось найти причину.
kroky
3
Я добавил нужную строку и опубликовал ее в следующем виде: gist.github.com/muhammad-naderi/…
Мухаммад Надери
2
@kroky прав. Системный языковой стандарт изменен правильно, но конфигурация возвращается к значениям по умолчанию. В результате файл ресурсов строк возвращается к значениям по умолчанию. Есть ли другой способ, кроме настройки конфигурации каждый раз в каждом
Яш
32

Наверное так:

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

Бонус: статья в блоге, использующая createConfigurationContext ()

compte14031879
источник
Спасибо за то, что указали мне правильное направление, я полагаю, что в конечном итоге нужно будет создать ContextWrapper и прикрепить его к действиям, как это делает Calligraphy. В любом случае награда принадлежит вам, но я не буду рассматривать ее как окончательный ответ до тех пор, пока я не опубликую правильную кодировку обходного пути.
Bassel Mourjan
59
API 24 + ... Глупый гугл, разве они не могут просто предоставить нам простой способ?
Али Бдейр
7
@click_whir Сказать «Все просто, если вы нацеливаетесь только на эти устройства» на самом деле не упрощает.
Влад
1
@Vlad Есть простой способ, если вам не нужна поддержка устройств, выпущенных до 2012 года. Добро пожаловать в разработку приложений!
click_whir 01
1
откуда вы LocaleList
взяли
4

Я создал это, вдохновленный каллиграфией, Мурджаном и мной.

сначала вы должны создать подкласс 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>

а жесткий код "en" - это язык по умолчанию, если pref_locale не установлен

Махпуя
источник
Этого недостаточно, вам также необходимо переопределить контекст в каждом действии. Поскольку в конечном итоге у вашего baseContext будет одна локаль, а у вашего приложения - другая. В результате в вашем пользовательском интерфейсе будут смешанные языки. Пожалуйста, посмотрите мой ответ.
Александр Албул
3

Здесь нет 100% рабочего решения. Вам нужно использовать оба createConfigurationContextи applyOverrideConfiguration. В противном случае, даже если вы замените baseContextкаждое действие новой конфигурацией, активность все равно будет использоваться ResourcesизContextThemeWrapper со старой местностью.

Итак, вот мое решение, которое работает до 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)
         }
}
Александр Албул
источник
Это не работает. Также рассмотрите возможность создания базового действия для всех действий вместо дублирования кода в каждом действии. Кроме того, recreateTask(Context context)метод не работает должным образом, поскольку я все еще вижу макет без каких-либо изменений.
blueware
@blueware Я обновил образцы. Ранее были некоторые ошибки. Но в настоящее время он должен работать, это код из моего производственного приложения. Забавная игра RecreateTask не могла работать, вы могли отображать тост типа «Язык будет изменен после перезапуска» ..
Александр Албул,
1

Вот решение @ bassel-mourjan с небольшим количеством доброты котлина :):

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очень-очень неправильная. Из-за этого у вас будет много ошибок. Вместо этого вам нужно создать новую конфигурацию. Смотрите мой ответ.
Александр Албул
0

здесь есть простое решение с contextWrapper: Android N изменяет язык программно. Обратите внимание на метод reconate ()

Танасис Саксанидис
источник
Ссылка полезная и хорошая. Я считаю, что лучше включить здесь фактический ответ, чем требовать дополнительного щелчка.
ToothlessRebel
вы правы, я новичок в stackoverflow, и я подумал, что было бы неправильно брать кредиты за ответ, поэтому я
публикую
-1

Попробуй это:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.createConfigurationContext(config);
Эрик Б.
источник
1
Это просто создает действие, необходимое для переключения контекста на новый
Али Караджа