迁移到Androidx后更改语言环境无效。

75

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

我使用此代码来更改应用程序的默认区域设置

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方法,在每个活动上调用此方法,如下所示:

@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值。

<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也不起作用,总是返回0!


如果您提供一个示例应用程序,我认为这些问题会更容易解决。 - Ibrahim Ali
您在应用语言更改后重新创建活动吗? - mustafiz012
如果我在attachBaseContext中调用recareate会造成循环调用... - Fred
@IbrahimAli 我在 https://github.com/Freydoonk/LanguageTest 分享了一份样例代码。 - Fred
2
有趣的是,设置 android:configChanges="uiMode" 可以解决这个问题,但是相应地,当在浅色和深色主题之间切换时,我们会得到颜色伪影。 - 0101100101
14个回答

132

2023年3月15日更新:

AppCompat 1.6.1已发布,但似乎没有破坏任何东西。下面的解决方法仍然有效,耶!

2022年9月5日更新:

发布了AppCompat 1.5.0,其中包括这些更改,具体如下:

此稳定版本包括夜间模式稳定性的改进

[...]

修复了一个问题,即AppCompat的上下文包装器重用应用程序上下文的支持资源实现,导致在应用程序上下文上覆盖了uiMode。(Idf9d5

深入代码后发现几个关键的更改,这意味着,与以前类似,如果您没有使用ContextWrapperContextThemeWrapper并且不提供用户设置与设备不同的夜间模式,那么所有上下文包装和主题和语言环境更新应该正常工作,您应该可以安全地删除下面解释的所有解决方法或任何其他已经实施的黑客行为。
不幸的是,如果您正在使用任何类型的ContextWrapper或允许用户手动设置应用程序的夜间模式,则仍然存在问题,我不确定为什么Google在修复此问题方面有困难。我遇到的问题是:如果uiMode不再被覆盖,那么当您:
  • 将设备设置为暗模式,
  • 将应用程序设置为亮模式(夜间模式已禁用),
  • 将手机旋转到横向模式,
  • 锁定屏幕,
  • 在仍处于横向模式的情况下再次解锁屏幕,

然后根据您的设备和可能的Android版本,您可能会看到应用程序上下文的uiMode过时,并最终导致黑色与黑色和白色与白色主题的故障。对于处于浅色模式的设备和处于夜间模式的应用程序也会发生这种情况。此外,您的应用程序的区域设置可能会重置为设备的区域设置。在这些情况下,您需要使用下面描述的解决方案。

当遇到区域设置或日/夜模式问题时,在AppCompat 1.2.0-1.6.1中的有效解决方案:

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

  1. 在1.2.0-1.4.2版本中,AppCompatActivity会进行内部调用,将您的ContextWrapper包装在另一个ContextThemeWrapperAppCompatDelegateImpl中,导致您的语言环境被忽略。
  2. 或者如果您使用了ContextThemeWrapper,则会覆盖其配置为空白配置,类似于1.1.0版本中发生的情况。

无论您在1.5.0版本中是否使用上下文包装器(如我最新更新中所述),仍可能出现主题故障和应用程序语言环境重置。解决方案始终相同,没有其他方法适用于我。最大的障碍是,与1.1.0版本不同,applyOverrideConfiguration是在您的基本上下文中调用的,而不是在您的宿主活动中调用,因此您不能像在1.1.0版本中那样在活动中覆盖该方法并修复语言环境(或uiMode)。我知道的唯一有效的解决方案是通过覆盖getDelegate()反转包装顺序,以确保您的包装和/或语言环境覆盖最后应用。首先,您需要添加以下类:

Kotlin示例(请注意,类必须位于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")
    }
}

然后在我们的基础活动类中(确保您首先删除任何其他以前的解决方法),添加此代码:

private var baseContextWrappingDelegate: AppCompatDelegate? = null

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

// OPTIONAL createConfigurationContext and/or onStart code below may be needed depending on your ContextWrapper implementation to avoid issues with themes

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

