Изменение языка не работает после перехода на Androidx
У меня есть старый проект, который поддерживает несколько языков. Я хочу обновить библиотеку поддержки и целевую платформу. До перехода на 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
не работа. Даже следующий код не работает и всегда показывает app_name
vlaue в ресурсе по умолчанию string.xml.
<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!
14 ответов
Наконец я нахожу проблему в своем приложении. При переносе проекта на 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
, моя проблема решена и смена языка работает правильно.
Я нахожу последнюю стабильную версию библиотеки appcompat в репозитории Maven. Я также изменяю другие библиотеки на последнюю стабильную версию.
Теперь мой раздел зависимостей приложения выглядит так:
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'
}
ОБНОВЛЕНИЕ 21 августа 2020 г.:
Наконец-то был выпущен AppCompat 1.2.0. Если вы не используетеContextWrapper
или ContextThemeWrapper
вообще, больше нечего делать, и вы сможете удалить любые обходные пути, которые были у вас в 1.1.0!
Если вы ДЕЙСТВИТЕЛЬНО используете ContextWrapper
или ContextThemeWrapper
внутри attachBaseContext
изменения локали будут нарушены, потому что, когда вы передаете свой обернутый контекст в super,
- 1.2.0
AppCompatActivity
делает внутренние вызовы, которые обертывают вашContextWrapper
в другойContextThemeWrapper
, - или если вы используете
ContextThemeWrapper
, заменяет свою конфигурацию пустой, аналогично тому, что произошло в 1.1.0.
Но решение всегда одно и то же. Я пробовал несколько других решений для ситуации 2, но, как указал @Kreiri в комментариях (спасибо за вашу помощь в расследовании!),AppCompatDelegateImpl
всегда заканчивалось удалением локали. Большое препятствие в том, что, в отличие от 1.1.0,applyOverrideConfiguration
вызывается в вашем базовом контексте, а не в активности вашего хоста, поэтому вы не можете просто переопределить этот метод в своей деятельности и исправить языковой стандарт, как в 1.1.0. Единственное рабочее решение, о котором я знаю, - это отменить упаковку, переопределив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")
}
}
Затем в нашем базовом классе активности вы удаляете все обходные пути 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. Я буду там, чтобы починить... Увидимся, космический ковбой!
СТАРЫЙ ОТВЕТ И РЕШЕНИЕ ДЛЯ 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) {
...
}
В невыпущенном коммите Криса Бэйнса это было фактически исправлено: новая конфигурация является полной копией конфигурации базового контекста.
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
:
Котлин раствор
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
не будет установлен AppCompatDelegateImpl
с onConfigurationChanged
. Это также можно исправить, если скопировать весь кодAppCompatDelegateImpl
используется для расчета текущего ночного режима для вашего базового кода активности, а затем отменяет его перед super.onConfigurationChanged
вызов. В Котлине это выглядело бы так:
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)
}
В новых библиотеках совместимости приложений есть проблема, связанная с ночным режимом, которая вызывает переопределение конфигурации на Android 21-25. Это можно исправить, применив вашу конфигурацию при вызове этой общедоступной функции:
public void applyOverrideConfiguration(конфигурация 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);
}
Я использую "androidx.appcompat:appcompat:1.3.0-alpha01", но полагаю, что он также будет работать с версией 1.2.0.
Следующий код основан на поиске кода Android.
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
}
}
Поздний ответ, но я подумал, что может быть полезно. Начиная с androidx.appcompat: appcompat:1.2.0-beta01 Решение переопределения 0101100101applyOverrideConfiguration
больше не работает на меня. Вместо этого, затем переопределить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 вам не о чем беспокоиться.
Наконец, у меня есть решение для поиска. В моем случае проблема была в bundle apk
потому что он разбивает файлы местоположения. Вbundle apk
по умолчанию будут созданы все разделения. но в блоке Android вашегоbuild.gradle
вы можете указать, какие разбиения будут созданы.
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
}
}
После добавления этого кода в блок Androidbuild.gradle
файл, моя проблема решена.
В androidx.appcompat:appcompat:1.1.0
ошибку также можно решить, просто позвонив getResources()
в Activity.applyOverrideConfiguration()
@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.1.0, androidx.appcompat: appcompat: 1.2.0
Проблема решилась только в этой библиотеке: androidx.appcompat: appcompat: 1.3.0-rc01
Попробуйте что-то вроде этого:
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;
}
}
}
Источники: комментарий Issueetracker и первый образец, на который есть ссылка из комментария Issueetracker.
Хотя приведенное выше работает для меня нормально, другой вариант из второго примера, связанный с комментарием Issueetracker, выглядит следующим образом (я лично не пробовал это):
@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;
}
}
Теперь есть более новая версия, которая также работает:
implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'
Как упомянул @Fred appcompat:1.1.0-alpha03
имеет глюк, хотя это не упоминается в журнале их релизных версий
Была такая же ошибка на androidx.appcompat:appcompat:1.1.0
. Переключился наandroidx.appcompat:appcompat:1.1.0-rc01
и теперь языки меняются на Android 5-6.
Решение:
В 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
Ответ от @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()
.
В моем случае я расширил ContextWrapper с переопределением getResources(). Но после вызова applyOverrideConfiguration я не могу получить доступ к своим пользовательским getResources. Вместо этого я получаю стандартные.
Если я использую приведенный выше код, все работает нормально.
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);
}
NB: Это нормально работает после того, как я воссоздаю упражнение