最近公司有了新需求,需要app支持多语言,这个任务分配到了我的头上,目前需要支持英文、中文简体和中文繁体,还有跟随系统语言。可是我没有做过国际化相关的需求,怎么办!当然是上网查了,Google或者百度都OK啦!废话不多说,下面开始进入正题啦
在正式开始之前,先来讲解一下 Android 应用资源国际化的知识。对于资源文件的国际化,在默认的情况下,在 Android src/main/res/ 目录下会有一个默认的values,这个时候不管如何切换语言,应用显示的资源都不会发生任何改变的。比方说你把系统语言切换为英文,那些系统的软件会跟着变成英文,而你的软件却什么都没变,还是中文。这是为什么呢!因为默认只有一个values,不管你切换成什么语言,它在没有找到对应的values文件目录,就会只用默认的values了。那需要怎么做才行,很简单!建立对应语言文件夹,格式一般为:values-语言代号-地区代号,默认的资源是不包含语言代号和地区代号的。
就拿我需要支持英文、中文简体和中文繁体来说吧!因为我是需要支持这三种类型的语言,那我只需要建立中文简体(values-zh-rCN)和中文繁体(values-zh-rTW),并且在目录下建立strings文件,当然还需要在对应的strings中加入对应的语言文案。其它的语言都认为它是英文,使用默认的values就好了
默认的values:
简体中文values-zh-rCN:
繁体中文values-zh-rTW:
让大家看下效果吧!系统语言切换成简体中文、繁体中文和英文的状态
这样关于资源文件values就好了,但是这样就结束了吗?答案是否定的,我们作为一个合格的应用,怎么能让用户每次想切换语言的时候都去切换系统的语言,这样多麻烦呀!我们肯定需要在自己的应用中就支持的呀!
先附上LanguageHelper完整代码,下面会为大家一一讲解
package com.example.jack.languagedemo
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.support.annotation.StringDef
import java.util.Locale
/**
* Created by jack on 2018/9/15.
* Copyright 2018 [email protected]. All rights reserved.
*/
object LanguageHelper {
@StringDef(SYSTEM, SIMPLIFIED_CHINESE, TRADITIONAL_CHINESE, ENGLISH)
@Retention(AnnotationRetention.SOURCE)
annotation class LanguageStatus
const val SYSTEM = "system"
const val SIMPLIFIED_CHINESE = "zh_CN"
const val TRADITIONAL_CHINESE = "zh_TW"
const val ENGLISH = "en_US"
private val SIMPLIFIED_CHINESE_TYPE: Locale = Locale.SIMPLIFIED_CHINESE
private val TRADITIONAL_CHINESE_TYPE: Locale = Locale.TRADITIONAL_CHINESE
private val ENGLISH_TYPE: Locale = Locale.ENGLISH
private const val CHINESE = "zh"
private const val SIMPLIFIED = "CN"
private const val TRADITIONAL = "TW"
private val languagePreference = LanguagePreference(CommonHelper.context)
var languageStatus: String? = null
get() {
if (field == null) {
val languageStatus = languagePreference.getLanguageStatus()
field = if (languageStatus.isNotEmpty()) {
languageStatus
} else {
SYSTEM
}
}
return field
}
fun switchLanguage(context: Context, @LanguageStatus language: String): Context {
saveLanguageStatus(language)
return when (language) {
SYSTEM -> languageCompat(context, systemLanguage())
SIMPLIFIED_CHINESE -> languageCompat(context, SIMPLIFIED_CHINESE_TYPE)
TRADITIONAL_CHINESE -> languageCompat(context, TRADITIONAL_CHINESE_TYPE)
ENGLISH -> languageCompat(context, ENGLISH_TYPE)
else -> context
}
}
private fun languageCompat(context: Context, locale: Locale): Context {
val resources = context.resources ?: return context
val config = resources.configuration ?: return context
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
config.setLocale(locale)
} else {
@Suppress("DEPRECATION")
config.locale = locale
}
if (VERSION.SDK_INT >= VERSION_CODES.N) {
return context.createConfigurationContext(config)
} else {
val dm = resources.displayMetrics ?: return context
@Suppress("DEPRECATION")
resources.updateConfiguration(config, dm)
return context
}
}
private fun saveLanguageStatus(@LanguageStatus languageStatus: String) {
LanguageHelper.languageStatus = languageStatus
languagePreference.setLanguageStatus(languageStatus)
}
fun getLanguageType(): String {
var languageType = languageStatus.toString()
if (languageStatus == SYSTEM) {
val systemLanguage = systemLanguage().language
val systemCountry = systemLanguage().country
if (systemLanguage == CHINESE) {
if (systemCountry == SIMPLIFIED) {
languageType = SIMPLIFIED_CHINESE
} else if (systemCountry == TRADITIONAL) {
languageType = TRADITIONAL_CHINESE
}
} else {
languageType = ENGLISH
}
}
return languageType
}
private fun systemLanguage(): Locale {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 解决了获取系统默认错误的问题
Resources.getSystem().configuration.locales.get(0)
} else {
Locale.getDefault()
}
}
fun isChineseLanguage(): Boolean {
return getLanguageType() != ENGLISH
}
fun getLocal(): Locale {
return when (LanguageHelper.getLanguageType()) {
this.SIMPLIFIED_CHINESE -> SIMPLIFIED_CHINESE_TYPE
this.TRADITIONAL_CHINESE -> TRADITIONAL_CHINESE_TYPE
else -> ENGLISH_TYPE
}
}
private class LanguagePreference(context: Context) {
private val sharedPreferences: SharedPreferences by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
context.applicationContext.getSharedPreferences("languagePreference", Context.MODE_PRIVATE)
}
private val languageStatus = "language_status"
fun setLanguageStatus(languageStatus: String) {
setString(this.languageStatus, languageStatus)
}
fun getLanguageStatus(): String {
return getString(this.languageStatus)
}
fun setString(keyName: String, value: String) {
val editor = sharedPreferences.edit()
editor.putString(keyName, value)
editor.apply()
}
fun getString(keyName: String): String {
val sp = sharedPreferences
return sp.getString(keyName, "")
}
}
}
只需要调用Android原生的API获取到应用内的语言,并且设置成我们想要的语言就好了,需要注意一下的事兼容性问题,Android >= Build.VERSION_CODES.JELLY_BEAN_MR1 用的是config.setLocale(locale),否则用的是config.locale = locale。Android >= Build.VERSION_CODES.N 用的是context.createConfigurationContext(config),它会重新创建一个context,val dm = resources.displayMetrics ?: return context resources.updateConfiguration(config, dm) 则还是原先的context
private fun languageCompat(context: Context, locale: Locale): Context {
val resources = context.resources ?: return context
val config = resources.configuration ?: return context
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
config.setLocale(locale)
} else {
@Suppress("DEPRECATION")
config.locale = locale
}
if (VERSION.SDK_INT >= VERSION_CODES.N) {
return context.createConfigurationContext(config)
} else {
val dm = resources.displayMetrics ?: return context
@Suppress("DEPRECATION")
resources.updateConfiguration(config, dm)
return context
}
}
根据我目前的需求,需要有四种状态:跟随系统、简体中文、繁体中文、英文这些状态,但是语言类型只有三种呀!分别是简体中文、繁体中文、英文这些语言类型,所以还需要把跟随系统的状态转为简体中文、繁体中文、英文的类型。然后就可以切换对应的语言了,再加上本地数据持久化。这边值得一提的是,获取系统默认语言在>=N的版本的情况下Locale.getDefault(),它是获取到你上次设置的应用内语言,不是我们想要的系统语言,需要使用Resources.getSystem().configuration.locales.get(0)才行,这是踩坑踩出来的
@StringDef(SYSTEM, SIMPLIFIED_CHINESE, TRADITIONAL_CHINESE, ENGLISH)
@Retention(AnnotationRetention.SOURCE)
annotation class LanguageStatus
const val SYSTEM = "system"
const val SIMPLIFIED_CHINESE = "zh_CN"
const val TRADITIONAL_CHINESE = "zh_TW"
const val ENGLISH = "en_US"
private val SIMPLIFIED_CHINESE_TYPE: Locale = Locale.SIMPLIFIED_CHINESE
private val TRADITIONAL_CHINESE_TYPE: Locale = Locale.TRADITIONAL_CHINESE
private val ENGLISH_TYPE: Locale = Locale.ENGLISH
private val languagePreference = LanguagePreference(CommonHelper.context)
var languageStatus: String? = null
get() {
if (field == null) {
val languageStatus = languagePreference.getLanguageStatus()
field = if (languageStatus.isNotEmpty()) {
languageStatus
} else {
SYSTEM
}
}
return field
}
fun switchLanguage(context: Context, @LanguageStatus language: String): Context {
saveLanguageStatus(language)
return when (language) {
SYSTEM -> languageCompat(context, systemLanguage())
SIMPLIFIED_CHINESE -> languageCompat(context, SIMPLIFIED_CHINESE_TYPE)
TRADITIONAL_CHINESE -> languageCompat(context, TRADITIONAL_CHINESE_TYPE)
ENGLISH -> languageCompat(context, ENGLISH_TYPE)
else -> context
}
}
private fun systemLanguage(): Locale {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 解决了获取系统默认错误的问题
Resources.getSystem().configuration.locales.get(0)
} else {
Locale.getDefault()
}
}
private class LanguagePreference(context: Context) {
private val sharedPreferences: SharedPreferences by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
context.applicationContext.getSharedPreferences("languagePreference", Context.MODE_PRIVATE)
}
private val languageStatus = "language_status"
fun setLanguageStatus(languageStatus: String) {
setString(this.languageStatus, languageStatus)
}
fun getLanguageStatus(): String {
return getString(this.languageStatus)
}
fun setString(keyName: String, value: String) {
val editor = sharedPreferences.edit()
editor.putString(keyName, value)
editor.apply()
}
fun getString(keyName: String): String {
val sp = sharedPreferences
return sp.getString(keyName, "")
}
}
根据产品需要我需要将当前切换成什么语言告诉接口和网页,需要提供出当前的语言类型。当前项目还有带有时间的自定义控件,需要提供出是否中英文的判断。以及系统的一些时间,需要根据不同的Local,展示出对应的时间格式
private const val CHINESE = "zh"
private const val SIMPLIFIED = "CN"
private const val TRADITIONAL = "TW"
fun getLanguageType(): String {
var languageType = languageStatus.toString()
if (languageStatus == SYSTEM) {
val systemLanguage = systemLanguage().language
val systemCountry = systemLanguage().country
if (systemLanguage == CHINESE) {
if (systemCountry == SIMPLIFIED) {
languageType = SIMPLIFIED_CHINESE
} else if (systemCountry == TRADITIONAL) {
languageType = TRADITIONAL_CHINESE
}
} else {
languageType = ENGLISH
}
}
return languageType
}
fun isChineseLanguage(): Boolean {
return getLanguageType() != ENGLISH
}
fun getLocal(): Locale {
return when (LanguageHelper.getLanguageType()) {
this.SIMPLIFIED_CHINESE -> SIMPLIFIED_CHINESE_TYPE
this.TRADITIONAL_CHINESE -> TRADITIONAL_CHINESE_TYPE
else -> ENGLISH_TYPE
}
}
先附上完整代码:
package com.example.jack.languagedemo
import android.content.Intent
import android.os.Bundle
import android.view.View
import com.example.jack.languagedemo.LanguageHelper.LanguageStatus
import kotlinx.android.synthetic.main.activity_language.englishIcon
import kotlinx.android.synthetic.main.activity_language.englishLayout
import kotlinx.android.synthetic.main.activity_language.simplifiedChineseIcon
import kotlinx.android.synthetic.main.activity_language.simplifiedChineseLayout
import kotlinx.android.synthetic.main.activity_language.tracingSystemIcon
import kotlinx.android.synthetic.main.activity_language.tracingSystemLayout
import kotlinx.android.synthetic.main.activity_language.traditionalChineseIcon
import kotlinx.android.synthetic.main.activity_language.traditionalChineseLayout
/**
* Created by jack on 2018/9/15.
* Copyright 2018 [email protected]. All rights reserved.
*/
class LanguageActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_language)
initStatus()
initView()
}
private fun initStatus() {
when (LanguageHelper.languageStatus) {
LanguageHelper.SYSTEM -> systemStatus()
LanguageHelper.SIMPLIFIED_CHINESE -> simplifiedChineseStatus()
LanguageHelper.TRADITIONAL_CHINESE -> traditionalChineseStatus()
LanguageHelper.ENGLISH -> englishStatus()
else -> systemStatus()
}
}
private fun initView() {
tracingSystemLayout.setOnClickListener {
systemStatus()
switchLanguage(LanguageHelper.SYSTEM)
}
simplifiedChineseLayout.setOnClickListener {
simplifiedChineseStatus()
switchLanguage(LanguageHelper.SIMPLIFIED_CHINESE)
}
traditionalChineseLayout.setOnClickListener {
traditionalChineseStatus()
switchLanguage(LanguageHelper.TRADITIONAL_CHINESE)
}
englishLayout.setOnClickListener {
englishStatus()
switchLanguage(LanguageHelper.ENGLISH)
}
}
private fun systemStatus() {
tracingSystemIcon.visibility = View.VISIBLE
simplifiedChineseIcon.visibility = View.GONE
traditionalChineseIcon.visibility = View.GONE
englishIcon.visibility = View.GONE
}
private fun simplifiedChineseStatus() {
tracingSystemIcon.visibility = View.GONE
simplifiedChineseIcon.visibility = View.VISIBLE
traditionalChineseIcon.visibility = View.GONE
englishIcon.visibility = View.GONE
}
private fun traditionalChineseStatus() {
tracingSystemIcon.visibility = View.GONE
simplifiedChineseIcon.visibility = View.GONE
traditionalChineseIcon.visibility = View.VISIBLE
englishIcon.visibility = View.GONE
}
private fun englishStatus() {
tracingSystemIcon.visibility = View.GONE
simplifiedChineseIcon.visibility = View.GONE
traditionalChineseIcon.visibility = View.GONE
englishIcon.visibility = View.VISIBLE
}
private fun switchLanguage(@LanguageStatus languageStatus: String) {
if (LanguageHelper.languageStatus == languageStatus) return
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
finish()
}
}
首先一进来需要获取到当前处于什么状态,根据状态展示UI效果
private fun initStatus() {
when (LanguageHelper.languageStatus) {
LanguageHelper.SYSTEM -> systemStatus()
LanguageHelper.SIMPLIFIED_CHINESE -> simplifiedChineseStatus()
LanguageHelper.TRADITIONAL_CHINESE -> traditionalChineseStatus()
LanguageHelper.ENGLISH -> englishStatus()
else -> systemStatus()
}
}
private fun systemStatus() {
tracingSystemIcon.visibility = View.VISIBLE
simplifiedChineseIcon.visibility = View.GONE
traditionalChineseIcon.visibility = View.GONE
englishIcon.visibility = View.GONE
}
private fun simplifiedChineseStatus() {
tracingSystemIcon.visibility = View.GONE
simplifiedChineseIcon.visibility = View.VISIBLE
traditionalChineseIcon.visibility = View.GONE
englishIcon.visibility = View.GONE
}
private fun traditionalChineseStatus() {
tracingSystemIcon.visibility = View.GONE
simplifiedChineseIcon.visibility = View.GONE
traditionalChineseIcon.visibility = View.VISIBLE
englishIcon.visibility = View.GONE
}
private fun englishStatus() {
tracingSystemIcon.visibility = View.GONE
simplifiedChineseIcon.visibility = View.GONE
traditionalChineseIcon.visibility = View.GONE
englishIcon.visibility = View.VISIBLE
}
然后就是切语言啦,想切哪个切哪个,切完之后把所有栈退掉,关闭当前页面,跳转至MainActivty。为什么要这样做,我不会关闭当前页面,不退栈不行吗?一会你就知道了
private fun initView() {
tracingSystemLayout.setOnClickListener {
systemStatus()
switchLanguage(LanguageHelper.SYSTEM)
}
simplifiedChineseLayout.setOnClickListener {
simplifiedChineseStatus()
switchLanguage(LanguageHelper.SIMPLIFIED_CHINESE)
}
traditionalChineseLayout.setOnClickListener {
traditionalChineseStatus()
switchLanguage(LanguageHelper.TRADITIONAL_CHINESE)
}
englishLayout.setOnClickListener {
englishStatus()
switchLanguage(LanguageHelper.ENGLISH)
}
}
private fun switchLanguage(@LanguageStatus languageStatus: String) {
if (LanguageHelper.languageStatus == languageStatus) return
CommonHelper.context = LanguageHelper.switchLanguage(CommonHelper.context, languageStatus)
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
finish()
}
这样就完了吗?答案是否定的,我们最重要的地方来了。思考一下,我们为什么可以实现多语言切换?前面说了根据系统切换语言是在Android src/main/res/ 的下建立对应的values,这样就能找到对应的strings文件,从而相匹配对应。不管是AndroidManifest还是布局文件都是.xml的格式,最终都会转化成java或kotlin代码,在java或kotlin代码中是怎样拿到string的呢!getString()拿到的对吧,而getString()又依赖于Context。关键点就在Context,先说说系统默认实现吧!当你切换系统语言的时候,系统会拿到你切换的语言和Context相关联(具体怎么实现自己看源码吧),这样Context里面的getString()就可以找到对应的values从而拿到对应的strings展示了。好吧!前面做了这么多,就是为了拿到一个带有语言信息的Context,所以我们只需要把这个Context替换掉原先的Context就好了。建立一个BaseActivity,让所有Activity继承它,重写attachBaseContext(),替换掉原来的Context。说到这里应该知道为什么切换完语言之后要退栈关闭本页面了吧!因为attachBaseContext()只有在第一次进入的时候才会执行
override fun attachBaseContext(newBase: Context) {
val context = LanguageHelper.languageStatus?.let { languageStatus ->
LanguageHelper.switchLanguage(newBase, languageStatus)
}
super.attachBaseContext(context)
}
看看效果吧!
不知道大家有没有发现,主界面和多语言(ActionBar)一直都是简体中文,并没有根据切换语言而改变,正常来说是不会的呀!哈哈,我是在AndroidManiest中设置ActionBar-title
为什么会这样呢!好像是因为AndroidManifest只会执行一次吧!切换语言的时候,它不知道呀!那我就想再在这设置便于统一管理,不想在代码中设置怎么办?它不是只会执行一次不是,那我就让它执行多次好了,在BaseActivity中重写onCreate(),动态的去获取在AndroidManiest中设置的label,然后设置给ActionBar,toolbar同理!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
title = getLabel()
}
private fun getLabel(): String? {
var label: String? = null
try {
val activityInfo = packageManager.getActivityInfo(componentName, 0) ?: return null
label = if (activityInfo.labelRes != 0) {
getString(activityInfo.labelRes)
} else {
activityInfo.nonLocalizedLabel as String?
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return label
}
看看效果如何:
这样Activty级别的多语言切换就OK了
一般来说,项目中都会有Application的Context,我这个是不是不生效呀!那我们就试一试
package com.example.jack.languagedemo
import android.app.Application
import android.content.Context
/**
* Created by jack on 2018/9/15.
* Copyright 2018 [email protected]. All rights reserved.
*/
class AppApplication : Application() {
override fun onCreate() {
super.onCreate()
CommonHelper.context = this
}
}
package com.example.jack.languagedemo
import android.annotation.SuppressLint
import android.content.Context
/**
* Created by jack on 2018/9/15.
* Copyright 2018 [email protected]. All rights reserved.
*/
@SuppressLint("StaticFieldLeak")
object CommonHelper {
/**
* Application context.
*/
lateinit var context: Context
}
package com.example.jack.languagedemo
import android.content.Intent
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.beautifulTextView
import kotlinx.android.synthetic.main.activity_main.mainLanguage
class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
}
private fun initView() {
beautifulTextView.text = CommonHelper.context.getString(R.string.beautiful)
mainLanguage.setOnClickListener({
startActivity(Intent(this@MainActivity, LanguageActivity::class.java))
})
}
}
看看效果:
今天天气不错啊!这段文字一直都是简体,并没有跟随语言切换。所以还需要对Application的Context进行处理,可是之前Activtiy切换完要退栈,那我Application切换完岂不是需要重启应用了,不好不好。还好Application的Context是全局独有而一直存在的,就不用考虑内存泄漏的问题了,我只要把带语言信息的Context保存下来,用的时候直接用这个就好了。修改两处地方,初始化和切换语言的时候
Application这边需要注意的是需要重写attachBaseContext(),因为LanguageHelper.languageStatus用到了s p,它是需要Context的,所以需要先取一下Context
package com.example.jack.languagedemo
import android.app.Application
import android.content.Context
/**
* Created by jack on 2018/9/15.
* Copyright 2018 [email protected]. All rights reserved.
*/
class AppApplication : Application() {
override fun onCreate() {
super.onCreate()
LanguageHelper.languageStatus?.let { languageStatus ->
CommonHelper.context = LanguageHelper.switchLanguage(this, languageStatus)
}
}
override fun attachBaseContext(base: Context) {
CommonHelper.context = base
super.attachBaseContext(base)
}
}
LanguageActivity这边需要注意的是不能给LanguageHelper.switchLanguage()传入Activity的Context,需要传入CommHelper的Context,不然Activity的Context就被静态引用了,N以下的设备就会引起内存泄漏了,大于等于N的是重新创建的Context,所以不会
private fun switchLanguage(@LanguageStatus languageStatus: String) {
if (LanguageHelper.languageStatus == languageStatus) return
CommonHelper.context = LanguageHelper.switchLanguage(CommonHelper.context, languageStatus)
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
finish()
}
看看效果:
好了,多语言切换就到此结束了