private fun fixStaleConfiguration() {
    // we only want to fix the configuration if our app theme is different than the system theme, otherwise it will result in an infinite configuration change loop causing a StackOverflowError and crashing your app
    if (AppCompatDelegate.getDefaultNightMode() != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
        applicationContext?.configuration?.uiMode = resources.configuration.uiMode
    TODO("your locale updating implementation here")
    // OPTIONAL if you are using a context wrapper, the wrapper might have stale resources with wrong uiMode and/or locale which you need to clear or update at this stage
}

override fun onRestart() {
    fixStaleConfiguration()
    super.onRestart()
}

// OPTIONAL if you have specified configChanges in your manifest, especially orientation and uiMode
override fun onConfigurationChanged(newConfig: Configuration) {
    fixStaleConfiguration()
    super.onConfigurationChanged(newConfig)
}

关于如何成功地“清除过期资源”的示例,这在你的情况下可能需要也可能不需要:

(context as? ContextThemeWrapper)?.run {
    if (mContextThemeWrapperResources == null) {
        mContextThemeWrapperResources = ContextThemeWrapper::class.java.getDeclaredField("mResources")
        mContextThemeWrapperResources!!.isAccessible = true
    }
    mContextThemeWrapperResources!!.set(this, null)
} ?: (context as? androidx.appcompat.view.ContextThemeWrapper)?.run {
    if (mAppCompatContextThemeWrapperResources == null) {
        mAppCompatContextThemeWrapperResources = androidx.appcompat.view.ContextThemeWrapper::class.java.getDeclaredField("mResources")
        mAppCompatContextThemeWrapperResources!!.isAccessible = true
    }
    mAppCompatContextThemeWrapperResources!!.set(this, null)
}
(context as? AppCompatActivity)?.run {
    if (mAppCompatActivityResources == null) {
        mAppCompatActivityResources = AppCompatActivity::class.java.getDeclaredField("mResources")
        mAppCompatActivityResources!!.isAccessible = true
    }
    mAppCompatActivityResources!!.set(this, null)
}

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) {
     ...
 }

在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带回去,以便不会丢失暗/亮模式设置。

请注意,如果您在清单文件中指定了configChanges="uiMode",则此方法仅适用于自身。否则,还有另一个错误:在onConfigurationChanged中,newConfig.uiMode不会被AppCompatDelegateImplonConfigurationChanged设置。如果您在您的基本活动代码中复制AppCompatDelegateImpl用于计算当前夜间模式的所有代码并在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)
}

1
它对我来说完美地运行了。谢谢!但是,我不明白如何覆盖“applyOverrideConfiguration”方法解决问题。你能解释一下吗? - Burak
1
@Burak 如果您检查ContextThemeWrapper的源代码,您应该能够看到传递给super.applyOverrideConfiguration(overrideConfiguration)的任何内容都将被存储并稍后用于获取任何类型的资源。这只是Google实现日/夜主题的方式。基本资源配置没有正确的uiMode设置,因此ContextThemeWrapper覆盖了getResources()以返回一个具有该配置的实例。 - 0101100101
1
是的,从1.2.0版本开始,“applyOverrideConfiguration”不会被调用。您必须手动从“attachBaseContext”调用此函数。 - Deepak Goyal
2
如果有帮助的话,这里是一个完整的差异,基于将此答案与另一个答案结合使用AppCompatActivity活动和库版本1.2.0实现了本地化更改。 - CNugteren
1
这也适用于AppCompat 1.3.0-beta01! - Claus Holst
显示剩余2条评论

19

最后,我发现了我的应用程序中的问题。 当将项目迁移到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存储库中找到了最新的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'
}

1
我曾经遇到过类似的问题。我的appcompat已经稳定在版本1.0.2,但我的问题出现在设计支持库或者com.google.android.material:material:1.1.0-alpha06上。所以当我切换到稳定版本1.0.0时,问题得到了解决。 - Abir Hasan Shawon

13

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

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);
}

