Изменение языка не работает после перехода на 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. 1.2.0 AppCompatActivity делает внутренние вызовы, которые обертывают ваш ContextWrapper в другой ContextThemeWrapper,
  2. или если вы используете 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: Это нормально работает после того, как я воссоздаю упражнение

Другие вопросы по тегам