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 {
//...
/**
* 切換語言環境
*
* 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监听到环境发生变化时,需要 根据情况来判断是否切换语言环境
*
* 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);
}
}
getApplicationLocale
方法很简单,只是获取当前应用的,或者传入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的提升,已有的功能都可能会有大的更改,类似的问题可能会越来越多,即便是之前看到的源码,虽然大体逻辑不便,但还是可能有细微处的差别