Android context.getResources.updateConfiguration () obsoleto

92

Recentemente, context.getResources (). updateConfiguration () tornou-se obsoleto na API 25 do Android e é aconselhável usar contexto. em vez disso, createConfigurationContext () .

Alguém sabe como createConfigurationContext pode ser usado para substituir a localidade do sistema Android?

antes que isso fosse feito por:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.getResources().updateConfiguration(config,
                                 context.getResources().getDisplayMetrics());
Bassel Mourjan
fonte
Que tal applyOverrideConfiguration (não testado)?
user1480019
há também uma solução simples aqui, muito semelhante a esta stackoverflow.com/questions/39705739/…
Thanasis Saxanidis
[updateConfiguration foi descontinuado na API de nível 25] developer.android.com/reference/android/content/res/Resources
Md Sifatul Islam

Respostas:

126

Inspirado na Caligrafia , acabei criando um wrapper de contexto. No meu caso, preciso substituir o idioma do sistema para fornecer aos usuários do meu aplicativo a opção de alterar o idioma do aplicativo, mas isso pode ser personalizado com qualquer lógica que você precise implementar.

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

e para injetar seu wrapper, em cada atividade adicione o seguinte código:

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

ATUALIZAÇÃO 22/12/2020 Após a implementação da biblioteca de materiais android do ContextThemeWrapper para suportar o modo escuro, a configuração de idioma seria interrompida e a configuração de idioma seria perdida. Após meses de coçar a cabeça, o problema foi resolvido adicionando o seguinte código ao método Activity and Fragment onCreate

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

ATUALIZAÇÃO 19/10/2018 Às vezes, após a mudança de orientação ou pausa / retomada da atividade, o objeto Configuração é redefinido para a configuração padrão do sistema e, como resultado, veremos o aplicativo exibindo o texto "en" em inglês, embora tenhamos agrupado o contexto com a localidade "fr" em francês . Portanto, como uma boa prática, nunca retenha o objeto Contexto / Atividade em uma variável global em atividades ou fragmentos.

além disso, crie e use o seguinte em um MyBaseFragment ou MyBaseActivity:

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

Esta prática irá fornecer a você uma solução 100% livre de bugs.

Bassel Mourjan
fonte
5
Tenho uma preocupação com essa abordagem ... Isso está sendo aplicado apenas a atividades e não a todo o aplicativo. O que acontecerá com os componentes do aplicativo que podem não iniciar a partir de atividades, como serviços?
rfgamaral
6
Por que você estenderia o ContextWrapper? Você não tem nada nele, apenas métodos estáticos?
vladimir123
7
Tive que tirar createConfigurationContext / updateConfiguration do branch if-else e adicionar abaixo dele, senão na primeira Activity estava tudo bem, mas quando aberto em segundo lugar, o idioma mudou de volta para o padrão do dispositivo. Não foi possível encontrar o motivo.
kroky
3
Eu adicionei a linha necessária e a postei
Muhammad Naderi
2
@kroky está certo. A localidade do sistema é alterada corretamente, mas a configuração volta ao padrão. Como resultado, o arquivo de recurso de strings volta ao padrão. Existe alguma outra maneira, além de definir a configuração sempre em todas as atividades
Yash Ladia
32

Provavelmente assim:

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

Bônus: um artigo de blog que usa createConfigurationContext ()

compte14031879
fonte
Obrigado por me apontar na direção certa, eu acho que eventualmente será necessário criar um ContextWrapper e anexá-lo às atividades como se fosse feito pela Caligrafia. De qualquer forma, o prêmio é seu, mas não o considerarei como uma resposta final até que eu poste a codificação correta da solução alternativa.
Bassel Mourjan
59
API 24 + ... Google estúpido, eles não podem apenas nos fornecer uma maneira simples?
Ali Bdeir
7
@click_whir Dizer "É simples se você direcionar apenas esses dispositivos" não simplifica realmente.
Vlad
1
@Vlad Existe um método simples se você não precisa oferecer suporte a dispositivos que foram feitos antes de 2012. Bem-vindo ao desenvolvimento de aplicativos!
click_whir 01 de
1
de onde você tirouLocaleList
EdgeDev
4

Inspirado por Calligraphy & Mourjan e por mim, eu criei isso.

primeiro você deve criar uma subclasse de 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());
        }
    }
}

então você precisa definir isso para sua tag de aplicativo AndroidManifest.xml:

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

