Android context.getResources.updateConfiguration () obsoleto

85

Recientemente context.getResources (). updateConfiguration () ha quedado obsoleto en la API de Android 25 y se recomienda utilizar el contexto. createConfigurationContext () en su lugar.

¿Alguien sabe cómo se puede usar createConfigurationContext para anular la configuración regional del sistema Android?

antes de que esto lo hiciera:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.getResources().updateConfiguration(config,
                                 context.getResources().getDisplayMetrics());
Bassel Mourjan
fuente
¿Qué tal applyOverrideConfiguration (no probado)?
user1480019
También hay una solución simple aquí, muy similar a esta stackoverflow.com/questions/39705739/…
Thanasis Saxanidis
[updateConfiguration quedó obsoleto en el nivel de API 25] developer.android.com/reference/android/content/res/Resources
Md Sifatul Islam

Respuestas:

122

Inspirado por la caligrafía , terminé creando un contenedor de contexto. En mi caso, necesito sobrescribir el idioma del sistema para brindar a los usuarios de mi aplicación la opción de cambiar el idioma de la aplicación, pero esto se puede personalizar con cualquier lógica que necesite 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);
    }
}

y para inyectar su contenedor, en cada actividad agregue el siguiente código:

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

ACTUALIZACIÓN 23/09/2020 En caso de anular el tema de la aplicación para aplicar el modo oscuro, por ejemplo, ContextThemeWrapper romperá la configuración de idioma, por lo tanto, agregue el siguiente código a su Actividad para restablecer la configuración regional deseada

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
      Locale locale = new Locale("fr");
      overrideConfiguration.setLocale(locale);
      super.applyOverrideConfiguration(overrideConfiguration);
}

ACTUALIZACIÓN 19/10/2018 A veces, después de un cambio de orientación, o una pausa / reanudación de la actividad, el objeto de configuración se restablece a la configuración predeterminada del sistema y, como resultado, veremos que la aplicación muestra el texto "en" en inglés a pesar de que envolvemos el contexto con la configuración regional "fr" en francés . Por lo tanto, y como buena práctica, nunca retenga el objeto Contexto / Actividad en una variable global en actividades o fragmentos.

además, cree y use lo siguiente en un MyBaseFragment o MyBaseActivity:

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

Esta práctica le proporcionará una solución 100% libre de errores.

Bassel Mourjan
fuente
5
Tengo una preocupación con este enfoque ... Actualmente se está aplicando solo a las actividades y no a toda la aplicación. ¿Qué pasará con los componentes de la aplicación que podrían no comenzar a partir de actividades, como los servicios?
rfgamaral
6
¿Por qué ampliaría ContextWrapper? ¿No tienes nada, solo métodos estáticos?
vladimir123
7
Tuve que sacar createConfigurationContext / updateConfiguration de la rama if-else y agregar debajo, de lo contrario, en la primera actividad todo estaba bien, pero cuando se abrió en segundo lugar, el idioma cambió de nuevo al predeterminado del dispositivo. No pude encontrar la razón.
kroky
3
Agregué
Muhammad Naderi
2
@kroky tiene razón. La configuración regional del sistema se ha cambiado correctamente, pero la configuración vuelve a la predeterminada. Como resultado, el archivo de recursos de cadenas vuelve al valor predeterminado. ¿Hay alguna otra forma, además de establecer la configuración cada vez en cada actividad
Yash Ladia
29

Probablemente así:

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

Bonificación: un artículo de blog que usa createConfigurationContext ()

compte14031879
fuente
Gracias por señalarme en la dirección correcta, supongo que eventualmente uno tendrá que crear un ContextWrapper y adjuntarlo a actividades como lo hace Calligraphy. De todos modos, el premio es suyo, pero no lo consideraré como una respuesta final hasta que publique la codificación correcta de la solución.
Bassel Mourjan
58
API 24 + ... Estúpido Google, ¿no pueden simplemente proporcionarnos una forma sencilla?
Ab
7
@click_whir Decir "Es simple si solo apuntas a estos dispositivos" no lo hace realmente simple.
Vlad
1
@Vlad Existe un método simple si no necesita admitir dispositivos fabricados antes de 2012. ¡Bienvenido al desarrollo de aplicaciones!
click_whir
1
de dónde lo conseguisteLocaleList
EdgeDev
4

