迁移到 Androidx 后更改区域设置不起作用

Change Locale not work after migrate to Androidx

我有一个支持多语言的旧项目。我想升级支持库和目标平台,在迁移到 Androidx 之前一切正常,但现在更改语言不起作用!

我使用此代码更改 App 的默认语言环境

private static Context updateResources(Context context, String language)
{
    Locale locale = new Locale(language);
    Locale.setDefault(locale);

    Configuration configuration = context.getResources().getConfiguration();
    configuration.setLocale(locale);

    return context.createConfigurationContext(configuration);
}

然后通过覆盖 attachBaseContext 在每个 activity 上调用此方法,如下所示:

@Override
protected void attachBaseContext(Context newBase)
{
    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
    String language = preferences.getString(SELECTED_LANGUAGE, "fa");
    super.attachBaseContext(updateResources(newBase, language));
}

我尝试了其他方法来获取字符串,我注意到 ‍‍‍‍getActivity().getBaseContext().getString 有效而 getActivity().getString 无效。即使下面的代码也不起作用,并且在默认资源 string.xml 中始终显示 app_name vlaue。

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/app_name"/>

我在https://github.com/Freydoonk/LanguageTest

中分享了一个示例代码

getActivity()..getResources().getIdentifier 不起作用,总是 return 0!

1.您可以在 attachBaseContext()

中使用的方法
private void setLanguage(Context mContext, String localeName) {
        Locale myLocale = new Locale(localeName);
        Resources res = mContext.getResources();
        DisplayMetrics dm = res.getDisplayMetrics();
        Configuration conf = res.getConfiguration();
        conf.locale = myLocale;
        res.updateConfiguration(conf, dm);
    }

2。在活动中覆盖

@Override
protected void attachBaseContext(Context newBase) {
    setLanguage(newBase, "your language");
    super.attachBaseContext(newBase);
}

注意: 在我重新创建 activity

后,这对我来说工作正常

终于,我在我的应用程序中找到了问题。将项目迁移到 Androidx 时,我项目的依赖项更改如下:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0-alpha04'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-alpha02'
} 

正如所见,androidx.appcompat:appcompat 的版本是 1.1.0-alpha03 当我将其更改为最新的稳定版本 1.0.2 时,我的问题已解决并且更改语言可以正常工作。

我在 Maven Repository 中找到了 appcompat 库的最新稳定版本。我也把其他库改成最新的稳定版。

现在我的应用程序依赖项部分如下所示:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.0.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}

现在有一个更新的版本也可以使用:

implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'

正如@Fred 提到的那样,appcompat:1.1.0-alpha03 有一个小故障,尽管在他们的 release versions log

中没有提到

最后我得到了定位的解决方案,在我的例子中,实际上问题出在 bundle apk 因为它拆分了定位文件。在 bundle apk 中,默认情况下将生成所有拆分。但在 build.gradle 文件的 android 块中,您可以声明将生成哪些拆分。

bundle {
            language {
                // Specifies that the app bundle should not support
                // configuration APKs for language resources. These
                // resources are instead packaged with each base and
                // dynamic feature APK.
                enableSplit = false
            }
        }

将此代码添加到 build.gradle 文件的 android 块后,我的问题得到解决。

androidx.appcompat:appcompat:1.1.0 上有同样的错误。切换到 androidx.appcompat:appcompat:1.1.0-rc01,现在语言在 Android 5-6.

上更改

新的应用兼容库中存在与夜间模式相关的问题,导致覆盖 android 21 到 25 上的配置。 这可以通过在调用此 public 函数时应用您的配置来解决:

public void applyOverrideConfiguration(Configuration overrideConfiguration

对我来说,这个小技巧通过将设置从覆盖的配置复制到我的配置而起作用,但你可以做任何你想做的事。最好将你的语言逻辑重新应用到新的配置中,以尽量减少错误

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
    if (Build.VERSION.SDK_INT >= 21&& Build.VERSION.SDK_INT <= 25) {
        //Use you logic to update overrideConfiguration locale
        Locale locale = getLocale()//your own implementation here;
        overrideConfiguration.setLocale(locale);
    }
    super.applyOverrideConfiguration(overrideConfiguration);
}

2020 年 8 月 21 日更新:

AppCompat 1.2.0 终于发布了。如果您根本不使用 ContextWrapperContextThemeWrapper,则应该没有其他事情可做,您应该能够删除 1.1.0!

中的任何变通方法

如果您确实在 attachBaseContext 中使用 ContextWrapperContextThemeWrapper,区域设置更改将会中断,因为当您将包装的上下文传递给 super 时,

  1. 1.2.0 AppCompatActivity 进行内部调用,将您的 ContextWrapper 包装在另一个 ContextThemeWrapper
  2. 或者如果您使用 ContextThemeWrapper,将其配置覆盖为空白,类似于 1.1.0 中发生的情况。

但是解决办法总是一样的。对于情况 2,我尝试了多种其他解决方案,但正如@Kreiri 在评论中指出的那样(感谢您的调查帮助!),AppCompatDelegateImpl 总是最终会剥离语言环境。最大的障碍是,与 1.1.0 不同,applyOverrideConfiguration 是在你的基础上下文中调用的,而不是你的主机 activity,所以你不能只在你的 activity 和像在 1.1.0 中那样修复语言环境。我知道的唯一可行的解​​决方案是 通过覆盖 getDelegate() 来反转包装,以确保您的包装 and/or 区域设置覆盖最后 。首先,您在下面添加 class:

Kotlin 示例(请注意 class 必须在 androidx.appcompat.app 包内,因为唯一存在的 AppCompatDelegate 构造函数是包私有的)

package androidx.appcompat.app

import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.AttributeSet
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar

class BaseContextWrappingDelegate(private val superDelegate: AppCompatDelegate) : AppCompatDelegate() {

    override fun getSupportActionBar() = superDelegate.supportActionBar

    override fun setSupportActionBar(toolbar: Toolbar?) = superDelegate.setSupportActionBar(toolbar)

    override fun getMenuInflater(): MenuInflater? = superDelegate.menuInflater

    override fun onCreate(savedInstanceState: Bundle?) {
        superDelegate.onCreate(savedInstanceState)
        removeActivityDelegate(superDelegate)
        addActiveDelegate(this)
    }

    override fun onPostCreate(savedInstanceState: Bundle?) = superDelegate.onPostCreate(savedInstanceState)

    override fun onConfigurationChanged(newConfig: Configuration?) = superDelegate.onConfigurationChanged(newConfig)

    override fun onStart() = superDelegate.onStart()

    override fun onStop() = superDelegate.onStop()

    override fun onPostResume() = superDelegate.onPostResume()

    override fun setTheme(themeResId: Int) = superDelegate.setTheme(themeResId)

    override fun <T : View?> findViewById(id: Int) = superDelegate.findViewById<T>(id)

    override fun setContentView(v: View?) = superDelegate.setContentView(v)

    override fun setContentView(resId: Int) = superDelegate.setContentView(resId)

    override fun setContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.setContentView(v, lp)

    override fun addContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.addContentView(v, lp)

    override fun attachBaseContext2(context: Context) = wrap(superDelegate.attachBaseContext2(super.attachBaseContext2(context)))

    override fun setTitle(title: CharSequence?) = superDelegate.setTitle(title)

    override fun invalidateOptionsMenu() = superDelegate.invalidateOptionsMenu()

    override fun onDestroy() {
        superDelegate.onDestroy()
        removeActivityDelegate(this)
    }

    override fun getDrawerToggleDelegate() = superDelegate.drawerToggleDelegate

    override fun requestWindowFeature(featureId: Int) = superDelegate.requestWindowFeature(featureId)

    override fun hasWindowFeature(featureId: Int) = superDelegate.hasWindowFeature(featureId)

    override fun startSupportActionMode(callback: ActionMode.Callback) = superDelegate.startSupportActionMode(callback)

    override fun installViewFactory() = superDelegate.installViewFactory()

    override fun createView(parent: View?, name: String?, context: Context, attrs: AttributeSet): View? = superDelegate.createView(parent, name, context, attrs)

    override fun setHandleNativeActionModesEnabled(enabled: Boolean) {
        superDelegate.isHandleNativeActionModesEnabled = enabled
    }

    override fun isHandleNativeActionModesEnabled() = superDelegate.isHandleNativeActionModesEnabled

    override fun onSaveInstanceState(outState: Bundle?) = superDelegate.onSaveInstanceState(outState)

    override fun applyDayNight() = superDelegate.applyDayNight()

    override fun setLocalNightMode(mode: Int) {
        superDelegate.localNightMode = mode
    }

    override fun getLocalNightMode() = superDelegate.localNightMode

    private fun wrap(context: Context): Context {
        TODO("your wrapping implementation here")
    }
}

然后在我们的基础 activity class 中删除所有 1.1.0 解决方法并简单地添加:

private var baseContextWrappingDelegate: AppCompatDelegate? = null

override fun getDelegate() = baseContextWrappingDelegate ?: BaseContextWrappingDelegate(super.getDelegate()).apply {
    baseContextWrappingDelegate = this
}

根据您使用的 ContextWrapper 实现,配置更改可能会破坏主题或区域设置更改。要解决此问题,请另外添加:

override fun createConfigurationContext(overrideConfiguration: Configuration) : Context {
    val context = super.createConfigurationContext(overrideConfiguration)
    TODO("your wrapping implementation here")
}

而且你很好!你可以期待 Google 在 1.3.0 中再次打破这一点。我会在那里修好...再见,space 牛仔!

APPCOMPAT 1.1.0 的旧答案和解决方案:

基本上后台发生的事情是,当您在 attachBaseContext 中正确设置配置时,AppCompatDelegateImpl 然后将配置覆盖为 全新的配置没有语言环境:

 final Configuration conf = new Configuration();
 conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);

 try {
     ...
     ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
     handled = true;
 } catch (IllegalStateException e) {
     ...
 }

In an unreleased commit by Chris Banes 这实际上已修复:新配置是基本上下文配置的深层副本。

final Configuration conf = new Configuration(baseConfiguration);
conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
try {
    ...
    ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
    handled = true;
} catch (IllegalStateException e) {
    ...
}

在此发布之前,可以手动执行完全相同的操作。要继续使用 1.1.0 版,请在 attachBaseContext:

下面添加

Kotlin 解决方案

override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
    if (overrideConfiguration != null) {
        val uiMode = overrideConfiguration.uiMode
        overrideConfiguration.setTo(baseContext.resources.configuration)
        overrideConfiguration.uiMode = uiMode
    }
    super.applyOverrideConfiguration(overrideConfiguration)
}

Java解法

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
    if (overrideConfiguration != null) {
        int uiMode = overrideConfiguration.uiMode;
        overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration());
        overrideConfiguration.uiMode = uiMode;
    }
    super.applyOverrideConfiguration(overrideConfiguration);
}

此代码与 Configuration(baseConfiguration) 在幕后所做的完全相同,但是因为我们在 之后 AppCompatDelegate 已经设置了正确的uiMode,我们必须确保在我们修复它之后将覆盖的 uiMode 移交给它,这样我们就不会丢失 dark/light 模式设置。

请注意,如果您没有在清单中指定 configChanges="uiMode",这仅适用于它自己。如果这样做,则会出现另一个错误:在 onConfigurationChanged 内部,newConfig.uiMode 不会被 AppCompatDelegateImplonConfigurationChanged 设置。如果您将 AppCompatDelegateImpl 用于计算当前夜间模式的所有代码复制到您的基本 activity 代码,然后在 super.onConfigurationChanged 调用之前覆盖它,也可以解决此问题。在 Kotlin 中它看起来像这样:

private var activityHandlesUiMode = false
private var activityHandlesUiModeChecked = false

private val isActivityManifestHandlingUiMode: Boolean
    get() {
        if (!activityHandlesUiModeChecked) {
            val pm = packageManager ?: return false
            activityHandlesUiMode = try {
                val info = pm.getActivityInfo(ComponentName(this, javaClass), 0)
                info.configChanges and ActivityInfo.CONFIG_UI_MODE != 0
            } catch (e: PackageManager.NameNotFoundException) {
                false
            }
        }
        activityHandlesUiModeChecked = true
        return activityHandlesUiMode
    }

override fun onConfigurationChanged(newConfig: Configuration) {
    if (isActivityManifestHandlingUiMode) {
        val nightMode = if (delegate.localNightMode != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED) 
            delegate.localNightMode
        else
            AppCompatDelegate.getDefaultNightMode()
        val configNightMode = when (nightMode) {
            AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES
            AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO
            else -> applicationContext.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
        }
        newConfig.uiMode = configNightMode or (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv())
    }
    super.onConfigurationChanged(newConfig)
}

@0101100101 的回答对我有用。

只有我用过

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration)
{
  if (overrideConfiguration != null) {
    int uiMode = overrideConfiguration.uiMode;
    overrideConfiguration.setTo(getResources().getConfiguration());
    overrideConfiguration.uiMode = uiMode;
  }
  super.applyOverrideConfiguration(overrideConfiguration);
}

所以只有 getResources() 而不是 getBaseContext().getResources()

在我的例子中,我已经使用覆盖的 getResources() 扩展了 ContextWrapper。 但是在调用 applyOverrideConfiguration 之后,我无法访问我的自定义 getResources。我得到的是标准的。

如果我使用上面的代码一切正常。

androidx.appcompat:appcompat:1.1.0 错误也可以通过简单地在 Activity.applyOverrideConfiguration()

中调用 getResources() 来解决
@Override public void
applyOverrideConfiguration(Configuration cfgOverride)
{
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
      Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    // add this to fix androidx.appcompat:appcompat 1.1.0 bug
    // which happens on Android 6.x ~ 7.x
    getResources();
  }

  super.applyOverrideConfiguration(cfgOverride);
}

迟到的答案,但我认为可能会有帮助。自 androidx.appcompat:appcompat:1.2.0-beta01 覆盖 applyOverrideConfiguration 的解决方案不再适用于我。相反,在 then overriden attacheBaseContext 中,您必须调用 applyOverrideConfiguration() 而不覆盖它 .

override fun attachBaseContext(newBase: Context) {
    val newContext = LocaleHelper.getUpdatedContext(newBase)
    super.attachBaseContext(newContext)
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1){
        applyOverrideConfiguration(newContext.resources.configuration)
    }
}

遗憾的是他的解决方案只适用于 1.1.0。根据我的研究,这应该已经正式修复。奇怪的是这个错误仍然存​​在。我知道我使用的是测试版,但对于想要使用最新版本的人来说,这个解决方案对我来说很有效。 在模拟器 api 级别 21-25 上测试。超过那个api水平,你就不用担心了。

尝试这样的事情:

public class MyActivity extends AppCompatActivity {
    public static final float CUSTOM_FONT_SCALE = 4.24f;
    public static final Locale CUSTOM_LOCALE = Locale.CANADA_FRENCH; // or whatever

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(useCustomConfig(newBase));
    }

    private Context useCustomConfig(Context context) {
        Locale.setDefault(CUSTOM_LOCALE);
        if (Build.VERSION.SDK_INT >= 17) {
            Configuration config = new Configuration();
            config.fontScale = CUSTOM_FONT_SCALE;
            config.setLocale(CUSTOM_LOCALE);
            return context.createConfigurationContext(config);
        } else {
            Resources res = context.getResources();
            Configuration config = new Configuration(res.getConfiguration());
            config.fontScale = CUSTOM_FONT_SCALE;
            config.locale = CUSTOM_LOCALE;
            res.updateConfiguration(config, res.getDisplayMetrics());
            return context;
        }
    }
}   

来源:issuetracker comment and the first sample linked from the issuetracker comment.

虽然以上对我来说工作正常,但 second sample linked from the issuetracker comment 的另一个选项如下(我还没有亲自尝试过):

@RequiresApi(17)
public class MyActivity extends AppCompatActivity {
    public static final float CUSTOM_FONT_SCALE = 4.24f;
    public static final Locale CUSTOM_LOCALE = Locale.CANADA_FRENCH; // or whatever

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        Configuration config = new Configuration();
        config.fontScale = CUSTOM_FONT_SCALE;
        applyOverrideConfiguration(config);
    }

    @Override
    public void applyOverrideConfiguration(Configuration newConfig) {
        super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig));
    }

    private Configuration updateConfigurationIfSupported(Configuration config) {
        if (Build.VERSION.SDK_INT >= 24) {
            if (!config.getLocales().isEmpty()) {
                return config;
            }
        } else {
            if (config.locale != null) {
                return config;
            }
        }

        Locale locale = CUSTOM_LOCALE;
        if (locale != null) {
            if (Build.VERSION.SDK_INT >= 17) {
                config.setLocale(locale);
            } else {
                config.locale = locale;
            }
        }
        return config;
    }
}

我正在使用“androidx.appcompat:appcompat:1.3.0-alpha01”,但我想它也适用于 Version 1.2.0
以下代码基于Android Code Search.

import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import java.util.*

open class MyBaseActivity :AppCompatActivity(){

    override fun attachBaseContext(newBase: Context?) {
        super.attachBaseContext(newBase)
        val config = Configuration()
        applyOverrideConfiguration(config)
    }

    override fun applyOverrideConfiguration(newConfig: Configuration) {
        super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig))
    }

    open fun updateConfigurationIfSupported(config: Configuration): Configuration? {
        // Configuration.getLocales is added after 24 and Configuration.locale is deprecated in 24
        if (Build.VERSION.SDK_INT >= 24) {
            if (!config.locales.isEmpty) {
                return config
            }
        } else {
            if (config.locale != null) {
                return config
            }
        }
        // Please Get your language code from some storage like shared preferences
        val languageCode = "fa"
        val locale = Locale(languageCode)
        if (locale != null) {
            // Configuration.setLocale is added after 17 and Configuration.locale is deprecated
            // after 24
            if (Build.VERSION.SDK_INT >= 17) {
                config.setLocale(locale)
            } else {
                config.locale = locale
            }
        }
        return config
    }
}

解法:

在应用级别 Gradle,我在 android 部分包含了以下代码,

bundle {
    language {
        // Specifies that the app bundle should not support
        // configuration APKs for language resources. These
        // resources are instead packaged with each base and
        // dynamic feature APK.
        enableSplit = false
    }
}

https://medium.com/dwarsoft/how-to-provide-languages-dynamically-using-app-bundle-567d2ec32be6

现在这些库的语言不会改变: androidx.appcompat:应用程序兼容性:1.1.0, androidx.appcompat:应用程序兼容性:1.2.0

这个问题只在这个库中解决了: androidx.appcompat:appcompat:1.3.0-rc01