System Android context.getResources.updateConfiguration () jest przestarzały

86

Niedawno context.getResources (). updateConfiguration () jest przestarzała w Android API 25 i zaleca się używanie kontekstu. Zamiast tego createConfigurationContext () .

Czy ktoś wie, jak można użyć createConfigurationContext do zastąpienia ustawień regionalnych systemu Android?

zanim zostanie to zrobione przez:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.getResources().updateConfiguration(config,
                                 context.getResources().getDisplayMetrics());
Bassel Mourjan
źródło
A co z applyOverrideConfiguration (nieprzetestowane)?
user1480019
jest tu też proste rozwiązanie, bardzo podobne do tego stackoverflow.com/questions/39705739/…
Thanasis Saxanidis
[updateConfiguration został wycofany na poziomie API 25] developer.android.com/reference/android/content/res/Resources
Md Sifatul Islam

Odpowiedzi:

122

Zainspirowany kaligrafią , w końcu stworzyłem opakowanie kontekstowe. W moim przypadku muszę nadpisać język systemu, aby zapewnić użytkownikom mojej aplikacji możliwość zmiany języka aplikacji, ale można to dostosować za pomocą dowolnej logiki, którą trzeba wdrożyć.

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

i aby wstrzyknąć opakowanie, w każdym działaniu dodaj następujący kod:

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

AKTUALIZACJA 23/09/2020 W przypadku nadpisania motywu aplikacji, aby na przykład zastosować tryb ciemny, ContextThemeWrapper złamie ustawienie języka, dlatego dodaj następujący kod do swojej aktywności, aby zresetować żądane ustawienia regionalne

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

AKTUALIZACJA 19/10/2018 Czasami po zmianie orientacji lub wstrzymaniu / wznowieniu działania obiekt konfiguracyjny resetuje się do domyślnej konfiguracji systemu, w wyniku czego aplikacja wyświetla angielski tekst „en”, mimo że kontekst został opakowany francuskim ustawieniem „fr” . Dlatego i jako dobrą praktykę, nigdy nie przechowuj obiektu Context / Activity w zmiennej globalnej w działaniach lub fragmentach.

ponadto utwórz i użyj następujących elementów w MyBaseFragment lub MyBaseActivity:

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

Ta praktyka zapewni Ci w 100% wolne od błędów rozwiązanie.

Bassel Mourjan
źródło
5
Mam jeden problem z tym podejściem ... Obecnie dotyczy to tylko działań, a nie całej aplikacji. Co się stanie z komponentami aplikacji, które mogą nie rozpoczynać się od działań, takich jak usługi?
rfgamaral
6
Dlaczego miałbyś rozszerzyć ContextWrapper? Nie masz w tym nic, tylko metody statyczne?
vladimir123
7
Musiałem wyjąć createConfigurationContext / updateConfiguration z gałęzi if-else i dodać poniżej, w przeciwnym razie w pierwszym działaniu wszystko było w porządku, ale po otwarciu jako drugi język zmienił się z powrotem na domyślny dla urządzenia. Nie mogłem znaleźć powodu.
kroky
3
Dodałem potrzebną linię i umieściłem
Muhammad Naderi
2
@kroky ma rację. Ustawienia regionalne systemu zostały poprawnie zmienione, ale konfiguracja wraca do ustawień domyślnych. W rezultacie plik zasobów ciągów wraca do wartości domyślnych. Czy jest jakiś inny sposób niż ustawianie konfiguracji za każdym razem w każdej czynności
Yash Ladia
29

Prawdopodobnie tak:

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

Bonus: artykuł na blogu, który używa funkcji createConfigurationContext ()

compte14031879
źródło
Dziękuję za wskazanie mi właściwego kierunku, myślę, że w końcu trzeba będzie stworzyć ContextWrapper i dołączyć go do działań, tak jak robi to Calligraphy. W każdym razie nagroda należy do Ciebie, ale nie uznam jej za ostateczną odpowiedź, dopóki nie opublikuję odpowiedniego kodu obejścia.
Bassel Mourjan
58
API 24 + ... Głupi Google, czy nie mogą nam po prostu zapewnić prostego sposobu?
Ali Bdeir
7
@click_whir Powiedzenie „To proste, jeśli kierujesz reklamy tylko na te urządzenia”, wcale tego nie ułatwia.
Vlad
1
@Vlad Jest prosta metoda, jeśli nie potrzebujesz obsługiwać urządzeń wyprodukowanych przed 2012 rokiem. Witamy w tworzeniu aplikacji!
click_whir
1
skąd maszLocaleList
EdgeDev
4

Zainspirowany Calligraphy & Mourjan & mną, stworzyłem to.

najpierw musisz utworzyć podklasę 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());
        }
    }
}

następnie musisz ustawić to na swój tag aplikacji AndroidManifest.xml:

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

i dodaj to do swojego tagu aktywności AndroidManifest.xml.

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