Inspirado por Calligraphy, Mourjan y yo mismo, creé esto.

primero debe crear una subclase de Aplicación:

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

entonces necesitas configurar esto en tu etiqueta de aplicación AndroidManifest.xml:

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

y agregue esto a su etiqueta de actividad AndroidManifest.xml.

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

tenga en cuenta que pref_locale es un recurso de cadena como este:

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

y el código "en" es el idioma predeterminado si pref_locale no está configurado

Mahpooya
fuente
Esto no es suficiente, también necesita anular el contexto en cada actividad. Como terminará con la situación de que su baseContext tiene una configuración regional y su aplicación tendrá otra. Como resultado, obtendrá idiomas mixtos en su interfaz de usuario. Por favor vea mi respuesta.
Oleksandr Albul
3

Aquí no hay una solución que funcione al 100%. Necesita usar ambos createConfigurationContexty applyOverrideConfiguration. De lo contrario, incluso si se reemplaza baseContexten todas las actividades con la nueva configuración, la actividad seguiría utilizar Resourcesdesde ContextThemeWrapperla antigua configuración regional.

Así que aquí está la solución mía que funciona hasta API 29:

Subclasifica tu MainApplicationclase de:

abstract class LocalApplication : Application() {

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

También cada Activitydesde:

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

Agregar LocaleExt.ktcon las siguientes funciones de extensión:

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
    }

Agregue a sus res/values/arrays.xmlidiomas compatibles en 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>

Quiero mencionar:

  • Úselo config.setLayoutDirection(toLocale);para cambiar la dirección del diseño cuando use configuraciones regionales RTL como árabe, persa, etc.
  • "sys" en el código hay un valor que significa "heredar el idioma predeterminado del sistema".
  • Aquí, "langPref" es una clave de preferencia donde coloca el idioma actual del usuario.
  • No es necesario volver a crear el contexto si ya utiliza la configuración regional necesaria.
  • No hay necesidad de ContextWraperlo publicado aquí, solo configure el nuevo contexto devuelto createConfigurationContextcomo baseContext
  • ¡Esto es muy importante! Cuando llame createConfigurationContext, debe pasar la configuración creada desde cero y solo con el Localeconjunto de propiedades. No debería haber ninguna otra propiedad establecida en esta configuración. Porque si establecemos otras propiedades para esta configuración ( orientación, por ejemplo), anulamos esa propiedad para siempre, y nuestro contexto ya no cambia esta propiedad de orientación incluso si giramos la pantalla.
  • No es suficiente con la recreateactividad cuando el usuario selecciona un idioma diferente, porque applicationContext permanecerá con la configuración regional anterior y podría proporcionar un comportamiento inesperado. Así que escuche el cambio de preferencia y reinicie toda la tarea de la aplicación en su lugar:

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
fuente
Esto no funciona. También considere hacer una actividad base para todas las actividades en lugar de duplicar el código en cada actividad. Además, el recreateTask(Context context)método no funciona correctamente ya que todavía veo el diseño sin ningún cambio.
blueware
@blueware He actualizado las muestras. Hubo algunos errores anteriormente. Pero actualmente debería funcionar, este es el código de mi aplicación de producción. La divertida tarea recreateTask no pudo funcionar, podría mostrar un brindis como "El idioma se cambiará después de reiniciar" ..
Oleksandr Albul
1

Aquí está la solución de @ bassel-mourjan con un poco de bondad 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)
}

Y así es como se usa:

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(ContextWrapper(newBase).wrap(defaultLocale.language))
}
Miguel
fuente
Esta línea val config = baseContext.resources.configurationestá muy, muy mal. Terminará con muchos errores debido a esto. En su lugar, necesita crear una nueva configuración. Mira mi respuesta.
Oleksandr Albul
0

hay una solución simple con contextWrapper aquí: Android N cambia el idioma programáticamente Presta atención al método recreate ()

Thanasis Saxanidis
fuente
El enlace es útil y una buena referencia. Creo que es mejor incluir aquí la respuesta real en lugar de requerir un clic adicional.
ToothlessRebel
tienes razón, soy nuevo en stackoverflow y pensé que estaría mal tomar créditos por la respuesta, así que
publico
-1

Prueba esto:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.createConfigurationContext(config);
Eric B.
fuente
1
Simplemente crea la actividad que necesita para cambiar el contexto con uno nuevo
Ali Karaca