#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()还是会出现错误,正在想办法解决中,不过应该是能满足大部分的需求了。哪位大佬知道怎么解决这个问题的,能否告知一下方法,感谢。