Android 7.0 语言设置爬坑

作者: Android_ZzT | 来源:发表于2017-04-16 20:58 被阅读5108次

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

    最近项目出现一个语言设置的 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/

    相关文章

      网友评论

      • NKming:其实获取真正系统的locale可以用Resources.getSystem().configuration.locale获取就可以了
      • 瓦西里超超:楼主,能给个Demo么?谢谢!
      • ghroost:public void setLocale(@Nullable Locale loc) {
        setLocales(loc == null ? LocaleList.getEmptyLocaleList() : new LocaleList(loc));
        }
        public void setLocales(@Nullable LocaleList locales) {
        mLocaleList = locales == null ? LocaleList.getEmptyLocaleList() : locales;
        locale = mLocaleList.get(0);
        setLayoutDirection(locale);
        }

        setLocale 后被设置的语言会加到 LocaleList 的第一个的原因是setLocale 后会重新生成一个LocaleList ,你设置的Locale 会变成第一个;
        public LocaleList(@NonNull Locale... list) {
        if (list.length == 0) {
        mList = sEmptyList;
        mStringRepresentation = "";
        } else {
        final Locale[] localeList = new Locale[list.length];
        final HashSet<Locale> seenLocales = new HashSet<Locale>();
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < list.length; i++) {
        final Locale l = list[i];
        if (l == null) {
        throw new NullPointerException("list[" + i + "] is null");
        } else if (seenLocales.contains(l)) {
        throw new IllegalArgumentException("list[" + i + "] is a repetition");
        } else {
        final Locale localeClone = (Locale) l.clone();
        localeList[i] = localeClone;
        sb.append(localeClone.toLanguageTag());
        if (i < list.length - 1) {
        sb.append(',');
        }
        seenLocales.add(localeClone);
        }
        }
        mList = localeList;
        mStringRepresentation = sb.toString();
        }
        }
        ghroost:看源码跳转更方便点
      • f396794368f3:看你的文章然后看到自己的文章链接 哈哈哈
      • 9ad819884679:sLocaleList这个成员变量应该声明成静态吧。

      本文标题:Android 7.0 语言设置爬坑

      本文链接:https://www.haomeiwen.com/subject/ymfzattx.html