×

Android 7.0 语言设置爬坑

96
Android_ZzT
2017.04.16 20:58* 字数 1651

本文为原创文章,如需转载请注明出处,谢谢!

最近项目出现一个语言设置的 bug,情况是这样:在程序中,语言默认选择的是「跟随系统」(系统语言列表中「简体中文」是第一个),然后选择「英语」,之后再切换回「跟随系统」,发现语言并没切回「简体中文」,而还是英文。这篇文章就给大家分享一下我是如何解决这个 bug 的。

Android 7.0 多语言特性

在开始解决 bug 之前,我们还是有必要了解 7.0 中对多语言资源的处理和之前有什么不同。在此小节中,我只阐述结论,具体的分析过程请参考这篇文章,讲的很细致。
http://blog.csdn.net/cekiasoo/article/details/53012646
当然官方文档也要仔细阅读一下,
https://developer.android.com/guide/topics/resources/multilingual-support.html

接下来,我直接根据官方文档中的两个表格简单解释一下 7.0 语言资源解析策略和之前系统相比有什么不同。首先我们看 6.0 及以前的解析策略。

如表中所示,6.0 及以前的系统在解析语言资源时,如果程序中没找到匹配的语言,就会直接使用默认语值(en)。接下来看看 7.0 做了哪些优化。

我们发现,如果 fr_CH 未能匹配到资源,系统会继续查找程序中有没有 fr 的子资源,于是找到了 fr_FR,而不再使用 en 作为默认语言。接下来再看另外一种情况。

用户在系统语言列表中添加了两个语言,在 fr 依次匹配失败以后,系统会继续查找用户的第二语言 it_CH,然而还是没匹配到,但是找到了 it 的子语言 it_IT,所以选其作为默认语言以更好的满足用户的需求。

在 7.0 中,系统提供了新的 APILocaleList.getDefault(),用于获取当前设置中的语言列表,之后我们也需要通过这个 API 来解决 bug。

bug 细节描述

首先,我先要说一下怎样用代码动态的设置当前的语言,大致分为 3 步:

  1. 更改 Configuration 中的 locale 属性,具体代码如下
    Resources resources = getContext().getResources();
    DisplayMetrics dm = resources.getDisplayMetrics();
    Configuration config = resources.getConfiguration();
    config.locale = Locale.ENGLISH; //设置语言
    resources.updateConfiguration(config, dm);

代码中通过更改 config.locale 已经被废弃,官方推荐使用 config.setLocales(LocaleList localeList)config.getLocales() 来进行设置和获取语言。

2.持久化语言选择,可通过 SharedPreference 进行存储,然后在每次进入程序时,先取出之前的配置然后再设置。

3.重启 HomeActivity,并将设置语言的方法放在 Activity 的 onCreate()中执行,以确保每次重启时都是设置最近一次的语言配置

上面只是粗略的说了一下,如果没做过语言设置的同学可以参考这篇文章,讲的很细很好。
http://jaeger.itscoder.com/android/2016/05/14/switch-language-on-android-app.html

了解如何动态设置语言之后,我来详细描述一下 bug 的细节。 我们在设置语言时,实际就是在修改 Configuration 的一 locale 属性,但是到了 7.0 ,Configuration 将通过 LocaleList 来管理语言,bug 的产生也正源于 LocaleList。

现在假设系统语言列表中是「简体中文,英语」,程序中的语言设置选的是「跟随系统」,此时打印 LocaleList.getDefault(),得到的结果是「简体中文,英语」,没有问题。接着我在程序中将语言切换为「德语」,然后再次打印 LocaleList,得到的结果是「德语,简体中文,英语」,发现当前语言被加到了列表的第一位,此时我们再切换回「跟随系统」,即调用 Locale.getDefault() 获取系统默认语言,发现语言切换成了「德语」,并不是「简体中文」,也就是默认获取了列表中的第一个语言。

在 setLocale 后为什么被设置的语言会加到 LocaleList 的第一个,我还没探究明白,需要再仔细看看源码,如果有同学知道也可以给我解答一下!

bug 解决思路

由于每次动态设置语言之后,系统语言列表都会被改变,所以我的思路是在程序一开始的时候就把系统列表保存起来,以便之后使用,话不多说,来看一下我修改后的代码:

public class LanguageUtil {

    private LocaleList sLocaleList;
    
    static {
        //由于API仅支持7.0,需要判断,否则程序会crash
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            sLocaleList = LocaleList.getDefault();
        }
    }
    
    public static void setLocale(Context context) {
        Resources res = context.getResources();
        DisplayMetrics dm = res.getDisplayMetrics();
        Configuration conf = res.getConfiguration();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (auto) { //选择跟随系统
                conf.setLocale(sLocaleList.get(0));
            } else { //设置选择的语言
                conf.setLocale(getLocale());
            }
        } else {
            conf.locale = getCurrentLocale(context);
        }
        res.updateConfiguration(conf, dm);
    }
    
    public static Locale getLocale() {
        Locale locale;
        if (auto) {
            locale = Locale.getDefault();
        } else {
            //从 sharedPreference 中获取 Locale
            locale = getLocaleFromSP();
        }
        return locale;
    }
    
    public void setSystemLocaleList(LocaleList localeList) {
        sLocaleList = localeList;
    }
}

这样修改后,我们每次切换回「跟随系统」,都是直接获取程序初始化时就获取了的系统列表的第一个语言,就不会出现每次之前那种现象了。

但是,我们如果在程序中切到设置中修改了语言列表,然后再切回程序,又会出现问题了,我们在程序初始化时就获取的列表是写死的,并不会跟随系统变化而变化,所以刚才的代码还是不够健壮。如果要是能监听系统语言列表改变的话就好了,没错,解决办法就是去监听语言变化,Android 会在系统语言列表被修改时发出广播 Intent.ACTION_LOCALE_CHANGED,所以我们需要在 BaseActivity 里注册监听该广播,然后在收到语言列表被修改的广播时更新我们自己的列表,然后再设置语言,这样就可以完美解决问题了!看下代码

public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.registerReceiver(localeChangedReceiver,
         new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
    }

    @Override
    protected void onDestroy() {
        this.unregisterReceiver(localeChangedReceiver);
        super.onDestroy();
    }
    
    private BroadcastReceiver localeChangedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                   LanguageUtil.setSystemLocaleList(LocaleList.getDefault());
                   LanguageUtil.setLocale(BaseActivity.this);
                }
            }
        }
    };

}

总结

这是第一次解决由于Android 系统升级导致的 bug,我的解决思路是:

  1. 了解最新系统的特性,和之前的最大区别是什么,可以应用的新 API 是什么
  2. Debug 看看核心问题究竟出现在哪个环节
  3. 结合新特性和新 API寻找解决方案
  4. 充分利用 Android 给我们提供的资源

我也是个初学者,如写的有问题,请及时联系我!感谢!
Blog地址 https://androidzzt.github.io/

Android 知识
Web note ad 1