背景
由于项目原因,需要用到国际化这一部分的知识。并且在 App 中需要动态切换语言,所以花了点时间研究了下具体的实现。并在兼容问题上做了较多的思考,目前兼容了 Android 4.4 到 Android 10 平台。
实现思路
大致思路如下:
- 我们通过页面上选择的国家语言标识(比如 zh 代表简体中文,en 代表英语),去拿到系统的 Locale 对象 locale;
- 通过 context 拿到系统资源 Resources 对象 resources;
- 通过 resources 拿到资源配置 Configuration 对象 configuration;
- 将获取到的 locale 通过 configuration.setLocale(locale) 更新到 configuration 中;
- 通过 resources 对象拿到 DisplayMetrics 对象 dm,为下一步更新 resources 配置方法提供参数;
- 通过 resources 对象的 updateConfiguration(configuration,dm),将刚刚选择的语言更新到配置中,这样在下一次启动的时候,依然保留了上次选择的语言版本,避免重复切换。
核心代码如下:
/**
* @param context 上下文
* @param newLanguage 想要切换的语言类型 比如 "en" ,"zh"
*/
fun changeAppLanguage(context: Context, newLanguage: String) {
if (TextUtils.isEmpty(newLanguage)) {
return
}
val resources = context.resources
val configuration = resources.configuration
// 获取想要切换的语言类型
val locale = getLocaleByLanguage(newLanguage)
configuration.setLocale(locale)
// updateConfiguration
val dm = resources.displayMetrics
resources.updateConfiguration(configuration, dm)
}
private fun getLocaleByLanguage(language: String): Locale {
var locale = Locale.SIMPLIFIED_CHINESE
if (language == LanguageType.CHINESE.language) {
locale = Locale.SIMPLIFIED_CHINESE
} else if (language == LanguageType.ENGLISH.language) {
locale = Locale.ENGLISH
}
return locale
}
开发步骤
后面会放出 git 源码,就不贴完整的工具类了。下面说一下具体的实现步骤:
- 在 RootApp 的 onCreate() 方法中获取到系统的语言,并写入 Sp(Sp 是封装了 SharedPreferenced 的工具类,源码中会提供)
package com.xzy.multilanguageswitch
import android.app.Application
import android.util.Log
import java.util.*
class RootApp : Application() {
override fun onCreate() {
super.onCreate()
INSTANCE = this
Log.d("", "初始化 Application")
// 获取系统当前的语言环境
val locale = Locale.getDefault().language
Sp.put("language",locale)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Log.e("", "onTrimMemory, level = $level")
}
companion object {
lateinit var INSTANCE: RootApp
}
}
- 在基类 BaseActivity 的 attachBaseContext() 方法中拿到我们写入的语言形态,并调用工具类获取到attach 对应语言环境下的 context 并在调用其 super.attachBaseContext() 时传入。目的是获取到attach 对应语言环境下的 context 。
注意:也可以在 MainActivity 中重写 attachBaseContext() 方法。(如果没有基类)
package com.xzy.multilanguageswitch
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import com.xzy.multilanguageswitch.language.LanguageUtil
/**
* Author: xzy
* Date:
* Description:
*/
abstract class BaseActivity : AppCompatActivity() {
override fun attachBaseContext(newBase: Context?) {
// 获取我们存储的语言环境 比如 "en","zh",等等
val language = Sp.get("language")
// attach对应语言环境下的context
val context = LanguageUtil.attachBaseContext(newBase!!, language!!)
super.attachBaseContext(context)
}
}
- 首次进入 App,通过之前设置的系统语言,显示 UI
// 根据系统首选语言确定刚进入时需要显示的界面
when (Sp.get("language")) {
LanguageType.ENGLISH.language -> {
tv_test.text = getString(R.string.test)
}
LanguageType.CHINESE.language -> {
tv_test.text = getString(R.string.test)
}
}
// 切换为中文
btn_zh.setOnClickListener {
if (Sp.get("language") == "zh") {
// 如果当前已经是中文,则不做任何操作
return@setOnClickListener
}
changeLanguage(LanguageType.CHINESE.language)
}
- 绑定按钮点击事件,动态切换语言
// 切换为中文
btn_zh.setOnClickListener {
if (Sp.get("language") == "zh") {
// 如果当前已经是中文,则不做任何操作
return@setOnClickListener
}
changeLanguage(LanguageType.CHINESE.language)
}
// 切换为英文
btn_en.setOnClickListener {
if (Sp.get("language") == "en") {
// 如果当前已经是英文,则不做任何操作
return@setOnClickListener
}
changeLanguage(LanguageType.ENGLISH.language)
}
/**
* 经过测试:android 8.0 以下的版本需要更新 configuration 和 resources,
* android 8.0 以上只需要将当前的语言环境写入 Sp 文件即可。
* 测试机型 android4.4、android6.0、android7.0、android7.1、android8.1
* 然后,重新创建当前页面。
* @param language
*/
private fun changeLanguage(language: String?) {
// 版本低于 android 8.0 不执行该方法
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 注意,这里的 context 不能传 Application 的 context
LanguageUtil.changeAppLanguage(this, language!!)
}
Sp.put("language", language!!)
// 不同的版本,使用不同的重启方式,达到最好的效果
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
// 6.0 以及以下版本,使用这种方式,并给 activity 添加启动动画效果,可以规避黑屏和闪烁问题
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
finish()
} else {
// 6.0 以上系统直接调用重新创建函数,可以达到无缝切换的效果
recreate()
}
}
至此,基本功能已经实现了。顺便贴一下 LanguageUtil 工具类源码:
package com.xzy.multilanguageswitch.language
import android.annotation.TargetApi
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.LocaleList
import android.text.TextUtils
import java.util.Locale
/**
* Created by xzy .
*/
enum class LanguageType {
CHINESE("zh"),
ENGLISH("en");
var language: String?
get() {
return field ?: ""
}
constructor(language: String?) {
this.language = language
}
}
@Suppress("unused")
object LanguageUtil {
private val TAG = "LanguageUtil"
var sharedPreferences: SharedPreferences? = null
var editor: SharedPreferences.Editor? = null
/**
* @param context 上下文
* @param newLanguage 想要切换的语言类型 比如 "en" ,"zh"
*/
fun changeAppLanguage(context: Context, newLanguage: String) {
if (TextUtils.isEmpty(newLanguage)) {
return
}
val resources = context.resources
val configuration = resources.configuration
// 获取想要切换的语言类型
val locale = getLocaleByLanguage(newLanguage)
configuration.setLocale(locale)
// updateConfiguration
val dm = resources.displayMetrics
resources.updateConfiguration(configuration, dm)
}
private fun getLocaleByLanguage(language: String): Locale {
var locale = Locale.SIMPLIFIED_CHINESE
if (language == LanguageType.CHINESE.language) {
locale = Locale.SIMPLIFIED_CHINESE
} else if (language == LanguageType.ENGLISH.language) {
locale = Locale.ENGLISH
}
return locale
}
fun attachBaseContext(context: Context, language: String): Context {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
updateResources(context, language)
} else {
context
}
}
@TargetApi(Build.VERSION_CODES.N)
private fun updateResources(context: Context, language: String): Context {
val resources = context.resources
val locale = getLocaleByLanguage(language)
val configuration = resources.configuration
configuration.setLocale(locale)
configuration.setLocales(LocaleList(locale))
return context.createConfigurationContext(configuration)
}
}
注意事项
- android 8.0 以下的版本才需要更新 configuration 和 resources;
- android 6.0 以及以下版本,重新创建 Activity 建议使用 startActivity 方式,并添加启动动画,可以避免页面黑屏和闪烁;
- android 6.0 以上版本,重新创建 Activity 建议使用 recreate() ,可以达到无缝切换的效果。
以上三点,看如下代码更清晰:
/**
* 经过测试:android 8.0 以下的版本需要更新 configuration 和 resources,
* android 8.0 以上只需要将当前的语言环境写入 Sp 文件即可。
* 测试机型 android4.4、android6.0、android7.0、android7.1、android8.1
* 然后,重新创建当前页面。
* @param language
*/
private fun changeLanguage(language: String?) {
// 版本低于 android 8.0 不执行该方法
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 注意,这里的 context 不能传 Application 的 context
LanguageUtil.changeAppLanguage(this, language!!)
}
Sp.put("language", language!!)
// 不同的版本,使用不同的重启方式,达到最好的效果
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
// 6.0 以及以下版本,使用这种方式,并给 activity 添加启动动画效果,可以规避黑屏和闪烁问题
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
finish()
} else {
// 6.0 以上系统直接调用重新创建函数,可以达到无缝切换的效果
recreate()
}
}
源码地址