项目经过Androidx改造后,多语言切换会失效,这里汇总下。
当引用了androidx.appcompat:appcompat:1.1.0时,BaseActivity中实现下面方法:
@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
// 兼容androidX在部分手机切换语言失败问题
if (overrideConfiguration != null) {
int uiMode = overrideConfiguration.uiMode;
overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration());
overrideConfiguration.uiMode = uiMode;
}
super.applyOverrideConfiguration(overrideConfiguration);
}
当引用了androidx.appcompat:appcompat:1.2.0时,BaseActivity中实现下面方法:
// BaseActivity继承AppCompatActivity
// 修复appcompat 1.2+版本导致多语言切换失败,传自定义的ContextThemeWrapper
@Override
protected void attachBaseContext(Context newBase) {
if (shouldSupportMultiLanguage()) {
Integer language = SpUtil.getInt(newBase, Cons.SP_KEY_OF_CHOOSED_LANGUAGE, -1);
Context context = LanguageUtil.attachBaseContext(newBase, language);
final Configuration configuration = context.getResources().getConfiguration();
// 此处的ContextThemeWrapper是androidx.appcompat.view包下的
// 你也可以使用android.view.ContextThemeWrapper,但是使用该对象最低只兼容到API 17
// 所以使用 androidx.appcompat.view.ContextThemeWrapper省心
final ContextThemeWrapper wrappedContext = new ContextThemeWrapper(context,
R.style.Theme_AppCompat_Empty) {
@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
if (overrideConfiguration != null) {
overrideConfiguration.setTo(configuration);
}
super.applyOverrideConfiguration(overrideConfiguration);
}
};
super.attachBaseContext(wrappedContext);
} else {
super.attachBaseContext(newBase);
}
}
// 下面是多语言切换方法
public static Context attachBaseContext(Context context, Integer language) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return createConfigurationResources(context, language);
} else {
applyLanguage(context, language);
return context;
}
}
public static void applyLanguage(Context context, Integer newLanguage) {
Resources resources = context.getResources();
Configuration configuration = resources.getConfiguration();
Locale locale = getSupportLanguage(newLanguage);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
DisplayMetrics dm = resources.getDisplayMetrics();
// apply locale
configuration.setLocale(locale);
resources.updateConfiguration(configuration, dm);
} else {
// updateConfiguration
DisplayMetrics dm = resources.getDisplayMetrics();
configuration.locale = locale;
resources.updateConfiguration(configuration, dm);
}
}
@TargetApi(Build.VERSION_CODES.N)
private static Context createConfigurationResources(Context context, Integer language) {
Resources resources = context.getResources();
final Configuration configuration = resources.getConfiguration();
final DisplayMetrics dm = resources.getDisplayMetrics();
Locale locale;
if (language < 0) {
// 如果没有指定语言使用系统首选语言
locale = getSystemPreferredLanguage();
} else {
// 指定了语言使用指定语言,没有则使用首选语言
locale = getSupportLanguage(language);
}
configuration.setLocale(locale);
resources.updateConfiguration(configuration, dm);
return context;
}
/**
* 获取系统首选语言
*
* @return Locale
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public static Locale getSystemPreferredLanguage() {
Locale locale;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
locale = LocaleList.getDefault().get(0);
} else {
locale = Locale.getDefault();
}
return locale;
}
// getSupportLanguage为自定义方法,看各位需求,自定义需要的Locale就行
升级后为什么会出现这个问题?
根据之前版本逻辑,多语言实现主要通过覆写AppCompatActivit
的attachBaseContext
来实现,所以从此方法入手。
// AppCompatActivit 中的实现
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(getDelegate().attachBaseContext2(newBase));
}
可以看到AppCompatActivit
中是通过代理对我们传进来的newBase进行了一些处理。再来看getDelegate()
方法,该方法创建了一个AppCompatDelegate
。
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
找到AppCompatDelegate
的实现类AppCompatDelegateImpl
,定位到 attachBaseContext2(Context)
方法 。
// AppCompatDelegateImpl
...
/**
* Flag indicating whether we can return a different context from attachBaseContext().
* Unfortunately, doing so breaks Robolectric tests, so we skip night mode application there.
*/
private static final boolean sCanReturnDifferentContext =
!"robolectric".equals(Build.FINGERPRINT);
/**
* Flag indicating whether ContextThemeWrapper.applyOverrideConfiguration() is available.
*/
private static final boolean sCanApplyOverrideConfiguration = Build.VERSION.SDK_INT >= 17;
...
@NonNull
@Override
@CallSuper
public Context attachBaseContext2(@NonNull final Context baseContext) {
mBaseContextAttached = true;
// This is a tricky method. Here are some things to avoid:
// 1. Don't modify the configuration of the Application context. All changes should remain
// local to the Activity to avoid conflicting with other Activities and internal logic.
// 2. Don't use createConfigurationContext() with Robolectric because Robolectric relies on
// method overrides.
// 3. Don't use createConfigurationContext() unless you're able to retain the base context's
// theme stack. Not the last theme applied -- the entire stack of applied themes.
final int modeToApply = mapNightMode(baseContext, calculateNightMode());
// If the base context is a ContextThemeWrapper (thus not an Application context)
// and nobody's touched its Resources yet, we can shortcut and directly apply our
// override configuration.
if (sCanApplyOverrideConfiguration
&& baseContext instanceof android.view.ContextThemeWrapper) {
// api>=17 并且通过attachBaseContext传递的对象需要是android.view.ContextThemeWrapper
// 上面的解决方案就是让AppCompatActivity中attachBaseContext方法代理程序进入此段代码
// 来达到返回自定义的ContextThemeWrapper,然后覆写applyOverrideConfiguration来实现
// 修改Configuration中local的功能
final Configuration config = createOverrideConfigurationForDayNight(
baseContext, modeToApply, null);
if (DEBUG) {
Log.d(TAG, String.format("Attempting to apply config to base context: %s",
config.toString()));
}
try {
ContextThemeWrapperCompatApi17Impl.applyOverrideConfiguration(
(android.view.ContextThemeWrapper) baseContext, config);
return baseContext;
} catch (IllegalStateException e) {
if (DEBUG) {
Log.d(TAG, "Failed to apply configuration to base context", e);
}
}
}
// Again, but using the AppCompat version of ContextThemeWrapper.
if (baseContext instanceof ContextThemeWrapper) {
// 通过attachBaseContext传递的对象需要是androidx.appcompat.view.ContextThemeWrapper
// 上面的解决方案就是让AppCompatActivity中attachBaseContext方法代理程序进入此段代码
// 来达到返回自定义的ContextThemeWrapper,然后覆写applyOverrideConfiguration来实现
// 修改Configuration中local的功能
final Configuration config = createOverrideConfigurationForDayNight(
baseContext, modeToApply, null);
if (DEBUG) {
Log.d(TAG, String.format("Attempting to apply config to base context: %s",
config.toString()));
}
try {
((ContextThemeWrapper) baseContext).applyOverrideConfiguration(config);
return baseContext;
} catch (IllegalStateException e) {
if (DEBUG) {
Log.d(TAG, "Failed to apply configuration to base context", e);
}
}
}
// We can't apply the configuration directly to the existing base context, so we need to
// wrap it. We can't create a new configuration context since the app may rely on method
// overrides or a specific theme -- neither of which are preserved when creating a
// configuration context. Instead, we'll make a best-effort at wrapping the context and
// rebasing the original theme.
if (!sCanReturnDifferentContext) {
return super.attachBaseContext2(baseContext);
}
// We can't trust the application resources returned from the base context, since they
// may have been altered by the caller, so instead we'll obtain them directly from the
// Package Manager.
final Configuration appConfig;
try {
appConfig = baseContext.getPackageManager().getResourcesForApplication(
baseContext.getApplicationInfo()).getConfiguration();
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Application failed to obtain resources from itself", e);
}
// The caller may have directly modified the base configuration, so we'll defensively
// re-structure their changes as a configuration overlay and merge them with our own
// night mode changes. Diffing against the application configuration reveals any changes.
final Configuration baseConfig = baseContext.getResources().getConfiguration();
final Configuration configOverlay;
if (!appConfig.equals(baseConfig)) {
configOverlay = generateConfigDelta(appConfig, baseConfig);
if (DEBUG) {
Log.d(TAG,
"Application config (" + appConfig + ") does not match base config ("
+ baseConfig + "), using base overlay: " + configOverlay);
}
} else {
configOverlay = null;
if (DEBUG) {
Log.d(TAG, "Application config (" + appConfig + ") matches base context "
+ "config, using empty base overlay");
}
}
final Configuration config = createOverrideConfigurationForDayNight(
baseContext, modeToApply, configOverlay);
if (DEBUG) {
Log.d(TAG, String.format("Applying night mode using ContextThemeWrapper and "
+ "applyOverrideConfiguration(). Config: %s", config.toString()));
}
// Next, we'll wrap the base context to ensure any method overrides or themes are left
// intact. Since ThemeOverlay.AppCompat theme is empty, we'll get the base context's theme.
// 如果没有通过attachBaseContext传递自定义ContextThemeWrapper,那么最终AppCompatActivity
// 得到的Context对象为此处new的ContextThemeWrapper,所以我们无法覆写applyOverrideConfiguration
// 最终也正是因为包裹了这一层,导致我们获取的Resources是此处ContextThemeWrapper的Resources,
// 而非我们修改语言后的Resources对象
final ContextThemeWrapper wrappedContext = new ContextThemeWrapper(baseContext,
R.style.Theme_AppCompat_Empty);
wrappedContext.applyOverrideConfiguration(config);
// Check whether the base context has an explicit theme or is able to obtain one
// from its outer context. If it throws an NPE because we're at an invalid point in app
// initialization, we don't need to worry about rebasing under the new configuration.
boolean needsThemeRebase;
try {
needsThemeRebase = baseContext.getTheme() != null;
} catch (NullPointerException e) {
needsThemeRebase = false;
}
if (needsThemeRebase) {
// Attempt to rebase the old theme within the new configuration. This will only
// work on SDK 23 and up, but it's unlikely that we're keeping the base theme
// anyway so maybe nobody will notice. Note that calling getTheme() will clone
// the base context's theme into the wrapped context's theme.
ResourcesCompat.ThemeCompat.rebase(wrappedContext.getTheme());
}
return super.attachBaseContext2(wrappedContext);
}
通过上面代码的分析,可以发现,如果在BaseActivity
中覆写attachBaseContext(Context)
传递的对象非ContextThemeWrapper
,那么系统就会自己创建一个ContextThemeWrapper
,然后将我们传递的Context
作为参数进行一层包裹。最终问题就出在这段代码,系统创建的 ContextThemeWrapper
的Resource
并没有使用我们Context
中的Resource
,而是自己也单独定义了一个Resource
,现在就是单独定义的 Resource
中 Configuration
采用的是系统当前语言,比如中国地区,就是中文,而非我们设定的语言。
最终我们切换语言,都是通过getResources()
去获取不同语言的资源,所以再来看看 AppCompatActivit
中 Resources
获取逻辑。
// AppCompatActivit
@Override
public Resources getResources() {
if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) {
mResources = new VectorEnabledTintResources(this, super.getResources());
}
return mResources == null ? super.getResources() : mResources;
}
通过上面代码,可以清楚的看到,如果AppCompatActivit
中mResources == null,
那么就会调用 super.getResources()
,而该方法得到的对象为ContextThemeWrapper
(android.view包下面的,这是没有传自定义ContextThemeWrapper
时获取的内容,如果传了自定义对象,则该值为ContextImpl
)。我在测试阶段,发现AppCompatActivit
不持有Resources
,最终都是通过父类上下文(ContextThemeWrapper
或者ContextImpl
)获取Resources
。
// android.view.ContextThemeWrapper
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
结合上面的代码,就可以知道为什么AppCompatDelegateImpl
中attachBaseContext2(Context)
返回的Context
仅仅是把我们传递的Context
包裹一层就导致多语言切换无效。因为attachBaseContext2(Context)
返回的ContextThemeWrapper
对象中的Resources
不为null,而该Resources
又没有应用我们设置的Context
中Resources
的Configuration
对象,所以多语言设置无效了。
要清楚的理解Resources
获取流程,建议Debug。通过Debug发现,如果不传自定义ContextThemeWrapper
,则AppCompatActivit
以及父类持有 Resources
结构如下:
AppCompatActivit
-mResources(值为null)
-mBase(AppCompatActivit父类(super)上下文,ContextThemeWrapper)
-mResources(不为null)
-mResourcesImpl
-mConfigration(使用的系统当前语言,非我们设置的语言,也是不传自定义ContextThemeWrapper最终采用的配置)
-mBase(ContextThemeWrapper父类上下文,ContextImpl)
-mResources(不为null)
-mResourcesImpl
-mConfigration(我们配置的语言)
传自定义ContextThemeWrapper后AppCompatActivit结构:
AppCompatActivit
-mResources(值为null)
-mBase(AppCompatActivit父类(super)上下文,ContextImpl)
-mResources(不为null)
-mResourcesImpl
-mConfigration(我们配置的语言)
参考:https://blog.csdn.net/u012527560/article/details/108816692