Android 國際化之動態語言切換(兼容 Android 4.4 - Android 10)

背景

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章