引言
項(xiàng)目中遇到切換app語(yǔ)言
的需求,要求在“簡(jiǎn)體中文”和“English”兩種語(yǔ)言之間切換部分控件的語(yǔ)言文案,不受系統(tǒng)語(yǔ)言切換的影響。
TODO 切換系統(tǒng)Configuration設(shè)置源碼分析
TODO Android資源管理機(jī)制
1.添加多語(yǔ)言資源文件
按照Android的資源管理方式,我們需要在res目錄下建立兩個(gè)values目錄,其中values是默認(rèn)的路徑,values-en是英文資源的目錄。
默認(rèn)情況下,app啟動(dòng)會(huì)根據(jù)系統(tǒng)的設(shè)置加載對(duì)應(yīng)的資源,系統(tǒng)切換了語(yǔ)言設(shè)置,app也會(huì)更新設(shè)置,所以這樣不能完全滿足我們的需求。
2.保持app語(yǔ)言設(shè)置,不受系統(tǒng)影響
我們不想要app隨著系統(tǒng)語(yǔ)言的改變而改變,而是保持用戶上一次的選擇。默認(rèn)安卓系統(tǒng)不會(huì)保留app的語(yǔ)言設(shè)置,我們需要本地記錄一下用戶的選擇,在app重新啟動(dòng)的時(shí)候加載之前保存的語(yǔ)言資源。存儲(chǔ)用戶的選擇比較容易,放到SharedPreference里即可。然后在app重新啟動(dòng)的時(shí)候,我們需要手動(dòng)更新下app的Application、Activity、Fragment和Service收到的配置信息。
以Activity為例,我們?cè)诨惖腶ttachBaseContext方法中更新保存有config信息的Context對(duì)象:
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(ConfigurationUtil.attachBaseContext(newBase));
}
/**
* 覆寫(xiě)Activity的attachBaseContext方法,更新context語(yǔ)言設(shè)置
*
* @param context 上下文
* @return 更新后的context
*/
public static Context attachBaseContext(Context context) {
final Locale locale;
final Resources res = context.getResources();
final Configuration config = res.getConfiguration();
if (LuckyApplication.mAppLanguage == AppConstant.ENGLISH) {
locale = Locale.ENGLISH;
} else {
locale = Locale.SIMPLIFIED_CHINESE;
}
updateConfig(res, config, locale);
return context;
}
/**
* 更新資源配置
* 7.0之后官方建議context.createConfigurationContext(config);
*
* @param config Configuration對(duì)象
* @param locale Locale對(duì)象
*/
private static void updateConfig(Resources res, Configuration config, Locale locale) {
setLocale(config, locale);
res.updateConfiguration(config, res.getDisplayMetrics());
}
/**
* 修改configuration的locale信息
*
* @param config Configuration對(duì)象
* @param locale Locale對(duì)象
*/
private static void setLocale(Configuration config, Locale locale) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.setLocale(locale);
} else {
config.locale = locale;
}
}
ConfigurationUtil.attachBaseContext(newBase)
方法中更新newBase對(duì)象,使其中保存的Locale為我們自定義,而非系統(tǒng)的。當(dāng)Activity被attach到window時(shí),調(diào)用到此方法,便會(huì)加載我們想要的資源。這時(shí)如果手動(dòng)修改了系統(tǒng)語(yǔ)言設(shè)置,然后從任務(wù)歷史中切回我們的app時(shí),棧中的所有Activity會(huì)被系統(tǒng)依照展示次序依次銷毀重建(棧頂被銷毀重建,退出到前一個(gè)頁(yè)面時(shí),前一個(gè)頁(yè)面會(huì)被銷毀重建)。由于Activity被銷毀重建,會(huì)重新執(zhí)行了生命周期方法,Activity的attachBaseContext方法也就會(huì)被重新執(zhí)行到,所以系統(tǒng)的語(yǔ)言修改后,我們?nèi)匀豢梢哉_地加載到我們自己設(shè)置的語(yǔ)言資源,所以從表面上看并沒(méi)有受到系統(tǒng)修改的影響。
通常官方也是建議我們重啟所有Activity的,這么做會(huì)銷毀舊的資源數(shù)據(jù),重新加載新的,安全方便。
官方描述:
如果應(yīng)用在特定配置變更期間無(wú)需更新資源,并且因性能限制您需要盡量避免重啟,則可聲明 Activity 將自行處理配置變更,這樣可以阻止系統(tǒng)重啟 Activity。
注:自行處理配置變更可能導(dǎo)致備用資源的使用更為困難,因?yàn)橄到y(tǒng)不會(huì)為您自動(dòng)應(yīng)用這些資源。
只能在您必須避免 Activity 因配置變更而重啟這一萬(wàn)般無(wú)奈的情況下,才考慮采用自行處理配置變更這種方法,而且對(duì)于大多數(shù)應(yīng)用并不建議使用此方法。
要聲明由 Activity 處理配置變更,請(qǐng)?jiān)谇鍐挝募芯庉嬒鄳?yīng)的 <activity> 元素,以包含 android:configChanges屬性以及代表要處理的配置的值。
android:configChanges屬性的文檔中列出了該屬性的可能值(最常用的值包括 "orientation" 和 "keyboardHidden",
分別用于避免因屏幕方向和可用鍵盤(pán)改變而導(dǎo)致重啟)。
您可以在該屬性中聲明多個(gè)配置值,方法是用管道 `|` 字符分隔這些配置值。
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
現(xiàn)在,當(dāng)其中一個(gè)配置發(fā)生變化時(shí),MyActivity不會(huì)重啟。相反,MyActivity會(huì)收到對(duì) onConfigurationChanged的調(diào)用。
向此方法傳遞 Configuration對(duì)象指定新設(shè)備配置。您可以通過(guò)讀取 Configuration中的字段,確定新配置,然后通過(guò)更新界面中使用的資源進(jìn)行適當(dāng)?shù)母摹?調(diào)用此方法時(shí),Activity 的 Resources對(duì)象會(huì)相應(yīng)地進(jìn)行更新,以根據(jù)新配置返回資源,這樣,您就能夠在系統(tǒng)不重啟 Activity 的情況下輕松重置 UI 的元素。
如果我們不希望在系統(tǒng)語(yǔ)言發(fā)生變化時(shí)重啟Activity,需要在Manifest.xml文件中配置:android:configChanges="locale"
即可。
這樣做之后我們會(huì)在重新回到Activity的時(shí)候,進(jìn)入回調(diào)方法public void onConfigurationChanged(Configuration newConfig) {...
中去執(zhí)行。其參數(shù)newConfig對(duì)象代表所有當(dāng)前配置,而不僅僅是已經(jīng)變更的配置。
3.運(yùn)行時(shí)系統(tǒng)設(shè)置變更
需要注意:當(dāng)我們修改系統(tǒng)語(yǔ)言設(shè)置后,系統(tǒng)會(huì)更新當(dāng)前手機(jī)中所有正在運(yùn)行的進(jìn)程里的所有組件(分析在開(kāi)篇的鏈接里有說(shuō)明),所以這種情況下再次回到Activity,如果我們刷新UI,UI會(huì)使用系統(tǒng)的設(shè)置加載相應(yīng)的資源文件。比如,
假設(shè)我們app設(shè)置為了簡(jiǎn)體中文(我們?cè)贏pplication中保存相應(yīng)的flag,來(lái)判斷系統(tǒng)的語(yǔ)言設(shè)置是否和app的設(shè)置一致或者發(fā)生了變化),如果系統(tǒng)由簡(jiǎn)體中文修改為了English,如果我們不做上述處理,刷新UI會(huì)加載英文資源。
這并不是我們想要的效果,所以我們需要在onConfigurationChanged回調(diào)中,再次更新Context中的Resources設(shè)置。做法如下:
@Override
public void onConfigurationChanged(Configuration newConfig) {
ConfigurationUtil.updateResources(getResources());
super.onConfigurationChanged(newConfig);
}
/**
* 系統(tǒng)語(yǔ)言配置改變,會(huì)通知每一個(gè)app,當(dāng)重新回到app時(shí),會(huì)回調(diào)onConfigurationChanged,
* 但是此時(shí),super內(nèi)部的mResources變量為空,update方法調(diào)用不了,所以手動(dòng)調(diào)用此方法
*
* @param res Resources對(duì)象
*/
public static void updateResources(Resources res) {
Configuration config = res.getConfiguration();
if (LuckyApplication.mAppLanguage == AppConstant.SIMPLIFIED_CHINESE && isEnglishLocale(config)) {
updateConfig(res, config, Locale.SIMPLIFIED_CHINESE);
} else if (LuckyApplication.mAppLanguage == AppConstant.ENGLISH && !isEnglishLocale(config)) {
updateConfig(res, config, Locale.ENGLISH);
}
}
/**
* 更新資源配置
* 7.0之后官方建議context.createConfigurationContext(config);
*
* @param config Configuration對(duì)象
* @param locale Locale對(duì)象
*/
private static void updateConfig(Resources res, Configuration config, Locale locale) {
setLocale(config, locale);
res.updateConfiguration(config, res.getDisplayMetrics());
}
/**
* 修改configuration的locale信息
*
* @param config Configuration對(duì)象
* @param locale Locale對(duì)象
*/
private static void setLocale(Configuration config, Locale locale) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.setLocale(locale);
} else {
config.locale = locale;
}
}
上述方案并非完美結(jié)局問(wèn)題,onConfigurationChanged方法調(diào)用之前getResource()有可能就會(huì)被調(diào)用多次,時(shí)序問(wèn)題,加載出依據(jù)系統(tǒng)的語(yǔ)言設(shè)置的資源文件。(。>︿<)_θ,所以會(huì)有4的兜底方案。
4. 異常狀況和兜底方案
TODO 切換語(yǔ)言并回到app,然后新建fragment并add,會(huì)有資源加載錯(cuò)誤情況
我們?cè)贏ctivity的onConfigurationChanged方法更新過(guò)資源,貌似這里沒(méi)有起作用。有種暴力的方式:
我們?cè)?code>getResources()方法中調(diào)用ConfigurationUtil.updateResources(getResources());
,但是getResources()
方法會(huì)被多次調(diào)用,每次調(diào)用會(huì)判斷一次,并不是特別理想,不過(guò)可以及時(shí)刷新,再未找到更好的方法前也算是一種兜底的方案。
5.最后補(bǔ)充一點(diǎn):
在切換的Activity中,我們最好重啟一下所有的app,執(zhí)行下方方法后,重新打開(kāi)singleTask的MainActivity...
changeAppLanguage(this, flag);
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
/**
* 修改應(yīng)用語(yǔ)言設(shè)置
*
* @param context 上下文
* @param lang 切換的語(yǔ)言
*/
public static void changeAppLanguage(Context context, int lang) {
Context appContext = context.getApplicationContext();
Resources resources = appContext.getResources();
Configuration config = resources.getConfiguration();
if (lang == AppConstant.ENGLISH) {
setLocale(config, Locale.ENGLISH);
} else {
setLocale(config, Locale.SIMPLIFIED_CHINESE);
}
appContext.getApplicationContext().getResources().updateConfiguration(config, resources.getDisplayMetrics());
}