1. 程式人生 > >Android多語言以及APP內切換語言的實現(適配7.0)

Android多語言以及APP內切換語言的實現(適配7.0)

Android多語言的適配以及APP內語言切換的實現(適配7.0)

最近專案需要釋出一個海外版,因此需要適配英文版以及在APP內部加入切換語言的功能。琢磨了一段時間,發現並不是之前瞭解的新增一個EN檔案就可以的,因此寫個博,希望能夠幫到他人。這是第一次寫,如果有什麼錯誤,還望見諒。

多語言的適配

簡單的語言適配

因為需要多種語言,因此需要在專案中宣告所需要的語言資原始檔。
在工程的res目錄下,新建values-en的資料夾(字尾表示的是語言,比如-en表示英文,-fr表示法語,-es表示西班牙語,等等不贅述了,想知道的同學網上隨便搜一下就知道了),在這個資料夾目錄下,新建字串資源xml檔案strings.xml。

新建的英文資原始檔夾

檔案建立完成後,就可以簡單的輸入一些中英文對應的字串資源了。舉例說明,“完成”這個字元資源,我們首先在values目錄下的strings.xml中存入該中文字串資源,並賦予id:done
中文字元資源

再在values-en目錄下的strings.xml中存入對應id的英文字串資源
英文字元資源

到這裡準備工作就結束了。當需要使用“完成”資源時,使用R.string.done即可使用該字串資源。當切換系統語言環境的時候,會自動呼叫values-en目錄下的strings.xml內,id為done的資源。到這裡,我們就已經實現了簡單的英文適配。
這種方式同樣適用於圖片資源的多語言適配,和values資料夾一樣,我們只需要新建drawable-en,drawable-en-hdpi,drawable-en-xhdpi的資料夾一樣,對應的中英文資源命名一致,即可完成圖片資源的語言適配。

通過工具類獲取其他語言資源

上面的方法,僅適用於像TextView,EditText這種可以通過setText(R.string.done)方法直接獲取字元並顯示的地方。而這種用處相對來說,在專案中僅僅佔極小一部分,大多數string,都需要對其進行一些處理,比如dialog,Toast等等。因此需要建立一個工具類用於獲取到當前語言環境的對應資源。
新建工具類ChangeLanguageHelper。類的初始化儘量放在Application中進行。先上程式碼:


/**
 * Created by Yif on 2017/8/16.
 */

