簡體   English   中英

Android - 以編程方式更改應用程序區域設置

[英]Android - Change application locale programmatically

在 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()來更新語言環境,就像上面一樣。

我的應用程序.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));
    }
}

雖然這成功地更改了應用程序上下文的語言環境,但活動上下文不再遵循自定義語言環境(無論我是否從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);
    }
}

一篇博客文章, 如何在運行時更改 Android 上的語言並且不要發瘋,解決了這個問題(與其他人一起),作者創建了一個名為Lingver的庫來解決這些問題。

編輯(2022 年 6 月 3 日): Lingver 庫完全未能解決一些問題,並且似乎有一段時間處於非活動狀態。 經過徹底調查,我提出了自己的實現:(您可以根據 Apache-2.0 或 GPL-3.0 或更高版本許可的條款復制以下代碼)

語言工具.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])));
    }
}

我的應用程序.java

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

在您更改語言的首選項中,您可以像這樣簡單地重新啟動首選項活動:

ActivityCompat.recreate(activity);

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

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

只需在基本活動中完成即可(所有活動的超級 class 都可以完成一次,或者如果您沒有活動,則在每個活動中執行此操作)

override fun attachBaseContext(newBase: Context) {
        val lang = "en" // or ar or any language you want
        val locale = Locale(lang)
        Locale.setDefault(locale)
        val config = Configuration()
        config.setLocale(locale)
        val ctx = newBase.createConfigurationContext(config)
        super.attachBaseContext(ctx)
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM