Android中的资源文件,在使用时都是根据系统语言来处理的,如果当前环境为英文,则在需要使用字符串等资源时,会自动从values-en
类目录中提取,这也是应用国际化
的基础
一般的软件中,不会在应用内进行语言环境的切换,默认在系统整体语言发生改变时,界面会进行重启
,当然,也可以人为进行拦截操作。
不过由于api一直在变更,针对本地语言的变更处理方式也有了些不同,同样,如果想要在应用内自定义一套语言切换功能,也变得比较的繁琐。
最后的话,也会就ActivityThread
类,来简单解读一下,在系统语言环境发生变化时,代码的执行逻辑
在app内单独定义语言切换的话,至少得先有切换的功能,支持的语言应当至少有三种:
在选定了中文环境
或者英文环境
时,系统语言的切换对该应用本身不起作用,在选定了跟随系统
时,则会根据当前系统语言,来动态的调整显示情况。
界面类似这样:
该 activity
源码如下:
@Route(path = ARouterConst.Activity_SwitchLocaleActivity) @InjectActivityTitle(titleRes = R.string.label_switch_locale) @DisableAPTProcess(disables = [APTPlugins.BUTTERKNIF, APTPlugins.AROUTER, APTPlugins.DAGGER]) class SwitchLocaleActivity : BaseActivity<BasePresenter<SoftSettingActivity>>(), LineMenuListener { /** * 布局文件控件 * * 默认 * 中文 * 台湾 * 香港 * 英文 */ private var lmvs = arrayOfNulls<LineMenuView>(5) override fun getContentOrViewId(): Int { verticalLayout { //toolbar include<AppBarLayout>(R.layout.layout_top_bar) //内容区域 scrollView { overScrollMode = View.OVER_SCROLL_ALWAYS isVerticalScrollBarEnabled = false verticalLayout { //系统存储的值 val locale = DefaultPreferenceUtil.getInstance().localeLanguageSwitch val menus = getStringArray(R.array.array_locale_language) //初始化界面 for (i in lmvs.indices) { //lmv lmvs[i] = lmv_select(menuText = menus[i]) { rightSelect = i == locale }.lparams(width = matchParent) { if (i == 0) { topMargin = dimen(R.dimen.view_padding_margin_10dp) } } //分隔符divider if (i < lmvs.size - 1) { dv_line().lparams(width = matchParent) } } }.lparams(width = matchParent).applyRecursively { if (it is LineMenuView || it is DividerView) { it.horizontalPadding = dimen(R.dimen.view_padding_margin_16dp) it.backgroundColorResource = R.color.main_color_white } } }.lparams(matchParent, matchParent) } return 0 } /** * @param v 被点击到的v;此时应该是该view自身:LineMenuView */ override fun performSelf(v: LineMenuView) { if (!v.rightSelect) { (v.getTag(LMVConfigs.TAG_POSITION) as Int).let { position -> ProgressDialog.getInstance(this@SwitchLocaleActivity).show() DefaultPreferenceUtil.getInstance().localeLanguageSwitch = position LOCALE_LANGUAGE_TYPES[position]?.also { BaseApplication.app.setLocale(it) } ?: BaseApplication.app.setLocale(BaseApplication.app.systemLocale) ProgressDialog.getInstance(this@SwitchLocaleActivity).dismiss() //恢复上个状态 for (i in lmvs.indices) { lmvs[i]?.rightSelect = i == position } //刷新界面布局 onConfigurationChanged(resources.configuration) } } } /** * 刷新當前界面:主要是標題和默認 */ override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) lmvs[0]?.menuText = getStringArray(R.array.array_locale_language)[0] title = Unit.getString(R.string.label_switch_locale) setResult(Activity.RESULT_OK) } }
这里使用的是kotlin语言
类上的注册主要是说明activity标题
等信息;
getContentOrViewId
方法是以anko
的形式注入了界面布局
performSelf
方法用来控件LineMenuView
的点击事件
onConfigurationChanged
方法处理当系统的语言改变时,activity
自定义的处理方法
这里使用的控件LineMenuView
是一种敏捷开发的菜单库,在实现一些行式
布局时比较方便,github地址为:LineMenuView
重要的是,要在Manifest
文件中,声明该活动需要自定义语言环境改变的处理方式(这里是连同横竖屏切换一同进行了拦截):
<activity android:name=".SwitchLocaleActivity" android:configChanges="keyboard|screenSize|orientation|locale|layoutDirection"/>
当然,还有一个使用到的布局文件没有贴上源码,不过查看效果图,就可以明白未说明部分的含义了
可以通过多种方式保存选择的语言环境,这里使用 preferences
处理:
public class DefaultPreferenceUtil { //... /** * 切換語言環境 * <p> * 0:未设置 或 跟随系统变化 * 1:简体中文-中国大陆 * 2:繁体中文-中国台湾 * 3:繁体中文-中国香港 * 4:英语-全体-English */ @NotNull public int getLocaleLanguageSwitch() { return preferences.getInt(LOCALE_LANGUAGE_SWITCH, 0); } public boolean setLocaleLanguageSwitch(@NotNull @IntRange(from = 0, to = 4) int locale) { return edit.putInt(LOCALE_LANGUAGE_SWITCH, locale).commit(); } //... }
用 int
来保存语言设置,默认为 0
,表示跟随系统。
就目前规定来说,不管在什么情况下,getLocaleLanguageSwitch
方法获取的值都只能处于 0 - 4
之间,同时还也对应着上面效果图中的五个LineMenuView
控件
需要注意的是,如果不进行任何处理的话,应用在启动时,读取到的语言环境将是系统设置的那个,因此我们需要在Application
启动时就做出处理,根据我们之前设定的环境进行更改
这个过程需要分两步进行:
根据 preferences
中存储的值,我们可以获取对应的 Locale
对象
val LOCALE_LANGUAGE_TYPES = arrayOf( null, Locale.SIMPLIFIED_CHINESE, Locale.TRADITIONAL_CHINESE, Locale("zh", "HK"), Locale.ENGLISH )
如果应用之前没有设置过语言环境,或者说设置的语言环境为跟随系统,则此处返回 null
,否则,返回语言地区对应的Locale
值
在 Application的onCreate方法中,调用以下代码
//设置默认环境 Locale target = Const.INSTANCE.getLOCALE_LANGUAGE_TYPES()[DefaultPreferenceUtil.getInstance().getLocaleLanguageSwitch()]; if (target != null) { setLocale(target); }
这里获取的 target
就是第一步中的Locale
对象,如果为null的话,就不修改原有逻辑,否则,更替目前应用使用的 Locale
值
setLocale
方法比较复杂,因此将其单独提出来:
/** * 设置语言对象 */ @SuppressLint("ObsoleteSdkInt") public void setLocale(@NotNull Locale target) { // 获得res资源对象 Resources resources = getResources(); // 获得设置对象 Configuration config = resources.getConfiguration(); // 获得屏幕参数:主要是分辨率,像素等。 DisplayMetrics dm = resources.getDisplayMetrics(); // 语言 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { config.setLocale(target); } else { config.locale = target; } resources.updateConfiguration(config, dm); }
这样,就完成了基本的语言切换功能;
不过仅仅如此的话,如果应用正在运行过程中,系统语言环境发生了改变,那么应用将不能够正确的进行处理;
比如,如果应用已经锁定了环境为英文,在系统语言环境切换为中文时,回到之前的界面,界面将自动重启,然后以中文的样式进行显示。
事实上,在系统环境发生变化时,我们需要根据应用中保存的值来判断,如果是跟随系统,那么将不进行任何处理,如果是其他情况,则会判断切换的语言是否与当前设置的语言相同,不相同则进行一次修改,相同则不进行任何处理。
具体逻辑如下:
/** * application监听到环境发生变化时,需要 根据情况来判断是否切换语言环境 * <p> * 1.如果当前应用设置了语言环境(非跟随系统变化) ,则不会通知应用切换语言(使用自身默认的语言) * 2.如果当前应用设置跟随系统变化,或者未设置默认语言环境(两者可做统一处理),则判断当前与系统语言是否相同,不同则进行切换(默认不操作) */ @Override public synchronized void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); //系统切换的Locale值 systemLocale = PlusFunsPluginsKt.getApplicationLocale(null, newConfig); Logger.v(getClass().getSimpleName() + "监听到:环境发生变化:%s", newConfig.toString()); int current = DefaultPreferenceUtil.getInstance().getLocaleLanguageSwitch(); switch (current) { case 0: case 1: case 2: case 3: case 4: Locale target = Const.INSTANCE.getLOCALE_LANGUAGE_TYPES()[current]; if (target != null && !target.equals(systemLocale)) { setLocale(target); } break; default: showToast("error language!!!"); System.exit(0); } }
``方法很简单,只是获取当前应用的,或者传入config
对应的Locale
值:
/** * get Application Locale * * 如果传入config则获取当前config的locale值,如果未传入,则默认返回当前应用的locale(非系统) */ inline fun <T> T.getApplicationLocale(config: Configuration? = null): Locale { return (config ?: BaseApplication.app.resources.configuration).run { if (Build.VERSION.SDK_INT < 24) locale else locales.get(0) } }
如此一来,哪怕系统环境发生了改变,app也能对应的做出处理:是保持原有,或者跟随系统变化
经过以上三个步骤,Application对应的Context对象在获取资源时,没有任何问题,但Activity中的Context对象,其实语言环境并没有切换过来
我们通过BaseApplication.app.getResources().getString()
方法获取到 的 值 和通过 activity.getResources().getString()
方法获取到的值可能会不同;因为他们分别对应着不同的Context
上下文对象。
因此只是在环境切换后重启Activity是不起作用的(安卓碎片化比较严重,api各个版本都有差别),还需要在BaseActivity
(活动的基类)中重载Context的绑定方式:
@Override protected void attachBaseContext(Context newBase) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // 8.0需要使用createConfigurationContext处理 newBase = updateResources(newBase); } super.attachBaseContext(newBase); } @TargetApi(Build.VERSION_CODES.N) public Context updateResources(Context context) { Locale locale = PlusFunsPluginsKt.getApplicationLocale(null, null); Configuration configuration = context.getResources().getConfiguration(); configuration.setLocale(locale); configuration.setLocales(new LocaleList(locale)); return context.createConfigurationContext(configuration); }
这样,在activity重启之后,界面语言环境才能显示正常;
然后我们回头再来看一下第一部分列出的Activity的部分源码:
/** * 刷新當前界面:主要是標題和默認 */ override fun onConfigurationChanged(newConfig: Configuration?) { super.onConfigurationChanged(newConfig) lmvs[0]?.menuText = getStringArray(R.array.array_locale_language)[0] title = Unit.getString(R.string.label_switch_locale) setResult(Activity.RESULT_OK) }
这里在回调中,在获取字符串值时,并没有直接使用getString(R.string.label_switch_locale)
,因为这样的话,将使用Activity的Context对象
来获取资源,此时,Activity的Context对象
对应的语言环境根本没有任何变化,因此界面会出现错误;
注:这里Unit.getString(R.string.label_switch_locale)
是利用kotlin动态添加的方法,实际上是利用BaseApplication的Context对象进行取值:
/** * 获取string */ @Suppress("NOTHING_TO_INLINE") inline fun <T> T.getString(@StringRes res: Int, vararg formatArgs: Any?): String { return BaseApplication.app.getString(res, *formatArgs) }
如果需要自定义系统语言事件的处理方法,则需要向前面说明的那样,首先在manifest
文件中进行configChanges
声明,然后在Activity中重载onConfigurationChanged
方法进行逻辑处理。
如果是在应用内切换了语言环境,那么一般来说,需要手动的进行重启,像这样:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { //重启整个应用 if (requestCode == Const.REQUEST_CODE_ONE && resultCode == Activity.RESULT_OK) { recreate() } }
这里假定的情况是:该代码所在的Activity启动了一个可以切换应用语言环境的 Activity,如果语言环境被切换,那么在界面返回时,代码所在界面要进行刷新处理,否则界面内容不会发生改变。
这里还有一点需要注意,有些包含Fragment的Activity,是不能直接调用recreate
方法的,否则会导致应用崩溃,因此需要采用其他方法来进行处理(可以重新启动一个新的activity,然后结束老的活动,或者其他方法)
在最后部分,简单来看一下语言环境改变时,系统方法的调用顺序;
首先,需要找到ActivityThread
类,该类是一切的起点,然后找到这个:
private class ApplicationThread extends IApplicationThread.Stub{ //... public void scheduleApplicationInfoChanged(ApplicationInfo ai) { sendMessage(H.APPLICATION_INFO_CHANGED, ai); } @Override public void scheduleActivityConfigurationChanged( IBinder token, Configuration overrideConfig) { sendMessage(H.ACTIVITY_CONFIGURATION_CHANGED, new ActivityConfigChangeData(token, overrideConfig)); } //... }
其实看到*Stub
的样式,就应该明白,则是个AIDL
调用,具体谁调用的,我们不去深究,以上两个方法已经指明:当Config有变化时,将发送Message消息 : ACTIVITY_CONFIGURATION_CHANGED
、 APPLICATION_INFO_CHANGED
APPLICATION_INFO_CHANGED
单从名字上就可以看出,是让Application执行相应逻辑
查看这段代码:
private class H extends Handler { public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { //... case APPLICATION_INFO_CHANGED: mUpdatingSystemConfig = true; try { handleApplicationInfoChanged((ApplicationInfo) msg.obj); } finally { mUpdatingSystemConfig = false; } break; //... } } }
查看 handleApplicationInfoChanged
方法逻辑
void handleApplicationInfoChanged(@NonNull final ApplicationInfo ai) { // ... handleConfigurationChanged(newConfig, null); // ... }
接着看 handleConfigurationChanged
方法
final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) { //... ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(false, config); freeTextLayoutCachesIfNeeded(configDiff); if (callbacks != null) { final int N = callbacks.size(); for (int i=0; i<N; i++) { ComponentCallbacks2 cb = callbacks.get(i); if (cb instanceof Activity) { // If callback is an Activity - call corresponding method to consider override // config and avoid onConfigurationChanged if it hasn't changed. Activity a = (Activity) cb; performConfigurationChangedForActivity(mActivities.get(a.getActivityToken()), config); } else if (!equivalent) { performConfigurationChanged(cb, config); } } } }
从判断语句中可以看到,当 callback
为 Activity
时,会执行 performConfigurationChangedForActivity
方法,这个逻辑在分割线后就可以看到;
那么 collectComponentCallbacks(false, config);
代码的含义大概就是搜寻所有需要处理 config - change 的对象。
--------------------- 分割线
然后再查看 ACTIVITY_CONFIGURATION_CHANGED
这个消息:
找出这段代码
private class H extends Handler { public void handleMessage(Message msg) { if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what)); switch (msg.what) { //... case ACTIVITY_CONFIGURATION_CHANGED: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); handleActivityConfigurationChanged((ActivityConfigChangeData) msg.obj, INVALID_DISPLAY); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; //... } } }
进入handleActivityConfigurationChanged
方法进行查看:
void handleActivityConfigurationChanged(ActivityConfigChangeData data, int displayId) { ActivityClientRecord r = mActivities.get(data.activityToken); // Check input params. if (r == null || r.activity == null) { if (DEBUG_CONFIGURATION) Slog.w(TAG, "Not found target activity to report to: " + r); return; } final boolean movedToDifferentDisplay = displayId != INVALID_DISPLAY && displayId != r.activity.getDisplay().getDisplayId(); // Perform updates. r.overrideConfig = data.overrideConfig; final ViewRootImpl viewRoot = r.activity.mDecor != null ? r.activity.mDecor.getViewRootImpl() : null; if (movedToDifferentDisplay) { if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity moved to display, activity:" + r.activityInfo.name + ", displayId=" + displayId + ", config=" + data.overrideConfig); final Configuration reportedConfig = performConfigurationChangedForActivity(r, mCompatConfiguration, displayId, true /* movedToDifferentDisplay */); if (viewRoot != null) { viewRoot.onMovedToDisplay(displayId, reportedConfig); } } else { if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle activity config changed: " + r.activityInfo.name + ", config=" + data.overrideConfig); performConfigurationChangedForActivity(r, mCompatConfiguration); } // Notify the ViewRootImpl instance about configuration changes. It may have initiated this // update to make sure that resources are updated before updating itself. if (viewRoot != null) { viewRoot.updateConfiguration(displayId); } mSomeActivitiesChanged = true; }
然后追踪进入 performConfigurationChangedForActivity
方法
private Configuration performConfigurationChangedForActivity(ActivityClientRecord r, Configuration newBaseConfig, int displayId, boolean movedToDifferentDisplay) { r.tmpConfig.setTo(newBaseConfig); if (r.overrideConfig != null) { r.tmpConfig.updateFrom(r.overrideConfig); } final Configuration reportedConfig = performActivityConfigurationChanged(r.activity, r.tmpConfig, r.overrideConfig, displayId, movedToDifferentDisplay); freeTextLayoutCachesIfNeeded(r.activity.mCurrentConfig.diff(r.tmpConfig)); return reportedConfig; }
然后追踪进入 performActivityConfigurationChanged
方法
private Configuration performActivityConfigurationChanged(Activity activity, Configuration newConfig, Configuration amOverrideConfig, int displayId, boolean movedToDifferentDisplay) { // ... if (shouldChangeConfig) { activity.mCalled = false; activity.onConfigurationChanged(configToReport); if (!activity.mCalled) { throw new SuperNotCalledException("Activity " + activity.getLocalClassName() + " did not call through to super.onConfigurationChanged()"); } } //... }
可以看到,最后是执行了 activity
的 onConfigurationChanged
方法
-------------- 结语
随着安卓api的提升,已有的功能都可能会有大的更改,类似的问题可能会越来越多,即便是之前看到的源码,虽然大体逻辑不便,但还是可能有细微处的差别