public class ChangeLanguageHelper
{
private static Context mContext = null; private static String country = null; public static final int CHANGE_LANGUAGE_CHINA = 1; public static final int CHANGE_LANGUAGE_ENGLISH = 2; public static final int CHANGE_LANGUAGE_DEFAULT = 0; private static String mLanguage = ""; private static Resources mResources; private static Locale mDefaultLocale; public static void init(Context context) { mResources = context.getResources(); country = context.getResources().getConfiguration().locale.getCountry(); mContext = context; int appLanguage = Configure.getAppLanguage(); changeLanguage(appLanguage); } /** * 獲取當前字串資源的內容 * * @param id * @return */ public static String getStringById(int id) { String string ; if (mLanguage != null && !"".equals(mLanguage)){ string = mResources.getString(id,mLanguage); }else { string = mResources.getString(id,""); } if (string != null && string.length() > 0) { return string; } return ""; } public static void changeLanguage(int language) { Configuration config = mResources.getConfiguration(); // 獲得設定物件 DisplayMetrics dm = mResources.getDisplayMetrics(); switch (language) { case CHANGE_LANGUAGE_CHINA: config.locale = Locale.SIMPLIFIED_CHINESE; // 中文 config.setLayoutDirection(Locale.SIMPLIFIED_CHINESE); mLanguage = "zh-CN"; country = "CN"; Configure.setAppLanguage(CHANGE_LANGUAGE_CHINA); break; case CHANGE_LANGUAGE_ENGLISH: config.locale = Locale.ENGLISH; // 英文 config.setLayoutDirection(Locale.ENGLISH); mLanguage = "en"; country = "US"; Configure.setAppLanguage(CHANGE_LANGUAGE_ENGLISH); break; case CHANGE_LANGUAGE_DEFAULT: country = Locale.getDefault().getCountry(); if ("CN".equals(country)){ mDefaultLocale = Locale.SIMPLIFIED_CHINESE; }else { mDefaultLocale = Locale.ENGLISH; } config.locale = mDefaultLocale; // 系統預設語言 config.setLayoutDirection(mDefaultLocale); Configure.setAppLanguage(CHANGE_LANGUAGE_DEFAULT); break; } mResources.updateConfiguration(config, dm); EventBus.getDefault().post(new AppEvent.ChangeLanguage()); } public static boolean getDefaultLanguage() { return ("CN".equals(country)); } }

Configure.getAppLanguage為通過sharepreference記錄後獲取的app語言環境(後面實現app內切換語言需要用到,大家可以自行處理)。
通過此工具類的getStringById()的方法,即可獲取到所需語言對應的資源。

APP內切換語言的實現

上述程式碼中changeLanguage()方法,即可實現。(在某些UI頁面切換語言後,無法自動重新整理語言時,需要使用到EventBus處理,具體使用方法就不說了,自行百度-.-)。

功能自此已經實現了,那麼來講講過程中遇到的坑吧。其實最主要的坑,就在於app內的語言環境,與app外系統語言環境的相互切換問題。

1.在app內切換語言為“自動”時,此時app內的語言為跟隨手機系統的語言環境。因此,我們需要獲取到當前手機系統的語言環境,我們可以通過獲取當前城市來進行判斷。

        country = Locale.getDefault().getCountry();
        if ("CN".equals(country)){
           mDefaultLocale = Locale.SIMPLIFIED_CHINESE;
        }else {
           mDefaultLocale =  Locale.ENGLISH;
        }

2.當app執行時,使用home鍵進入後臺,進入設定,切換當前手機語言環境後,再次進入app時,我們看到的是app重新執行的UI。因此,下意識的認為,又走了一遍Application中ChangeLanguageHelper的初始化方法。其實並不是這樣的,application並沒有被銷燬,只是所有的UI均重新渲染了,這是一個坑。

3.設定語言環境時,看到很多文章都說要這麼寫:

        config.locale = Locale.ENGLISH;   // 英文
        Locale.setDefault(Locale.ENGLISH);
        config.setLayoutDirection(Locale.ENGLISH);

從我的工具類可以看出來區別在於,我減少了Locale.setDefault(Locale.ENGLISH);這一句程式碼。這句的程式碼的意思是,以後所有需要Locale.getDefault()的地方,獲取到的值,都不會跟隨手機系統的語言環境,而是這裡設定的值。這樣的寫法會導致一個問題,在我在app內切換中文後繼續切換為自動時,當前app語言環境為中文,但是當我繼續切換為英文再繼續切換為自動後,這時確變成了英文,這顯然是不符合邏輯的。因此,我把這一句去掉了,邏輯就正確了。

最後上一張效果圖:
這裡寫圖片描述

————分割線,更新於2018年8月9日17:45:37————

適配Android7.0(Android N)

以上的方法,僅適用於Android7.0版本以下的系統,Android7.0及以上的版本,在多語言的處理上,API發生了很大的變化,因此需要特殊處理重新適配。
在解決適配問題前,先來了解一下7.0在語言部分有什麼變化。
從7.0開始,API獲取到的系統語言不再是單一語言,而是一個列表,並且預設情況下列表的順序是經API調整過的。調整策略大概是根據使用者實際設定的系統語言優先順序結合應用本身提供的資源種類做調整,而載入頁面時會根據Context中儲存的地域語言資訊(Context → Resource → Configuration → Locale)選擇資原始檔,正是獲取Locale的API發生了變化導致這個現象。具體的變化,可以參考→Android 7.0多語言淺析

這裡需要新增一些方法,先上程式碼:

/**
 * Created by Yif on 2017/8/16.
 */
public class ChangeLanguageHelper {

    public static final int LANGUAGE_CHINA = 1;
    public static final int LANGUAGE_ENGLISH = 2;
    public static final int LANGUAGE_DEFAULT = 0;

    private static String country = null;

    private static String mLanguage = "";

    private static Resources mResources;

    private static String mAutoCountry;

    public static void init(Context context) {
//        mResources = context.getResources();
        initResources(context);
        int currentLanguage = AppConfiguration.getDefault().getAppLanguage();

        switch (currentLanguage) {
            case ChangeLanguageHelper.LANGUAGE_DEFAULT:
                country = context.getResources().getConfiguration().locale.getCountry();
                if ("TW".equals(country) || "HK".equals(country) || "MO".equals(country)) {
                    country = "CN";
                }
                if ("CN".equals(country)) {
                    mLanguage = "zh-CN";
                } else if ("US".equals(country)) {
                    mLanguage = "en";
                }
                break;
            case ChangeLanguageHelper.LANGUAGE_CHINA:
                country = "CN";
                mLanguage = "zh-CN";
                break;
            case ChangeLanguageHelper.LANGUAGE_ENGLISH:
                country = "US";
                mLanguage = "en";
                break;
            default:
                country = context.getResources().getConfiguration().locale.getCountry();
                if ("CN".equals(country)) {
                    mLanguage = "zh-CN";
                } else if ("US".equals(country)) {
                    mLanguage = "en";
                }
                break;
        }

        mAutoCountry = Locale.getDefault().getCountry();
    }

    /**
     * 獲取當前字串資源的內容
     */
    public static String getStringById(int id) {
        String string;
        if (mLanguage != null && !"".equals(mLanguage)) {
            string = mResources.getString(id, mLanguage);
        } else {
            string = mResources.getString(id, "");
        }

        if (string != null && string.length() > 0) {
            return string;
        }
        return "";
    }

    public static void changeLanguage(Context context, int language) {
        switch (language) {
            case LANGUAGE_CHINA:
                mLanguage = "zh-CN";
                country = "CN";
                AppConfiguration.getDefault().setAppLanguage(LANGUAGE_CHINA);
                break;
            case LANGUAGE_ENGLISH:
                mLanguage = "en";
                country = "US";
                AppConfiguration.getDefault().setAppLanguage(LANGUAGE_ENGLISH);
                break;
            case LANGUAGE_DEFAULT:
//                country = Locale.getDefault().getCountry();
                country = mAutoCountry;
                if ("TW".equals(country) || "HK".equals(country) || "MO".equals(country)) {
                    country = "CN";
                }
                if ("CN".equals(country)) {
                    mLanguage = "zh-CN";
                } else if ("US".equals(country)) {
                    mLanguage = "en";
                }
                AppConfiguration.getDefault().setAppLanguage(LANGUAGE_DEFAULT);

                break;
        }

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            LanguageUtils.applyChange(context);
        }
    }

    public static boolean getDefaultLanguage() {
        return ("CN".equals(country));
    }

    public static void initResources(Context context) {
        mResources = context.getResources();
    }
}

接下來是LanguageUtils

/**
 * Created by Yif on 2018/6/4.
 */

public class LanguageUtils {

