Android - 以编程方式更改应用程序语言环境

Android - Change application locale programmatically

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

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

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

虽然这成功更改了应用程序上下文的语言环境,但 activity 上下文不再遵循自定义语言环境(无论我是否从 BaseActivity 扩展每个 activity 或不) .奇怪。

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. 如果在应用程序上下文中设置了语言环境,无论您是否设置 activity 上下文,语言环境都将仅设置为应用程序上下文,而不是 activity(或任何其他)上下文.
  2. 如果区域设置未在应用程序上下文中设置,而是在 activity 上下文中设置,则区域设置将设置为 activity 上下文。

我能想到的解决方法是:

  1. 在 activity 上下文中设置语言环境并在任何地方使用它们。但是如果没有打开通知等将不起作用 activity.
  2. 在应用程序上下文中设置语言环境并在任何地方使用它。但这意味着您不能利用 Context#getResources() 获得 activity.

编辑(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, addressed the issue (along with others) and the author created a library called Lingver解决问题。

编辑(2022 年 6 月 3 日): Lingver 库完全未能解决一些问题,并且似乎有一段时间处于非活动状态。经过彻底调查,我想出了自己的实现:(您可以根据 Apache-2.0 或 GPL-3.0-or-later 许可的条款复制下面的代码)

LangUtils.java

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

    private static ArrayMap<String, Locale> sLocaleMap;

    public static void init(@NonNull Application application) {
        application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
            private final HashMap<ComponentName, Locale> mLastLocales = new HashMap<>();
            @Override
            public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
                mLastLocales.put(activity.getComponentName(), applyLocaleToActivity(activity));
            }

            @Override
            public void onActivityStarted(@NonNull Activity activity) {
                if (!Objects.equals(mLastLocales.get(activity.getComponentName()), getFromPreference(activity))) {
                    Log.d("LangUtils", "Locale changed in activity " + activity.getComponentName());
                    ActivityCompat.recreate(activity);
                }
            }

            @Override
            public void onActivityResumed(@NonNull Activity activity) {
            }

            @Override
            public void onActivityPaused(@NonNull Activity activity) {
            }

            @Override
            public void onActivityStopped(@NonNull Activity activity) {
            }

            @Override
            public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
            }

            @Override
            public void onActivityDestroyed(@NonNull Activity activity) {
                mLastLocales.remove(activity.getComponentName());
            }
        });
        application.registerComponentCallbacks(new ComponentCallbacks() {
            @Override
            public void onConfigurationChanged(@NonNull Configuration newConfig) {
                applyLocale(application);
            }

            @Override
            public void onLowMemory() {
            }
        });
        applyLocale(application);
    }

    public static void setAppLanguages(@NonNull Context context) {
        if (sLocaleMap == null) sLocaleMap = new ArrayMap<>();
        Resources res = context.getResources();
        Configuration conf = res.getConfiguration();
        // Assume that there is an array called language_key which contains all the supported language tags
        String[] locales = context.getResources().getStringArray(R.array.languages_key);
        Locale appDefaultLocale = Locale.forLanguageTag(LANG_DEFAULT);

        for (String locale : locales) {
            conf.setLocale(Locale.forLanguageTag(locale));
            Context ctx = context.createConfigurationContext(conf);
            String langTag = ctx.getString(R.string._lang_tag);

            if (LANG_AUTO.equals(locale)) {
                sLocaleMap.put(LANG_AUTO, null);
            } else if (LANG_DEFAULT.equals(langTag)) {
                sLocaleMap.put(LANG_DEFAULT, appDefaultLocale);
            } else sLocaleMap.put(locale, ConfigurationCompat.getLocales(conf).get(0));
        }
    }

    @NonNull
    public static ArrayMap<String, Locale> getAppLanguages(@NonNull Context context) {
        if (sLocaleMap == null) setAppLanguages(context);
        return sLocaleMap;
    }

    @NonNull
    public static Locale getFromPreference(@NonNull Context context) {
        String language = // TODO: Fetch current language from the shared preferences
        getAppLanguages(context);
        Locale locale = sLocaleMap.get(language);
        if (locale != null) {
            return locale;
        }
        // Load from system configuration
        Configuration conf = Resources.getSystem().getConfiguration();
        //noinspection deprecation
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? conf.getLocales().get(0) : conf.locale;
    }

    public static Locale applyLocaleToActivity(Activity activity) {
        Locale locale = applyLocale(activity);
        // Update title
        try {
            ActivityInfo info = activity.getPackageManager().getActivityInfo(activity.getComponentName(), 0);
            if (info.labelRes != 0) {
                activity.setTitle(info.labelRes);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // Update menu
        activity.invalidateOptionsMenu();
        return locale;
    }

    private static Locale applyLocale(Context context) {
        return applyLocale(context, LangUtils.getFromPreference(context));
    }

    private static Locale applyLocale(@NonNull Context context, @NonNull Locale locale) {
        updateResources(context, locale);
        Context appContext = context.getApplicationContext();
        if (appContext != context) {
            updateResources(appContext, locale);
        }
        return locale;
    }

    private static void updateResources(@NonNull Context context, @NonNull Locale locale) {
        Locale.setDefault(locale);

        Resources res = context.getResources();
        Configuration conf = res.getConfiguration();
        //noinspection deprecation
        Locale current = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? conf.getLocales().get(0) : conf.locale;

        if (current == locale) {
            return;
        }

        conf = new Configuration(conf);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            setLocaleApi24(conf, locale);
        } else {
            conf.setLocale(locale);
        }
        //noinspection deprecation
        res.updateConfiguration(conf, res.getDisplayMetrics());
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private static void setLocaleApi24(@NonNull Configuration config, @NonNull Locale locale) {
        LocaleList defaultLocales = LocaleList.getDefault();
        LinkedHashSet<Locale> locales = new LinkedHashSet<>(defaultLocales.size() + 1);
        // Bring the target locale to the front of the list
        // There's a hidden API, but it's not currently used here.
        locales.add(locale);
        for (int i = 0; i < defaultLocales.size(); ++i) {
            locales.add(defaultLocales.get(i));
        }
        config.setLocales(new LocaleList(locales.toArray(new Locale[0])));
    }
}

MyApplication.java

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        LangUtils.init(this);
    }
}

在您更改语言的首选项中,您可以像这样简单地重新启动首选项 activity:

ActivityCompat.recreate(activity);

使用 Android WebView 的活动 通过 Activity.findViewById() 加载 webview 后,您可以立即添加以下行:

// Fix locale issue due to WebView (https://issuetracker.google.com/issues/37113860)
LangUtils.applyLocaleToActivity(this);