背景
由於項目原因,需要用到國際化這一部分的知識。並且在 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()
}
}