zwróć uwagę, że pref_locale to zasób w postaci ciągu:

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

a hardcode „en” jest domyślnym językiem, jeśli pref_locale nie jest ustawione

Mahpooya
źródło
To nie wystarczy, musisz też nadpisać kontekst w każdym działaniu. Skończysz z sytuacją, że twój baseContext będzie miał jedno ustawienie regionalne, a aplikacja będzie miała inne. W rezultacie będziesz mieć mieszane języki w swoim interfejsie użytkownika. Proszę zobaczyć moją odpowiedź.
Oleksandr Albul
3

Tutaj nie ma 100% działającego rozwiązania. Musisz użyć obu createConfigurationContexti applyOverrideConfiguration. W przeciwnym razie, nawet jeśli zastąpić baseContextw każdej działalności z nową konfiguracją, aktywność będzie nadal korzystać Resourcesz ContextThemeWrapperze starej lokalizacji.

Oto moje rozwiązanie, które działa do API 29:

Podklasuj swoją MainApplicationklasę z:

abstract class LocalApplication : Application() {

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

Również każdy Activityz:

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

Dodaj LocaleExt.ktz następnymi funkcjami rozszerzenia:

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
    }

Dodaj do res/values/arrays.xmlobsługiwanych języków w tablicy:

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

Chcę wspomnieć:

  • Służy config.setLayoutDirection(toLocale);do zmiany kierunku układu podczas korzystania z ustawień regionalnych RTL, takich jak arabski, perski itp.
  • "sys" w kodzie jest wartością oznaczającą „dziedziczenie domyślnego języka systemu”.
  • Tutaj „langPref” jest kluczem preferencji, w którym umieszcza się bieżący język użytkownika.
  • Nie ma potrzeby ponownego tworzenia kontekstu, jeśli używa już potrzebnych ustawień regionalnych.
  • Nie ma potrzeby, aby jak napisano ContextWrapertutaj, wystarczy ustawić nowy kontekst zwrócony z createConfigurationContextjako baseContext
  • To jest bardzo ważne! Kiedy dzwonisz createConfigurationContext, powinieneś przekazać konfigurację utworzoną od zera i tylko z Localeustawioną właściwością. W tej konfiguracji nie powinno być żadnych innych właściwości. Ponieważ jeśli ustawimy inne właściwości dla tej konfiguracji ( na przykład orientację ), nadpisujemy tę właściwość na zawsze, a nasz kontekst nie zmienia już tej właściwości orientacji, nawet jeśli obrócimy ekran.
  • Nie wystarczy tylko recreatedziałać, gdy użytkownik wybierze inny język, ponieważ applicationContext pozostanie ze starymi ustawieniami lokalnymi i może zapewnić nieoczekiwane zachowanie. Więc posłuchaj zmiany preferencji i zamiast tego zrestartuj całe zadanie aplikacji:

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
źródło
To nie działa. Rozważ także utworzenie podstawowego działania dla wszystkich działań zamiast powielania kodu w każdym działaniu. Ponadto recreateTask(Context context)metoda nie działa poprawnie, ponieważ nadal widzę układ bez żadnych zmian.
blueware
@blueware Zaktualizowałem próbki. Wcześniej było kilka błędów. Ale obecnie powinno działać, to jest kod z mojej aplikacji produkcyjnej. Zabawne odtworzenieTask nie mogło działać, można było wyświetlić toast typu „Język zostanie zmieniony po restarcie” ..
Oleksandr Albul
1

Oto rozwiązanie @ bassel-mourjan z odrobiną dobroci kotlina :):

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

A oto jak tego używasz:

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(ContextWrapper(newBase).wrap(defaultLocale.language))
}
Michał
źródło
Ta kwestia val config = baseContext.resources.configurationjest bardzo błędna. Skończysz z wieloma błędami z tego powodu. Zamiast tego musisz utworzyć nową konfigurację. Zobacz moją odpowiedź.
Oleksandr Albul
0

jest tutaj proste rozwiązanie z contextWrapper: Android N zmienia język programowo Zwróć uwagę na metodęrereate ()

Thanasis Saxanidis
źródło
Link jest pomocny i stanowi dobre odniesienie. Uważam, że lepiej jest podać tutaj rzeczywistą odpowiedź, niż wymagać dodatkowego kliknięcia.
ToothlessRebel
masz rację jestem po prostu nowy na stackoverflow i pomyślałem, że byłoby źle wziąć kredyty na odpowiedź, więc opublikować link do oryginalnego autora
Thanasis Saxanidis
-1

Spróbuj tego:

Configuration config = getBaseContext().getResources().getConfiguration();
config.setLocale(locale);
context.createConfigurationContext(config);
Eric B.
źródło
1
Po prostu tworzy aktywność, której potrzebujesz, aby zmienić kontekst na nowy
Ali Karaca.