    public static Locale getSetLocale() {

        int currentLanguage = AppConfiguration.getDefault().getAppLanguage();

        switch (currentLanguage) {
            case ChangeLanguageHelper.LANGUAGE_DEFAULT:
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    return Resources.getSystem().getConfiguration().getLocales().get(0);//解決了獲取系統預設錯誤的問題
                } else {
                    return Locale.getDefault();
                }
            case ChangeLanguageHelper.LANGUAGE_CHINA:
                return Locale.CHINA;
            case ChangeLanguageHelper.LANGUAGE_ENGLISH:
                return Locale.ENGLISH;
            default:
                return Locale.ENGLISH;
        }

    }

    @TargetApi(Build.VERSION_CODES.N)
    public static Context wrapContext(Context context) {
        Resources resources = context.getResources();
        Locale locale = LanguageUtils.getSetLocale();

        Configuration configuration = resources.getConfiguration();
        configuration.setLocale(locale);
        LocaleList localeList = new LocaleList(locale);
        LocaleList.setDefault(localeList);
        configuration.setLocales(localeList);
        return context.createConfigurationContext(configuration);
    }

    public static void applyChange(Context context) {
        Resources res = context.getResources();
        DisplayMetrics dm = res.getDisplayMetrics();
        Configuration conf = res.getConfiguration();

        Locale locale = getSetLocale();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            conf.setLocale(locale);
            LocaleList localeList = new LocaleList(locale);
            LocaleList.setDefault(localeList);
            conf.setLocales(localeList);
        } else {
            conf.locale = locale;
            try
            {
                conf.setLayoutDirection(locale);
            }
            catch(Exception e)
            {
                e.printStackTrace();
            }
        }
        res.updateConfiguration(conf, dm);
    }
}