e adicione-o à sua tag de atividade AndroidManifest.xml.

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

observe que pref_locale é um recurso de string como este:

<string name="pref_locale">fa</string>

e hardcode "en" é o idioma padrão se pref_locale não estiver definido

Mahpooya
fonte
Isso não é suficiente, você também precisa substituir o contexto em todas as atividades. Como você vai acabar tendo uma situação em que seu baseContext tem um locale seu aplicativo terá outro. Como resultado, você terá idiomas mistos em sua interface do usuário. Por favor, veja minha resposta.
Oleksandr Albul
3

Não há solução 100% funcional. Você precisa usar createConfigurationContexte applyOverrideConfiguration. Caso contrário, mesmo se você substituir baseContextem todas as atividades por uma nova configuração, a atividade ainda será usada a Resourcespartir do ContextThemeWrapperantigo local.

Então aqui está minha solução que funciona até a API 29:

Subclasse sua MainApplicationclasse de:

abstract class LocalApplication : Application() {

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

Também todos Activityde:

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

Adicionar LocaleExt.ktcom as próximas funções de extensão:

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
    }

Adicione aos seus res/values/arrays.xmlidiomas suportados na matriz:

<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>

Eu quero mencionar:

  • Use config.setLayoutDirection(toLocale);para alterar a direção do layout ao usar localidades RTL como árabe, persa, etc.
  • "sys" no código está um valor que significa "herdar o idioma padrão do sistema".
  • Aqui, "langPref" é uma chave de preferência onde você coloca o idioma atual do usuário.
  • Não há necessidade de recriar o contexto se ele já usa a localidade necessária.
  • Não há necessidade de ContextWrapercomo postado aqui, basta definir o novo contexto retornado de createConfigurationContextcomo baseContext
  • Isto é muito importante! Ao ligar, createConfigurationContextvocê deve passar a configuração criada do zero e apenas com a Localepropriedade definida. Não deve haver nenhuma outra propriedade definida para esta configuração. Porque se definirmos algumas outras propriedades para esta configuração ( orientação, por exemplo), substituiremos essa propriedade para sempre, e nosso contexto não mudará mais esta propriedade de orientação , mesmo se girarmos a tela.
  • Não é suficiente apenas recreateatuar quando o usuário seleciona um idioma diferente, porque applicationContext permanecerá com a localidade antiga e pode fornecer um comportamento inesperado. Portanto, ouça a mudança de preferência e reinicie toda a tarefa do aplicativo:

fun Context.recreateTask() {
    this.packageManager
        .getLaunchIntentForPackage(context.packageName)
        ?.let { intent ->
            val restartIntent = Intent.makeRestartActivityTask(intent.component)
            this.startActivity(restartIntent)
            Runtime.getRuntime().exit(0)
         }
}
Oleksandr Albul
fonte
Isso não funciona. Considere também fazer uma atividade base para todas as atividades, em vez de duplicar o código em todas as atividades. Além disso, o recreateTask(Context context)método não está funcionando corretamente, pois ainda vejo o layout sem nenhuma alteração.
blueware
@blueware Eu atualizei os exemplos. Houve alguns bugs anteriormente. Mas, atualmente, deve funcionar, este é o código do meu aplicativo de produção. A diversão recreateTask não funcionou. Você pode exibir um brinde como "O idioma será alterado após a reinicialização".
Oleksandr Albul
1

Aqui está a solução de @ bassel-mourjan com um pouco de bondade 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)
}

E aqui está como você o usa:

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(ContextWrapper(newBase).wrap(defaultLocale.language))
}
Michael
fonte
Esta linha val config = baseContext.resources.configurationestá muito errada. Você vai acabar com muitos bugs por causa disso. Em vez disso, você precisa criar uma nova configuração. Veja minha resposta.
Oleksandr Albul
0

há uma solução simples com contextWrapper aqui: Android N altera a linguagem programaticamente Preste atenção ao método recreate ()

Thanasis Saxanidis
fonte
O link é útil e uma boa referência. Acredito que seja melhor incluir a resposta real aqui, em vez de exigir um clique adicional.
ToothlessRebel
você está certo, eu sou apenas novo no stackoverflow e pensei que seria errado receber os créditos pela resposta, então postarei o link do autor original
Thanasis Saxanidis
-1

Experimente isto:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.createConfigurationContext(config);
Eric B.
fonte
1
Ele apenas cria a atividade necessária para alternar o contexto com um novo
Ali Karaca