Android-以编程方式更改应用程序区域设置

问题描述

在Android应用程序中更改语言环境从未如此简单。使用androidx.appcompat:appcompat:1.3.0-alpha02,在应用程序中更改语言环境似乎比我想象的要困难得多。看来活动上下文和应用程序上下文的行为非常不同。如果我使用通用的BaseActivity(如下所示)更改活动的区域设置,它将对相应的活动有效。

BaseActivity.java

public class BaseActivity extends AppCompatActivity {
    private Locale currentLocale;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        currentLocale = LangUtils.updateLanguage(this);
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(LangUtils.attachBaseContext(newBase));
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (currentLocale != LangUtils.getLocaleByLanguage(this)) recreate();
    }
}

但是我需要更改应用程序上下文的语言环境,因为这仅限于活动。为此,我可以像上面一样轻松地覆盖Application#attachBaseContext()来更新语言环境。

MyApplication.java

public class MyApplication extends Application {
    private static AppManager instance;

    @NonNull
    public static AppManager getInstance() {
        return instance;
    }

    @NonNull
    public static Context getContext() {
        return instance.getBaseContext();
    }

    @Override
    public void onCreate() {
        instance = this;
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(LangUtils.attachBaseContext(base));
    }
}

尽管这成功更改了应用程序上下文的语言环境,但活动上下文不再遵守自定义语言环境(无论我是否从BaseActivity扩展每个活动)。很奇怪。

LangUtils.java

public final class LangUtils {
    public static final String LANG_AUTO = "auto";

    private static Map<String,Locale> sLocaleMap;
    private static Locale sDefaultLocale;

    static {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            sDefaultLocale = LocaleList.getDefault().get(0);
        } else sDefaultLocale = Locale.getDefault();
    }

    public static Locale updateLanguage(@NonNull Context context) {
        Resources resources = context.getResources();
        Configuration config = resources.getConfiguration();
        Locale currentLocale = getLocaleByLanguage(context);
        config.setLocale(currentLocale);
        displayMetrics dm = resources.getdisplayMetrics();
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N){
            context.getApplicationContext().createConfigurationContext(config);
        } else {
            resources.updateConfiguration(config,dm);
        }
        return currentLocale;
    }

    public static Locale getLocaleByLanguage(Context context) {
        // Get language from shared preferences
        String language = AppPref.getNewInstance(context).getString(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR);
        if (sLocaleMap == null) {
            String[] languages = context.getResources().getStringArray(R.array.languages_key);
            sLocaleMap = new HashMap<>(languages.length);
            for (String lang : languages) {
                if (LANG_AUTO.equals(lang)) {
                    sLocaleMap.put(LANG_AUTO,sDefaultLocale);
                } else {
                    String[] langComponents = lang.split("-",2);
                    if (langComponents.length == 1) {
                        sLocaleMap.put(lang,new Locale(langComponents[0]));
                    } else if (langComponents.length == 2) {
                        sLocaleMap.put(lang,new Locale(langComponents[0],langComponents[1]));
                    } else {
                        Log.d("LangUtils","Invalid language: " + lang);
                        sLocaleMap.put(LANG_AUTO,sDefaultLocale);
                    }
                }
            }
        }
        Locale locale = sLocaleMap.get(language);
        return locale != null ? locale : sDefaultLocale;
    }

    public static Context attachBaseContext(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return updateResources(context);
        } else {
            return context;
        }
    }

    @TargetApi(Build.VERSION_CODES.N)
    private static Context updateResources(@NonNull Context context) {
        Resources resources = context.getResources();
        Locale locale = getLocaleByLanguage(context);
        Configuration configuration = resources.getConfiguration();
        configuration.setLocale(locale);
        configuration.setLocales(new LocaleList(locale));
        return context.createConfigurationContext(configuration);
    }
}

因此,我的结论是:

  1. 如果在应用程序上下文中设置了语言环境,则无论您是否设置活动上下文,都将仅将语言环境设置为应用程序上下文,而不是活动(或任何其他)上下文。
  2. 如果不是在应用程序上下文中设置语言环境,而是在活动上下文中设置语言环境,则该语言环境将设置为活动上下文。

我能想到的解决方法是:

  1. 在活动上下文中设置区域设置,并在各处使用它们。但是,如果没有任何打开的活动,则通知等将不起作用。
  2. 在应用程序上下文中设置语言环境,并在各处使用它。但这意味着您无法利用Context#getResources()进行活动。

编辑(2020年10月30日):有些人建议使用Contextwrapper。我尝试使用一个(如下所示),但仍然是相同的问题。一旦使用上下文包装器包装了应用程序上下文,语言环境就停止为活动和片段工作。没什么改变。


public class MyContextwrapper extends Contextwrapper {
    public MyContextwrapper(Context base) {
        super(base);
    }

    @NonNull
    public static Contextwrapper wrap(@NonNull Context context) {
        Resources res = context.getResources();
        Configuration configuration = res.getConfiguration();
        Locale locale = LangUtils.getLocaleByLanguage(context);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            configuration.setLocale(locale);
            LocaleList localeList = new LocaleList(locale);
            LocaleList.setDefault(localeList);
            configuration.setLocales(localeList);
        } else {
            configuration.setLocale(locale);
            displayMetrics dm = res.getdisplayMetrics();
            res.updateConfiguration(configuration,dm);
        }
        configuration.setLayoutDirection(locale);
        context = context.createConfigurationContext(configuration);
        return new MyContextwrapper(context);
    }
}

解决方法

博客文章how to change the language on Android at runtime and don’t go mad解决了这个问题(以及其他问题),作者创建了一个名为Lingver的库来解决问题。

LangUtils.java

public final class LangUtils {
    public static final String LANG_AUTO = "auto";

    private static Map<String,Locale> sLocaleMap;
    private static final Locale sDefaultLocale;

    static {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            sDefaultLocale = LocaleList.getDefault().get(0);
        } else sDefaultLocale = Locale.getDefault();
    }

    public static Locale getLocaleByLanguage(Context context) {
        // Get language from shared preferences
        String language = AppPref.getNewInstance(context).getString(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR);
        if (sLocaleMap == null) {
            String[] languages = context.getResources().getStringArray(R.array.languages_key);
            sLocaleMap = new HashMap<>(languages.length);
            for (String lang : languages) {
                if (LANG_AUTO.equals(lang)) {
                    sLocaleMap.put(LANG_AUTO,sDefaultLocale);
                } else {
                    sLocaleMap.put(lang,Locale.forLanguageTag(lang));
                }
            }
        }
        Locale locale = sLocaleMap.get(language);
        return locale != null ? locale : sDefaultLocale;
    }
}

MyApplication.java

public class MyApplication extends Application {
    private static MyApplication instance;

    @NonNull
    public static MyApplication getInstance() {
        return instance;
    }

    @NonNull
    public static Context getContext() {
        return instance.getBaseContext();
    }

    @Override
    public void onCreate() {
        instance = this;
        super.onCreate();
        Lingver.init(instance,LangUtils.getLocaleByLanguage(instance));
    }
}

在您要更改语言的首选项中,可以添加以下内容:

// Set new language to the shared preferences
AppPref.set(AppPref.PrefKey.PREF_CUSTOM_LOCALE_STR,currentLang);
// Apply the language 
Lingver.getInstance().setLocale(activity,LangUtils.getLocaleByLanguage(activity));
// Recreate the current activity
activity.recreate();