可以看到,我只在Android N以下版本,才呼叫了LanguageUtils.applyChange()方法。原因是在N以下的版本中,修改語言資源設定,只需要Resources.updateConfiguration()方法即可實現,而在N以上的系統中,該方法無效。
接下來我們來處理7.0的坑。
上述程式碼僅僅是提供了修改的方法,在7.0中,並未真正生效,我們還需要一個操作。

@Override
    protected void attachBaseContext(Context newBase) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
            super.attachBaseContext(LanguageUtils.wrapContext(newBase));
        }else {
            super.attachBaseContext(newBase);
        }

    }

由於7.0以上系統的語言設定是需要跟隨Context上下文來設定的,我們必須要在我們的Activity的attachBaseContext中重新attach修改後的context。專案中的話,一般會有基類去實現。
那LanguageUtils.wrapContext()究竟做了什麼呢?來看看。

 Locale locale = LanguageUtils.getSetLocale();

先通過本地SP檔案獲取設定的語種。
然後是

Configuration configuration = resources.getConfiguration();
        configuration.setLocale(locale);

這裡和以前的沒有什麼變化。
接下來是

LocaleList localeList = new LocaleList(locale);
        LocaleList.setDefault(localeList);
        configuration.setLocales(localeList);

這裡就是7.0的新特性,將所有Locale作為集合設定,實現第一語言、第二語言等的功能。
最重要的就是下面這句,在7.0以上,需要通過這種方式對context進行配置的修改才能生效。

context.createConfigurationContext(configuration);

光有這些還不夠,在修改語言後,我們需要對當前的設定頁面(APP內部)重新繪製。7.0以上需要Activity重新recreate,以下的話就根據實際程式碼自行處理。

/**
     * 改變語言後重新載入當前頁面
     */
    private void reloadPageAfterChangeLanguage() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (mContext instanceof MainActivity) {
                MainActivity mainActivity = (MainActivity) mContext;
                mainActivity.recreate();
            }

        } else {
            EventBus.getDefault().post(new AppEvent.ChangeLanguage());
            ChangeLanguagePage.this.removeAllViews();
            initView(mContext);
            checkItem();
        }
    }

以上就是適配7.0的方案。然而這種方案並不完美,還是會有一點小瑕疵,由於app內切換自動後,會出現Locale.getDefault().getCountry()出現空字串的情況,導致getDefaultLanguage()方法會出現錯誤, 暫時使用臨時變數儲存自動的語言。目前會出現在app執行中,切換後臺更改系統語言後,getDefaultLanguage()還是會出現錯誤,正在想辦法解決中,不過應該是能滿足大部分的需求了。哪位大佬知道怎麼解決這個問題的,能否告知一下方法,感謝。