这段代码更改导致某些设备上多个位置崩溃,错误提示为“尝试在空对象引用上调用虚拟方法'java.lang.String java.util.Locale.toString()' android.widget.Editor$TextActionModeCallback.populateMenuWithItemsExt”。建议降级或遵循@0101100101的答案。 - sanjeev
getLocale和setLocale应该在Android N/24或以上版本上使用。 - Rahmat Ihsan

10

我正在使用"androidx.appcompat:appcompat:1.3.0-alpha01",但我想它也可以在版本 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
    }
}

1
我认为这是解决我的问题最简单的答案,谢谢!@EhsanShadi我会等待1.3.0更新,看看接下来会发生什么,:) - Ric17101
这个怎么运作的?创建新的配置会覆盖所有预定义的配置并破坏用户界面。 - dor506
@dor506,您遇到了什么样的UI破坏问题?这似乎是我唯一有效的解决方案 androidx.appcompat:appcompat:1.3.0-alpha03,但我很乐意了解一些需要准备发布的注意事项。谢谢。 - saintjab
@saintjab,请看一下0101100101的最新更新(8月20日),它实际上在更新当前配置(context.getResources().getConfiguration()),而不是在每个活动中创建新的配置。 - dor506

8
晚回答了,但我认为可能会有所帮助。从 androidx.appcompat:appcompat:1.2.0-beta01 开始,0101100101 的覆盖 applyOverrideConfiguration 的解决方案对我不再起作用。相反,在覆盖的 attachBaseContext 中,您必须调用 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版本。根据我的研究,这个问题应该已经被官方修复了。很奇怪这个Bug仍然存在。我知道我在使用Beta版,但对于那些想要使用最新版本的人来说,这个解决方案对我来说是有效的。已测试在模拟器API 21-25上。在更高的API级别上,您不必担心它。


1
这是唯一的解决方案,对于androidx.appcompat...1.2.0 对于某些手机(我的情况是三星S5),否则地区更改不会发生。谢谢! - syed_noorullah

6

最终我找到了定位的解决方法,在我的情况下问题实际上出现在 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区块添加此代码后,我的问题得到了解决。

1
我曾经遇到过同样的问题。在发布到Play商店之前,当我检查时一切正常,但是在发布后,本地化更改就不起作用了。我采用了这个解决方案,现在一切都完美无缺了。谢谢。 - Sunny
1
我有同样的问题。每当我安装调试版本时它都可以工作,但是当我发布捆绑包时就会出现问题,尝试了很多解决方案,但这是唯一有效的一个。谢谢。 - Yonatan Dawit
这太有道理了。其中一部测试手机从应用程序中选择的语言从未显示过,我们想知道为什么只有在从Google Play下载时才会发生这种情况。我通过添加系统语言->清除数据->重新启动应用程序来验证它的工作。谢谢! - Saehun Sean Oh

5

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);
}

3

现在这些库不会更改语言版本:

androidx.appcompat:appcompat:1.1.0, androidx.appcompat:appcompat:1.2.0

仅在以下库中解决了该问题:

androidx.appcompat:appcompat:1.3.0-rc01

嘿,生产级别发布中使用1.3.0-rc01版本而不是1.2.0版本好吗? - Coder
是的,很好。否则,1.2.0根本无法工作。 在我的情况下,应用程序上下文存在问题,但使用活动上下文一切正常。 - Tim Kruichkov

3

试试像这样:

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 commentissuetracker评论中链接的第一个示例
尽管上述方法对我来说运行良好,但另一个选项issuetracker评论中链接的第二个示例如下(我个人没有尝试过):
@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;
    }
}

config.setLocale(locale); 应该在 Android N(24)或更高版本上使用。 - Rahmat Ihsan

1
现在有一个更新的版本也可以使用:
implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'

正如@Fred所提到的,appcompat:1.1.0-alpha03存在一个小故障,尽管它没有在他们的发布版本日志中提到。


我测试了 androidx.appcompat:appcompat:1.1.0-alpha04,它仍然存在这个问题。 - Fred
我的生产应用程序运行得很好,我为每个活动的attachBaseContext(Context newBase)使用自定义的ContextWrapper实现。 - Choletski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接