背景

由于项目原因,需要用到国际化这一部分的知识。并且在 App 中需要动态切换语言,所以花了点时间研究了下具体的实现。并在兼容问题上做了较多的思考,目前兼容了 Android 4.4 到 Android 10 平台。

实现思路

大致思路如下:

  1. 我们通过页面上选择的国家语言标识(比如 zh 代表简体中文,en 代表英语),去拿到系统的 Locale 对象 locale;
  2. 通过 context 拿到系统资源 Resources 对象 resources;
  3. 通过 resources 拿到资源配置 Configuration 对象 configuration;
  4. 将获取到的 locale 通过 configuration.setLocale(locale) 更新到 configuration 中;
  5. 通过 resources 对象拿到 DisplayMetrics 对象 dm,为下一步更新 resources 配置方法提供参数;
  6. 通过 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 源码,就不贴完整的工具类了。下面说一下具体的实现步骤:

  1. 在 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
    }
}
  1. 在基类 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)
    }
}
  1. 首次进入 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)
        }
  1. 绑定按钮点击事件,动态切换语言
// 切换为中文
        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)
    }
}

注意事项

  1. android 8.0 以下的版本才需要更新 configuration 和 resources;
  2. android 6.0 以及以下版本,重新创建 Activity 建议使用 startActivity 方式,并添加启动动画,可以避免页面黑屏和闪烁;
  3. 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()
        }
    }

源码地址

MultiLanguageSwitch