Androidx 切换多语言失效解决方案(appcompat版本有关)

项目经过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就行

升级后为什么会出现这个问题?

根据之前版本逻辑,多语言实现主要通过覆写AppCompatActivitattachBaseContext 来实现,所以从此方法入手。

// 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作为参数进行一层包裹。最终问题就出在这段代码,系统创建的 ContextThemeWrapperResource并没有使用我们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;
} 

通过上面代码,可以清楚的看到,如果AppCompatActivitmResources == 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;
}

结合上面的代码,就可以知道为什么AppCompatDelegateImplattachBaseContext2(Context)返回的Context仅仅是把我们传递的Context包裹一层就导致多语言切换无效。因为attachBaseContext2(Context)返回的ContextThemeWrapper对象中的Resources不为null,而该Resources又没有应用我们设置的ContextResourcesConfiguration对象,所以多语言设置无效了。

要清楚的理解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 

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/109285503#comments_21826704