├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── issue_zh_template_bug.yml │ ├── issue_zh_template_question.yml │ └── issue_zh_template_suggest.yml └── workflows │ └── android.yml ├── .gitignore ├── HelpDoc.md ├── LICENSE ├── README.md ├── app ├── AppSignature.jks ├── build.gradle ├── gradle.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── hjq │ │ └── language │ │ └── demo │ │ ├── ActivityManager.java │ │ ├── AppApplication.java │ │ ├── BaseActivity.java │ │ ├── LanguagesWebView.java │ │ └── MainActivity.java │ └── res │ ├── anim │ ├── activity_alpha_in.xml │ └── activity_alpha_out.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-en │ └── strings.xml │ ├── values-v23 │ └── styles.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── network_security_config.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── hjq │ └── language │ ├── ActivityLanguages.java │ ├── ConfigurationObserver.java │ ├── LanguagesConfig.java │ ├── LanguagesUtils.java │ ├── LocaleChangeReceiver.java │ ├── LocaleContract.java │ ├── MultiLanguages.java │ └── OnLanguageListener.java ├── picture ├── demo_code.png └── dynamic_figure.gif └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_zh_template_bug.yml: -------------------------------------------------------------------------------- 1 | name: 提交 Bug 2 | description: 请告诉我框架存在的问题,我会协助你解决此问题! 3 | title: "[Bug]:" 4 | labels: ["bug"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) 11 | - type: input 12 | id: input_id_1 13 | attributes: 14 | label: 框架版本【必填】 15 | description: 请输入你使用的框架版本 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: input_id_2 20 | attributes: 21 | label: 问题描述【必填】 22 | description: 请输入你对这个问题的描述 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: input_id_3 27 | attributes: 28 | label: 复现步骤【必填】 29 | description: 请输入问题的复现步骤 30 | validations: 31 | required: true 32 | - type: dropdown 33 | id: input_id_4 34 | attributes: 35 | label: 是否必现【必填】 36 | multiple: false 37 | options: 38 | - 未选择 39 | - 是 40 | - 否 41 | validations: 42 | required: true 43 | - type: input 44 | id: input_id_5 45 | attributes: 46 | label: 项目 targetSdkVersion【必填】 47 | validations: 48 | required: true 49 | - type: input 50 | id: input_id_6 51 | attributes: 52 | label: 出现问题的手机信息【必填】 53 | description: 请填写出现问题的品牌和机型 54 | validations: 55 | required: true 56 | - type: input 57 | id: input_id_7 58 | attributes: 59 | label: 出现问题的安卓版本【必填】 60 | description: 请填写出现问题的 Android 版本 61 | validations: 62 | required: true 63 | - type: dropdown 64 | id: input_id_8 65 | attributes: 66 | label: 问题信息的来源渠道【必填】 67 | multiple: true 68 | options: 69 | - 自己遇到的 70 | - Bugly 看到的 71 | - 用户反馈 72 | - 其他渠道 73 | - type: input 74 | id: input_id_9 75 | attributes: 76 | label: 是部分机型还是所有机型都会出现【必答】 77 | description: 部分/全部(例如:某为,某 Android 版本会出现) 78 | validations: 79 | required: true 80 | - type: dropdown 81 | id: input_id_10 82 | attributes: 83 | label: 框架最新的版本是否存在这个问题【必答】 84 | description: 如果用的是旧版本的话,建议升级看问题是否还存在 85 | multiple: false 86 | options: 87 | - 未选择 88 | - 是 89 | - 否 90 | validations: 91 | required: true 92 | - type: dropdown 93 | id: input_id_11 94 | attributes: 95 | label: 框架文档是否提及了该问题【必答】 96 | description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 97 | multiple: false 98 | options: 99 | - 未选择 100 | - 是 101 | - 否 102 | validations: 103 | required: true 104 | - type: dropdown 105 | id: input_id_12 106 | attributes: 107 | label: 是否已经查阅框架文档但还未能解决的【必答】 108 | description: 如果查阅了文档但还是没有解决的话,可以选择是 109 | multiple: false 110 | options: 111 | - 未选择 112 | - 是 113 | - 否 114 | validations: 115 | required: true 116 | - type: dropdown 117 | id: input_id_13 118 | attributes: 119 | label: issue 列表中是否有人曾提过类似的问题【必答】 120 | description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 121 | multiple: false 122 | options: 123 | - 未选择 124 | - 是 125 | - 否 126 | validations: 127 | required: true 128 | - type: dropdown 129 | id: input_id_14 130 | attributes: 131 | label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 132 | description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 133 | multiple: false 134 | options: 135 | - 未选择 136 | - 是 137 | - 否 138 | validations: 139 | required: true 140 | - type: dropdown 141 | id: input_id_15 142 | attributes: 143 | label: 是否可以通过 Demo 来复现该问题【必答】 144 | description: 排查一下是不是自己的项目代码写得有问题导致的 145 | multiple: false 146 | options: 147 | - 未选择 148 | - 是 149 | - 否 150 | validations: 151 | required: true 152 | - type: textarea 153 | id: input_id_16 154 | attributes: 155 | label: 提供报错堆栈 156 | description: 如果有报错的话必填,注意不要拿被混淆过的代码堆栈上来 157 | render: text 158 | validations: 159 | required: false 160 | - type: textarea 161 | id: input_id_17 162 | attributes: 163 | label: 提供截图或视频 164 | description: 根据需要提供,此项不强制 165 | validations: 166 | required: false 167 | - type: textarea 168 | id: input_id_18 169 | attributes: 170 | label: 提供解决方案 171 | description: 如果已经解决了的话,此项不强制 172 | validations: 173 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_zh_template_question.yml: -------------------------------------------------------------------------------- 1 | name: 提出疑问 2 | description: 提出你的困惑,我会给你解答 3 | title: "[疑惑]:" 4 | labels: ["question"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) 11 | - type: textarea 12 | id: input_id_1 13 | attributes: 14 | label: 问题描述【必填】 15 | description: 请描述一下你的问题(注意:如果确定是框架 bug 请不要在这里提,否则一概不受理) 16 | validations: 17 | required: true 18 | - type: dropdown 19 | id: input_id_2 20 | attributes: 21 | label: 框架文档是否提及了该问题【必答】 22 | description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 23 | multiple: false 24 | options: 25 | - 未选择 26 | - 是 27 | - 否 28 | validations: 29 | required: true 30 | - type: dropdown 31 | id: input_id_3 32 | attributes: 33 | label: 是否已经查阅框架文档但还未能解决的【必答】 34 | description: 如果查阅了文档但还是没有解决的话,可以选择是 35 | multiple: false 36 | options: 37 | - 未选择 38 | - 是 39 | - 否 40 | validations: 41 | required: true 42 | - type: dropdown 43 | id: input_id_4 44 | attributes: 45 | label: issue 列表中是否有人曾提过类似的问题【必答】 46 | description: 可以在 issue 列表在搜索问题关键字,参考一下别人的解决方案 47 | multiple: false 48 | options: 49 | - 未选择 50 | - 是 51 | - 否 52 | validations: 53 | required: true 54 | - type: dropdown 55 | id: input_id_5 56 | attributes: 57 | label: 是否已经搜索过了 issue 列表但还未能解决的【必答】 58 | description: 如果搜索过了 issue 列表但是问题没有解决的话,可以选择是 59 | multiple: false 60 | options: 61 | - 未选择 62 | - 是 63 | - 否 64 | validations: 65 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_zh_template_suggest.yml: -------------------------------------------------------------------------------- 1 | name: 提交建议 2 | description: 请告诉我框架的不足之处,让我做得更好! 3 | title: "[建议]:" 4 | labels: ["help wanted"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | ## [【警告:请务必按照 issue 模板填写,不要抱有侥幸心理,一旦发现 issue 没有按照模板认真填写,一律直接关闭】](https://github.com/getActivity/IssueTemplateGuide) 11 | - type: textarea 12 | id: input_id_1 13 | attributes: 14 | label: 你觉得框架有什么不足之处?【必答】 15 | description: 你可以描述框架有什么令你不满意的地方 16 | validations: 17 | required: true 18 | - type: dropdown 19 | id: input_id_2 20 | attributes: 21 | label: issue 是否有人曾提过类似的建议?【必答】 22 | description: 一旦出现重复提问我将不会再次解答 23 | multiple: false 24 | options: 25 | - 未选择 26 | - 是 27 | - 否 28 | validations: 29 | required: true 30 | - type: dropdown 31 | id: input_id_3 32 | attributes: 33 | label: 框架文档是否提及了该问题【必答】 34 | description: 文档会提供最常见的问题解答,可以先看看是否有自己想要的 35 | multiple: false 36 | options: 37 | - 未选择 38 | - 是 39 | - 否 40 | validations: 41 | required: true 42 | - type: dropdown 43 | id: input_id_4 44 | attributes: 45 | label: 是否已经查阅框架文档但还未能解决的【必答】 46 | description: 如果查阅了文档但还是没有解决的话,可以选择是 47 | multiple: false 48 | options: 49 | - 未选择 50 | - 是 51 | - 否 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: input_id_5 56 | attributes: 57 | label: 你觉得该怎么去完善会比较好?【非必答】 58 | description: 你可以提供一下自己的想法或者做法供作者参考 59 | validations: 60 | required: false -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /build 4 | */build 5 | /captures 6 | /.cxx 7 | */.cxx 8 | /.externalNativeBuild 9 | 10 | ._* 11 | *.iml 12 | .DS_Store 13 | local.properties -------------------------------------------------------------------------------- /HelpDoc.md: -------------------------------------------------------------------------------- 1 | #### 目录 2 | 3 | * [如何设置 App 默认的语种](#如何设置-app-默认的语种) 4 | 5 | * [切换语种后没有任何效果该怎么办](#切换语种后没有任何效果该怎么办) 6 | 7 | * [怎么在切换语种后应用到所有 Activity 上](#怎么在切换语种后应用到所有-activity-上) 8 | 9 | * [怎么在用户切换系统语种的时候刷新所有界面](#怎么在用户切换系统语种的时候刷新所有界面) 10 | 11 | * [在系统切换语种返回应用后没有生效怎么办](#在系统切换语种返回应用后没有生效怎么办) 12 | 13 | * [WebView 导致语种失效的解决方案](#webview-导致语种失效的解决方案) 14 | 15 | * [Android 13 WebView 语种失效怎么办](#android-13-webview-语种失效怎么办) 16 | 17 | * [有没有一种不用通过重启的方式来切换语种](#有没有一种不用通过重启的方式来切换语种) 18 | 19 | #### 如何设置 App 默认的语种 20 | 21 | * 在从来没有调用过 `MultiLanguages.setAppLanguage` 的情况下,又不想让应用跟随系统的语种,而是想指定某个语种该怎么做?具体写法示例如下: 22 | 23 | ```java 24 | public final class XxxApplication extends Application { 25 | 26 | static { 27 | // 设置默认的语种(越早设置越好) 28 | MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 29 | } 30 | } 31 | ``` 32 | 33 | * 当然你如果想判断当前的系统语种类型,然后再设置默认的语种,可以这样写: 34 | 35 | ```java 36 | public final class XxxApplication extends Application { 37 | 38 | @Override 39 | protected void attachBaseContext(Context newBase) { 40 | if (newBase != null) { 41 | Locale systemLanguage = MultiLanguages.getSystemLanguage(newBase); 42 | // 如果当前语种既不是中文(包含简体和繁体)和英语(包含美式英式等),就默认设置成英文的,避免跟随系统语种 43 | if (!MultiLanguages.equalsLanguage(systemLanguage, LocaleContract.getChineseLocale()) && 44 | !MultiLanguages.equalsLanguage(systemLanguage, LocaleContract.getEnglishLocale())) { 45 | MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 46 | } 47 | } 48 | // 绑定语种 49 | super.attachBaseContext(MultiLanguages.attach(newBase)); 50 | } 51 | } 52 | ``` 53 | 54 | #### 切换语种后没有任何效果该怎么办 55 | 56 | * 情况一:可以检查一下是否在 `build.gradle` 文件中配置了仅保留某个国家的语种资源,例如 `resConfigs 'zh'` 就代表只保留和中文相关的语种资源,而其他国家的语种资源就不会被打包进 apk 包中,这样就会导致在切换语种的时候始终都是中文的尴尬局面。 57 | 58 | * 情况二:如果是 Fragment 里面的语种切换之后没有变化,请检查 Fragment 在 Activity 重启之后是否被复用了,一般情况下是调用了 `fragment.setRetainInstance(true)` 导致的。 59 | 60 | * 情况三:如果是上架 GooglePlay 或者华为的 aab 包后,上架成功后再下载发现切换不了 App 语种,那是因为在分包的时候导致多语言资源缺失了,这时候需要对主模块的 `build.gradle` 文件进行配置,具体用法可以参考[官方文档](https://developer.android.google.cn/guide/app-bundle/configure-base?hl=zh-cn) 61 | 62 | ```groovy 63 | android { 64 | 65 | ...... 66 | 67 | bundle { 68 | 69 | ...... 70 | 71 | language { 72 | enableSplit = false 73 | } 74 | 75 | ...... 76 | } 77 | } 78 | ``` 79 | 80 | * 其他情况:如果不是以上原因造成的,请提一个 [issue](https://github.com/getActivity/MultiLanguages/issues) 给到我处理。 81 | 82 | #### 怎么在切换语种后应用到所有 Activity 上 83 | 84 | * 在 Application 的 onCreate 方法中加入以下代码 85 | 86 | ```java 87 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 88 | 89 | @Override 90 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 91 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 92 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 93 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 94 | List activityList = getAllActivity(); 95 | for (Activity activity : activityList) { 96 | activity.recreate(); 97 | } 98 | } 99 | 100 | @Override 101 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 102 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 103 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 104 | } 105 | }); 106 | ``` 107 | 108 | #### 怎么在用户切换系统语种的时候刷新所有界面 109 | 110 | * 在 Application 的 onCreate 方法中加入以下代码 111 | 112 | ```java 113 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 114 | 115 | @Override 116 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 117 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 118 | } 119 | 120 | @Override 121 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 122 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 123 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 124 | 125 | // 当前语种是否是跟随系统,如果不是则不往下执行 126 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 127 | return; 128 | } 129 | 130 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 131 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 132 | List activityList = getAllActivity(); 133 | for (Activity activity : activityList) { 134 | activity.recreate(); 135 | } 136 | } 137 | }); 138 | ``` 139 | 140 | #### 在系统切换语种返回应用后没有生效怎么办 141 | 142 | * 请检查在 Activity 配置了 `android:configChanges` 属性,有的话请去掉再进行尝试,这个属性的文档请查看 [``](https://developer.android.google.cn/guide/topics/manifest/activity-element?hl=zh) 143 | 144 | ```xml 145 | 148 | ``` 149 | 150 | * 如果你不想去除 `android:configChanges` 属性,又想切换系统语种后生效,在 Application 的 onCreate 方法中加入以下代码 151 | 152 | ```java 153 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 154 | 155 | @Override 156 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 157 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 158 | } 159 | 160 | @Override 161 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 162 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 163 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 164 | 165 | // 当前语种是否是跟随系统,如果不是则不往下执行 166 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 167 | return; 168 | } 169 | 170 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 171 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 172 | List activityList = getAllActivity(); 173 | for (Activity activity : activityList) { 174 | activity.recreate(); 175 | } 176 | } 177 | }); 178 | ``` 179 | 180 | #### WebView 导致语种失效的解决方案 181 | 182 | * 由于 WebView 初始化会修改 Activity 语种配置,间接导致 Activity 语种会被还原回去,所以需要你手动重写 WebView 对这个问题进行修复 183 | 184 | ```java 185 | public final class LanguagesWebView extends WebView { 186 | 187 | public LanguagesWebView(@NonNull Context context) { 188 | this(context, null); 189 | } 190 | 191 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs) { 192 | this(context, attrs, android.R.attr.webViewStyle); 193 | } 194 | 195 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 196 | super(context, attrs, defStyleAttr); 197 | 198 | // 修复 WebView 初始化时会修改 Activity 语种配置的问题 199 | MultiLanguages.updateAppLanguage(context); 200 | } 201 | } 202 | ``` 203 | 204 | #### Android 13 WebView 语种失效怎么办 205 | 206 | * 将 `webView.loadUrl(@NonNull String url)` 替换成 `webView.loadUrl(@NonNull String url, @NonNull Map additionalHttpHeaders)`,并添加 `Accept-Language` 请求头即可,具体的示例代码如下: 207 | 208 | ```java 209 | public final class MainActivity extends Activity { 210 | 211 | private WebView mWebView; 212 | 213 | @Override 214 | protected void onCreate(Bundle savedInstanceState) { 215 | super.onCreate(savedInstanceState); 216 | setContentView(R.layout.activity_main); 217 | 218 | mWebView = findViewById(R.id.wv_main_web); 219 | 220 | mWebView.setWebViewClient(new LanguagesViewClient()); 221 | mWebView.setWebChromeClient(new WebChromeClient()); 222 | mWebView.loadUrl("https://developer.android.google.cn/kotlin", generateLanguageRequestHeader(this)); 223 | } 224 | 225 | public static class LanguagesViewClient extends WebViewClient { 226 | 227 | @TargetApi(Build.VERSION_CODES.N) 228 | @Override 229 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 230 | return shouldOverrideUrlLoading(view, request.getUrl().toString()); 231 | } 232 | 233 | @Override 234 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 235 | String scheme = Uri.parse(url).getScheme(); 236 | if (scheme == null) { 237 | return false; 238 | } 239 | switch (scheme) { 240 | // 如果这是跳链接操作 241 | case "http": 242 | case "https": 243 | view.loadUrl(url, generateLanguageRequestHeader(view.getContext())); 244 | break; 245 | default: 246 | break; 247 | } 248 | return true; 249 | } 250 | } 251 | 252 | /** 253 | * 给 WebView 请求头添加语种环境 254 | */ 255 | @NonNull 256 | public static Map generateLanguageRequestHeader(Context context) { 257 | Map map = new HashMap<>(1); 258 | // Android 13 上面语种失效的问题解决方案 259 | // https://developer.android.google.cn/about/versions/13/features/app-languages?hl=zh-cn#consider-header 260 | map.put("Accept-Language", String.valueOf(MultiLanguages.getAppLanguage(context))); 261 | return map; 262 | } 263 | } 264 | ``` 265 | 266 | #### 有没有一种不用通过重启的方式来切换语种 267 | 268 | * 我先问大家一个问题,生米煮成熟饭了,怎么从熟饭变成生米?这显然是不现实的,退一万步讲,假设框架能做到,文字和图片都能自动跟随语种的变化而变化,那么通过接口请求的数据又怎么切换语种?是不是得重新请求?如果是列表数据是不是得从第 1 页开始请求?再问大家一个问题,还有语种切换是一个常用动作吗?我相信大家此时心里已经有了答案。 269 | 270 | * 所以并不是做不到不用重启的效果,而是没有那个必要(切语种不是常用动作),并且存在一定的硬伤(虽然 UI 层不用动,但是数据层还是要重新请求)。 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, July 2019 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Huang JinQun 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 语种切换框架 2 | 3 | * 项目地址:[Github](https://github.com/getActivity/MultiLanguages) 4 | 5 | * 可以扫码下载 Demo 进行演示或者测试,如果扫码下载不了的,[点击此处可直接下载](https://github.com/getActivity/MultiLanguages/releases/download/9.5/MultiLanguages.apk) 6 | 7 | ![](picture/demo_code.png) 8 | 9 | ![](picture/dynamic_figure.gif) 10 | 11 | #### 集成步骤 12 | 13 | * 如果你的项目 Gradle 配置是在 `7.0 以下`,需要在 `build.gradle` 文件中加入 14 | 15 | ```groovy 16 | allprojects { 17 | repositories { 18 | // JitPack 远程仓库:https://jitpack.io 19 | maven { url 'https://jitpack.io' } 20 | } 21 | } 22 | ``` 23 | 24 | * 如果你的 Gradle 配置是 `7.0 及以上`,则需要在 `settings.gradle` 文件中加入 25 | 26 | ```groovy 27 | dependencyResolutionManagement { 28 | repositories { 29 | // JitPack 远程仓库:https://jitpack.io 30 | maven { url 'https://jitpack.io' } 31 | } 32 | } 33 | ``` 34 | 35 | * 配置完远程仓库后,在项目 app 模块下的 `build.gradle` 文件中加入远程依赖 36 | 37 | ```groovy 38 | dependencies { 39 | // 语种切换框架:https://github.com/getActivity/MultiLanguages 40 | implementation 'com.github.getActivity:MultiLanguages:9.5' 41 | } 42 | ``` 43 | 44 | #### 初始化框架 45 | 46 | * 在 Application 中初始化框架 47 | 48 | ```java 49 | public final class XxxApplication extends Application { 50 | 51 | @Override 52 | public void onCreate() { 53 | super.onCreate(); 54 | 55 | // 初始化语种切换框架 56 | MultiLanguages.init(this); 57 | } 58 | } 59 | ``` 60 | 61 | * 重写 Application 的 attachBaseContext 方法 62 | 63 | ```java 64 | @Override 65 | protected void attachBaseContext(Context base) { 66 | // 绑定语种 67 | super.attachBaseContext(MultiLanguages.attach(base)); 68 | } 69 | ``` 70 | 71 | * 重写**基类** BaseActivity 的 attachBaseContext 方法 72 | 73 | ```java 74 | @Override 75 | protected void attachBaseContext(Context newBase) { 76 | // 绑定语种 77 | super.attachBaseContext(MultiLanguages.attach(newBase)); 78 | } 79 | ``` 80 | 81 | * 只要是 Context 的子类都需要重写,Service 也雷同,这里不再赘述 82 | 83 | * 温馨提示:Fragment 不需要重写此方法,因为它不是 Context 的子类 84 | 85 | #### 语种设置 86 | 87 | ```java 88 | // 设置当前的语种(返回 true 表示需要重启 App) 89 | MultiLanguages.setAppLanguage(Context context, Locale locale); 90 | 91 | // 获取当前的语种 92 | MultiLanguages.getAppLanguage(Context context); 93 | 94 | // 跟随系统语种(返回 true 表示需要重启 App) 95 | MultiLanguages.clearAppLanguage(Context context); 96 | ``` 97 | 98 | #### 其他 API 99 | 100 | ```java 101 | // 获取系统的语种 102 | MultiLanguages.getSystemLanguage(Context context); 103 | // 是否跟随系统的语种 104 | MultiLanguages.isSystemLanguage(Context context); 105 | 106 | // 对比两个语言是否是同一个语种(比如:中文有简体和繁体,英语有美式和英式) 107 | MultiLanguages.equalsLanguage(Locale locale1, Locale locale2); 108 | // 对比两个语言是否是同一个地方的(比如:中国大陆用的中文简体,中国台湾用的中文繁体) 109 | MultiLanguages.equalsCountry(Locale locale1, Locale locale2); 110 | 111 | // 获取某个语种下的 String 112 | MultiLanguages.getLanguageString(Context context, Locale locale, int stringId); 113 | // 生成某个语种下的 Resources 对象 114 | MultiLanguages.generateLanguageResources(Context context, Locale locale); 115 | 116 | // 更新 Context 的语种 117 | MultiLanguages.updateAppLanguage(Context context); 118 | // 更新 Resources 的语种 119 | MultiLanguages.updateAppLanguage(Resources resources); 120 | 121 | // 设置默认的语种(越早设置越好) 122 | MultiLanguages.setDefaultLanguage(Locale locale); 123 | ``` 124 | 125 | #### 语种变化监听器 126 | 127 | ```java 128 | // 设置语种变化监听器 129 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 130 | 131 | @Override 132 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 133 | Log.d("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 134 | } 135 | 136 | @Override 137 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 138 | Log.d("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + ",是否跟随系统:" + MultiLanguages.isSystemLanguage()); 139 | } 140 | }); 141 | ``` 142 | 143 | #### 使用案例 144 | 145 | ```java 146 | @Override 147 | public void onClick(View v) { 148 | // 是否需要重启 149 | boolean restart; 150 | switch (v.getId()) { 151 | // 跟随系统 152 | case R.id.btn_language_auto: 153 | restart = MultiLanguages.clearAppLanguage(this); 154 | break; 155 | // 简体中文 156 | case R.id.btn_language_cn: 157 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getSimplifiedChineseLocale()); 158 | break; 159 | // 繁体中文 160 | case R.id.btn_language_tw: 161 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getTraditionalChineseLocale()); 162 | break; 163 | // 英语 164 | case R.id.btn_language_en: 165 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getEnglishLocale()); 166 | break; 167 | default: 168 | restart = false; 169 | break; 170 | } 171 | 172 | if (restart) { 173 | // 我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果 174 | Intent intent = new Intent(this, MainActivity.class); 175 | // Github 地址:https://github.com/getActivity/MultiLanguages/issues/55 176 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 177 | startActivity(intent); 178 | overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 179 | finish(); 180 | } 181 | } 182 | ``` 183 | 184 | ## [常见疑问请点击此处查看](HelpDoc.md) 185 | 186 | #### 其他资源:[语言代码列表大全](https://github.com/championswimmer/android-locales) 187 | 188 | #### 作者的其他开源项目 189 | 190 | * 安卓技术中台:[AndroidProject](https://github.com/getActivity/AndroidProject) ![](https://img.shields.io/github/stars/getActivity/AndroidProject.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject.svg) 191 | 192 | * 安卓技术中台 Kt 版:[AndroidProject-Kotlin](https://github.com/getActivity/AndroidProject-Kotlin) ![](https://img.shields.io/github/stars/getActivity/AndroidProject-Kotlin.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidProject-Kotlin.svg) 193 | 194 | * 权限框架:[XXPermissions](https://github.com/getActivity/XXPermissions) ![](https://img.shields.io/github/stars/getActivity/XXPermissions.svg) ![](https://img.shields.io/github/forks/getActivity/XXPermissions.svg) 195 | 196 | * 吐司框架:[Toaster](https://github.com/getActivity/Toaster) ![](https://img.shields.io/github/stars/getActivity/Toaster.svg) ![](https://img.shields.io/github/forks/getActivity/Toaster.svg) 197 | 198 | * 网络框架:[EasyHttp](https://github.com/getActivity/EasyHttp) ![](https://img.shields.io/github/stars/getActivity/EasyHttp.svg) ![](https://img.shields.io/github/forks/getActivity/EasyHttp.svg) 199 | 200 | * 标题栏框架:[TitleBar](https://github.com/getActivity/TitleBar) ![](https://img.shields.io/github/stars/getActivity/TitleBar.svg) ![](https://img.shields.io/github/forks/getActivity/TitleBar.svg) 201 | 202 | * 悬浮窗框架:[EasyWindow](https://github.com/getActivity/EasyWindow) ![](https://img.shields.io/github/stars/getActivity/EasyWindow.svg) ![](https://img.shields.io/github/forks/getActivity/EasyWindow.svg) 203 | 204 | * Shape 框架:[ShapeView](https://github.com/getActivity/ShapeView) ![](https://img.shields.io/github/stars/getActivity/ShapeView.svg) ![](https://img.shields.io/github/forks/getActivity/ShapeView.svg) 205 | 206 | * Gson 解析容错:[GsonFactory](https://github.com/getActivity/GsonFactory) ![](https://img.shields.io/github/stars/getActivity/GsonFactory.svg) ![](https://img.shields.io/github/forks/getActivity/GsonFactory.svg) 207 | 208 | * 日志查看框架:[Logcat](https://github.com/getActivity/Logcat) ![](https://img.shields.io/github/stars/getActivity/Logcat.svg) ![](https://img.shields.io/github/forks/getActivity/Logcat.svg) 209 | 210 | * Android 版本适配:[AndroidVersionAdapter](https://github.com/getActivity/AndroidVersionAdapter) ![](https://img.shields.io/github/stars/getActivity/AndroidVersionAdapter.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidVersionAdapter.svg) 211 | 212 | * Android 代码规范:[AndroidCodeStandard](https://github.com/getActivity/AndroidCodeStandard) ![](https://img.shields.io/github/stars/getActivity/AndroidCodeStandard.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidCodeStandard.svg) 213 | 214 | * Android 资源大汇总:[AndroidIndex](https://github.com/getActivity/AndroidIndex) ![](https://img.shields.io/github/stars/getActivity/AndroidIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidIndex.svg) 215 | 216 | * Android 开源排行榜:[AndroidGithubBoss](https://github.com/getActivity/AndroidGithubBoss) ![](https://img.shields.io/github/stars/getActivity/AndroidGithubBoss.svg) ![](https://img.shields.io/github/forks/getActivity/AndroidGithubBoss.svg) 217 | 218 | * Studio 精品插件:[StudioPlugins](https://github.com/getActivity/StudioPlugins) ![](https://img.shields.io/github/stars/getActivity/StudioPlugins.svg) ![](https://img.shields.io/github/forks/getActivity/StudioPlugins.svg) 219 | 220 | * 表情包大集合:[EmojiPackage](https://github.com/getActivity/EmojiPackage) ![](https://img.shields.io/github/stars/getActivity/EmojiPackage.svg) ![](https://img.shields.io/github/forks/getActivity/EmojiPackage.svg) 221 | 222 | * AI 资源大汇总:[AiIndex](https://github.com/getActivity/AiIndex) ![](https://img.shields.io/github/stars/getActivity/AiIndex.svg) ![](https://img.shields.io/github/forks/getActivity/AiIndex.svg) 223 | 224 | * 省市区 Json 数据:[ProvinceJson](https://github.com/getActivity/ProvinceJson) ![](https://img.shields.io/github/stars/getActivity/ProvinceJson.svg) ![](https://img.shields.io/github/forks/getActivity/ProvinceJson.svg) 225 | 226 | * Markdown 语法文档:[MarkdownDoc](https://github.com/getActivity/MarkdownDoc) ![](https://img.shields.io/github/stars/getActivity/MarkdownDoc.svg) ![](https://img.shields.io/github/forks/getActivity/MarkdownDoc.svg) 227 | 228 | #### 微信公众号:Android轮子哥 229 | 230 | ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/official_ccount.png) 231 | 232 | #### Android 技术 Q 群:10047167 233 | 234 | #### 如果您觉得我的开源库帮你节省了大量的开发时间,请扫描下方的二维码随意打赏,要是能打赏个 10.24 :monkey_face:就太:thumbsup:了。您的支持将鼓励我继续创作:octocat:([点击查看捐赠列表](https://github.com/getActivity/Donate)) 235 | 236 | ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_ali.png) ![](https://raw.githubusercontent.com/getActivity/Donate/master/picture/pay_wechat.png) 237 | 238 | ## License 239 | 240 | ```text 241 | Copyright 2019 Huang JinQun 242 | 243 | Licensed under the Apache License, Version 2.0 (the "License"); 244 | you may not use this file except in compliance with the License. 245 | You may obtain a copy of the License at 246 | 247 | http://www.apache.org/licenses/LICENSE-2.0 248 | 249 | Unless required by applicable law or agreed to in writing, software 250 | distributed under the License is distributed on an "AS IS" BASIS, 251 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 252 | See the License for the specific language governing permissions and 253 | limitations under the License. 254 | ``` -------------------------------------------------------------------------------- /app/AppSignature.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/AppSignature.jks -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | // apply plugin: 'org.jetbrains.kotlin.android' 3 | 4 | android { 5 | compileSdkVersion 34 6 | 7 | defaultConfig { 8 | applicationId "com.hjq.language.demo" 9 | minSdkVersion 16 10 | targetSdkVersion 34 11 | versionCode 950 12 | versionName "9.5" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | // 支持 JDK 1.8 17 | compileOptions { 18 | targetCompatibility JavaVersion.VERSION_1_8 19 | sourceCompatibility JavaVersion.VERSION_1_8 20 | } 21 | 22 | // Apk 签名的那些事:https://www.jianshu.com/p/a1f8e5896aa2 23 | signingConfigs { 24 | config { 25 | storeFile file(StoreFile) 26 | storePassword StorePassword 27 | keyAlias KeyAlias 28 | keyPassword KeyPassword 29 | } 30 | } 31 | 32 | buildTypes { 33 | debug { 34 | minifyEnabled false 35 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 36 | signingConfig signingConfigs.config 37 | } 38 | 39 | release { 40 | minifyEnabled true 41 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 42 | signingConfig signingConfigs.config 43 | } 44 | } 45 | 46 | applicationVariants.configureEach { variant -> 47 | // apk 输出文件名配置 48 | variant.outputs.configureEach { output -> 49 | outputFileName = rootProject.getName() + '.apk' 50 | } 51 | } 52 | 53 | bundle { 54 | language { 55 | enableSplit = false 56 | } 57 | } 58 | 59 | //androidResources { 60 | // 启用各应用自动设定语言支持: 61 | // https://developer.android.google.cn/guide/topics/resources/app-languages?hl=zh-cn#auto-localeconfig 62 | //generateLocaleConfig = true 63 | //} 64 | } 65 | 66 | dependencies { 67 | // 依赖 libs 目录下所有的 jar 和 aar 包 68 | implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') 69 | 70 | implementation project(':library') 71 | 72 | // AndroidX 库:https://github.com/androidx/androidx 73 | implementation 'androidx.appcompat:appcompat:1.4.0' 74 | // Material 库:https://github.com/material-components/material-components-android 75 | implementation 'com.google.android.material:material:1.4.0' 76 | 77 | // 标题栏框架:https://github.com/getActivity/TitleBar 78 | implementation 'com.github.getActivity:TitleBar:10.6' 79 | 80 | // 吐司框架:https://github.com/getActivity/Toaster 81 | implementation 'com.github.getActivity:Toaster:12.8' 82 | 83 | // 内存泄漏检测:https://github.com/square/leakcanary 84 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' 85 | } -------------------------------------------------------------------------------- /app/gradle.properties: -------------------------------------------------------------------------------- 1 | StoreFile = AppSignature.jks 2 | StorePassword = AndroidProject 3 | KeyAlias = AndroidProject 4 | KeyPassword = AndroidProject -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\SDK\Studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/ActivityManager.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.text.TextUtils; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | 14 | import java.io.FileInputStream; 15 | import java.io.IOException; 16 | import java.lang.reflect.InvocationTargetException; 17 | import java.lang.reflect.Method; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.ArrayList; 20 | import java.util.Iterator; 21 | import java.util.List; 22 | 23 | /** 24 | * author : Android 轮子哥 25 | * github : https://github.com/getActivity/AndroidProject 26 | * time : 2018/11/18 27 | * desc : Activity 管理器 28 | */ 29 | public final class ActivityManager implements Application.ActivityLifecycleCallbacks { 30 | 31 | private static volatile ActivityManager sInstance; 32 | 33 | /** Activity 存放集合 */ 34 | private final List mActivityList = new ArrayList<>(); 35 | 36 | /** 应用生命周期回调 */ 37 | private final ArrayList mLifecycleCallbacks = new ArrayList<>(); 38 | 39 | /** 当前应用上下文对象 */ 40 | private Application mApplication; 41 | /** 栈顶的 Activity 对象 */ 42 | private Activity mTopActivity; 43 | /** 前台并且可见的 Activity 对象 */ 44 | private Activity mResumedActivity; 45 | 46 | private ActivityManager() {} 47 | 48 | public static ActivityManager getInstance() { 49 | if(sInstance == null) { 50 | synchronized (ActivityManager.class) { 51 | if(sInstance == null) { 52 | sInstance = new ActivityManager(); 53 | } 54 | } 55 | } 56 | return sInstance; 57 | } 58 | 59 | public void init(Application application) { 60 | mApplication = application; 61 | mApplication.registerActivityLifecycleCallbacks(this); 62 | } 63 | 64 | /** 65 | * 获取 Application 对象 66 | */ 67 | @NonNull 68 | public Application getApplication() { 69 | return mApplication; 70 | } 71 | 72 | /** 73 | * 获取栈顶的 Activity 74 | */ 75 | @Nullable 76 | public Activity getTopActivity() { 77 | return mTopActivity; 78 | } 79 | 80 | /** 81 | * 获取前台并且可见的 Activity 82 | */ 83 | @Nullable 84 | public Activity getResumedActivity() { 85 | return mResumedActivity; 86 | } 87 | 88 | /** 89 | * 获取 Activity 集合 90 | */ 91 | @NonNull 92 | public List getActivityList() { 93 | return mActivityList; 94 | } 95 | 96 | /** 97 | * 判断当前应用是否处于前台状态 98 | */ 99 | public boolean isForeground() { 100 | return getResumedActivity() != null; 101 | } 102 | 103 | /** 104 | * 注册应用生命周期回调 105 | */ 106 | public void registerApplicationLifecycleCallback(ApplicationLifecycleCallback callback) { 107 | mLifecycleCallbacks.add(callback); 108 | } 109 | 110 | /** 111 | * 取消注册应用生命周期回调 112 | */ 113 | public void unregisterApplicationLifecycleCallback(ApplicationLifecycleCallback callback) { 114 | mLifecycleCallbacks.remove(callback); 115 | } 116 | 117 | /** 118 | * 销毁指定的 Activity 119 | */ 120 | public void finishActivity(Class clazz) { 121 | if (clazz == null) { 122 | return; 123 | } 124 | 125 | Iterator iterator = mActivityList.iterator(); 126 | while (iterator.hasNext()) { 127 | Activity activity = iterator.next(); 128 | if (!activity.getClass().equals(clazz)) { 129 | continue; 130 | } 131 | if (!activity.isFinishing()) { 132 | activity.finish(); 133 | } 134 | iterator.remove(); 135 | } 136 | } 137 | 138 | /** 139 | * 销毁所有的 Activity 140 | */ 141 | public void finishAllActivities() { 142 | finishAllActivities((Class) null); 143 | } 144 | 145 | /** 146 | * 销毁所有的 Activity 147 | * 148 | * @param classArray 白名单 Activity 149 | */ 150 | @SafeVarargs 151 | public final void finishAllActivities(Class... classArray) { 152 | Iterator iterator = mActivityList.iterator(); 153 | while (iterator.hasNext()) { 154 | Activity activity = iterator.next(); 155 | boolean whiteClazz = false; 156 | if (classArray != null) { 157 | for (Class clazz : classArray) { 158 | if (activity.getClass().equals(clazz)) { 159 | whiteClazz = true; 160 | } 161 | } 162 | } 163 | if (whiteClazz) { 164 | continue; 165 | } 166 | // 如果不是白名单上面的 Activity 就销毁掉 167 | if (!activity.isFinishing()) { 168 | activity.finish(); 169 | } 170 | iterator.remove(); 171 | } 172 | } 173 | 174 | @Override 175 | public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { 176 | if (mActivityList.isEmpty()) { 177 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 178 | callback.onApplicationCreate(activity); 179 | } 180 | } 181 | mActivityList.add(activity); 182 | mTopActivity = activity; 183 | } 184 | 185 | @Override 186 | public void onActivityStarted(@NonNull Activity activity) { 187 | } 188 | 189 | @Override 190 | public void onActivityResumed(@NonNull Activity activity) { 191 | if (mTopActivity == activity && mResumedActivity == null) { 192 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 193 | callback.onApplicationForeground(activity); 194 | } 195 | } 196 | mTopActivity = activity; 197 | mResumedActivity = activity; 198 | } 199 | 200 | @Override 201 | public void onActivityPaused(@NonNull Activity activity) { 202 | } 203 | 204 | @Override 205 | public void onActivityStopped(@NonNull Activity activity) { 206 | if (mResumedActivity == activity) { 207 | mResumedActivity = null; 208 | } 209 | if (mResumedActivity == null) { 210 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 211 | callback.onApplicationBackground(activity); 212 | } 213 | } 214 | } 215 | 216 | @Override 217 | public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) { 218 | } 219 | 220 | @Override 221 | public void onActivityDestroyed(@NonNull Activity activity) { 222 | mActivityList.remove(activity); 223 | if (mTopActivity == activity) { 224 | mTopActivity = null; 225 | } 226 | if (mActivityList.isEmpty()) { 227 | for (ApplicationLifecycleCallback callback : mLifecycleCallbacks) { 228 | callback.onApplicationDestroy(activity); 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * 判断是否在主进程中 235 | */ 236 | public static boolean isMainProcess(Context context) { 237 | String processName = getProcessName(); 238 | if (TextUtils.isEmpty(processName)) { 239 | // 如果获取不到进程名称,那么则将它当做主进程 240 | return true; 241 | } 242 | return TextUtils.equals(processName, context.getPackageName()); 243 | } 244 | 245 | /** 246 | * 获取当前进程名称 247 | */ 248 | @SuppressLint("PrivateApi, DiscouragedPrivateApi") 249 | @Nullable 250 | public static String getProcessName() { 251 | String processName = null; 252 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 253 | processName = Application.getProcessName(); 254 | } else { 255 | try { 256 | Class activityThread = Class.forName("android.app.ActivityThread"); 257 | Method currentProcessNameMethod = activityThread.getDeclaredMethod("currentProcessName"); 258 | processName = (String) currentProcessNameMethod.invoke(null); 259 | } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 260 | e.printStackTrace(); 261 | } 262 | } 263 | 264 | if (!TextUtils.isEmpty(processName)) { 265 | return processName; 266 | } 267 | 268 | // 利用 Linux 系统获取进程名 269 | FileInputStream inputStream = null; 270 | try { 271 | inputStream = new FileInputStream("/proc/self/cmdline"); 272 | byte[] buffer = new byte[256]; 273 | int len = 0; 274 | int b; 275 | while ((b = inputStream.read()) > 0 && len < buffer.length) { 276 | buffer[len++] = (byte) b; 277 | } 278 | if (len > 0) { 279 | return new String(buffer, 0, len, StandardCharsets.UTF_8); 280 | } 281 | } catch (IOException e) { 282 | e.printStackTrace(); 283 | } finally { 284 | if (inputStream != null) { 285 | try { 286 | inputStream.close(); 287 | } catch (IOException e) { 288 | e.printStackTrace(); 289 | } 290 | } 291 | } 292 | return null; 293 | } 294 | 295 | /** 296 | * 应用生命周期回调 297 | */ 298 | public interface ApplicationLifecycleCallback { 299 | 300 | /** 301 | * 第一个 Activity 创建了 302 | */ 303 | void onApplicationCreate(Activity activity); 304 | 305 | /** 306 | * 最后一个 Activity 销毁了 307 | */ 308 | void onApplicationDestroy(Activity activity); 309 | 310 | /** 311 | * 应用从前台进入到后台 312 | */ 313 | void onApplicationBackground(Activity activity); 314 | 315 | /** 316 | * 应用从后台进入到前台 317 | */ 318 | void onApplicationForeground(Activity activity); 319 | } 320 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/AppApplication.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.Context; 6 | import android.util.Log; 7 | import com.hjq.language.MultiLanguages; 8 | import com.hjq.language.OnLanguageListener; 9 | import com.hjq.toast.Toaster; 10 | import java.util.List; 11 | import java.util.Locale; 12 | 13 | /** 14 | * author : Android 轮子哥 15 | * github : https://github.com/getActivity/MultiLanguages 16 | * time : 2019/08/10 17 | * desc : 应用入口 18 | */ 19 | public final class AppApplication extends Application { 20 | 21 | // static { 22 | // // 设置默认的语种(越早设置越好) 23 | // MultiLanguages.setDefaultLanguage(LocaleContract.getEnglishLocale()); 24 | // } 25 | 26 | @Override 27 | public void onCreate() { 28 | super.onCreate(); 29 | 30 | ActivityManager.getInstance().init(this); 31 | 32 | // 初始化 Toast 框架 33 | Toaster.init(this); 34 | 35 | // 初始化多语种框架 36 | MultiLanguages.init(this); 37 | // 设置语种变化监听器 38 | MultiLanguages.setOnLanguageListener(new OnLanguageListener() { 39 | 40 | @Override 41 | public void onAppLocaleChange(Locale oldLocale, Locale newLocale) { 42 | Log.i("MultiLanguages", "监听到应用切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale); 43 | } 44 | 45 | @Override 46 | public void onSystemLocaleChange(Locale oldLocale, Locale newLocale) { 47 | Log.i("MultiLanguages", "监听到系统切换了语种,旧语种:" + oldLocale + ",新语种:" + newLocale + 48 | ",是否跟随系统:" + MultiLanguages.isSystemLanguage(AppApplication.this)); 49 | 50 | if (!MultiLanguages.isSystemLanguage(AppApplication.this)) { 51 | return; 52 | } 53 | // 如需在系统切换语种后应用也要随之变化的,可以在这里获取所有的 Activity 并调用它的 recreate 方法 54 | // getAllActivity 只是演示代码,需要自行替换成项目已实现的方法,若项目中没有,请自行封装 55 | List activityList = ActivityManager.getInstance().getActivityList(); 56 | for (Activity activity : activityList) { 57 | activity.recreate(); 58 | } 59 | } 60 | }); 61 | } 62 | 63 | @Override 64 | protected void attachBaseContext(Context newBase) { 65 | // 绑定语种 66 | super.attachBaseContext(MultiLanguages.attach(newBase)); 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | 7 | import com.hjq.language.MultiLanguages; 8 | 9 | /** 10 | * author : Android 轮子哥 11 | * github : https://github.com/getActivity/MultiLanguages 12 | * time : 2019/08/10 13 | * desc : Activity 基类 14 | */ 15 | public abstract class BaseActivity extends AppCompatActivity { 16 | 17 | @Override 18 | protected void attachBaseContext(Context newBase) { 19 | // 绑定语种 20 | super.attachBaseContext(MultiLanguages.attach(newBase)); 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/LanguagesWebView.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.webkit.WebView; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | import com.hjq.language.MultiLanguages; 11 | 12 | /** 13 | * author : Android 轮子哥 14 | * github : https://github.com/getActivity/MultiLanguages 15 | * time : 2019/08/10 16 | * desc : 修复语种的 WebView 17 | */ 18 | public final class LanguagesWebView extends WebView { 19 | 20 | public LanguagesWebView(@NonNull Context context) { 21 | this(context, null); 22 | } 23 | 24 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs) { 25 | this(context, attrs, android.R.attr.webViewStyle); 26 | } 27 | 28 | public LanguagesWebView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | // 修复 WebView 初始化时会修改 Activity 语种配置的问题 31 | MultiLanguages.updateAppLanguage(context); 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/hjq/language/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language.demo; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.webkit.WebChromeClient; 10 | import android.webkit.WebResourceRequest; 11 | import android.webkit.WebView; 12 | import android.webkit.WebViewClient; 13 | import android.widget.RadioGroup; 14 | import android.widget.TextView; 15 | import androidx.annotation.NonNull; 16 | import com.hjq.bar.OnTitleBarListener; 17 | import com.hjq.bar.TitleBar; 18 | import com.hjq.language.LocaleContract; 19 | import com.hjq.language.MultiLanguages; 20 | import java.util.HashMap; 21 | import java.util.Locale; 22 | import java.util.Map; 23 | 24 | /** 25 | * author : Android 轮子哥 26 | * github : https://github.com/getActivity/MultiLanguages 27 | * time : 2019/08/10 28 | * desc : 多语种切换演示 29 | */ 30 | public final class MainActivity extends BaseActivity 31 | implements RadioGroup.OnCheckedChangeListener, OnTitleBarListener { 32 | 33 | private WebView mWebView; 34 | private TextView mSystemLanguageView; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | 41 | mWebView = findViewById(R.id.wv_main_web); 42 | 43 | TitleBar titleBar = findViewById(R.id.tb_main_bar); 44 | RadioGroup radioGroup = findViewById(R.id.rg_main_languages); 45 | 46 | titleBar.setOnTitleBarListener(this); 47 | 48 | mWebView.setWebViewClient(new LanguagesViewClient()); 49 | mWebView.setWebChromeClient(new WebChromeClient()); 50 | mWebView.loadUrl("https://developer.android.google.cn/kotlin", generateLanguageRequestHeader(this)); 51 | 52 | //((TextView) findViewById(R.id.tv_language_activity)).setText(this.getResources().getString(R.string.current_language)); 53 | ((TextView) findViewById(R.id.tv_main_language_application)).setText( 54 | getApplication().getResources().getString(R.string.current_language)); 55 | mSystemLanguageView = findViewById(R.id.tv_main_language_system); 56 | mSystemLanguageView.setText(MultiLanguages.getLanguageString(this, 57 | MultiLanguages.getSystemLanguage(this), R.string.current_language)); 58 | 59 | if (MultiLanguages.isSystemLanguage(this)) { 60 | radioGroup.check(R.id.rb_main_language_auto); 61 | } else { 62 | Locale locale = MultiLanguages.getAppLanguage(this); 63 | if (LocaleContract.getSimplifiedChineseLocale().equals(locale)) { 64 | radioGroup.check(R.id.rb_main_language_cn); 65 | } else if (LocaleContract.getTraditionalChineseLocale().equals(locale)) { 66 | radioGroup.check(R.id.rb_main_language_tw); 67 | } else if (LocaleContract.getEnglishLocale().equals(locale)) { 68 | radioGroup.check(R.id.rb_main_language_en); 69 | } else { 70 | radioGroup.check(R.id.rb_main_language_auto); 71 | } 72 | } 73 | 74 | radioGroup.setOnCheckedChangeListener(this); 75 | } 76 | 77 | /** 78 | * {@link RadioGroup.OnCheckedChangeListener} 79 | */ 80 | @Override 81 | public void onCheckedChanged(RadioGroup group, int checkedId) { 82 | // 是否需要重启 83 | boolean restart = false; 84 | 85 | if (checkedId == R.id.rb_main_language_auto) { 86 | // 跟随系统 87 | restart = MultiLanguages.clearAppLanguage(this); 88 | } else if (checkedId == R.id.rb_main_language_cn) { 89 | // 简体中文 90 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getSimplifiedChineseLocale()); 91 | } else if (checkedId == R.id.rb_main_language_tw) { 92 | // 繁体中文 93 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getTraditionalChineseLocale()); 94 | } else if (checkedId == R.id.rb_main_language_en) { 95 | // 英语 96 | restart = MultiLanguages.setAppLanguage(this, LocaleContract.getEnglishLocale()); 97 | } 98 | 99 | if (restart) { 100 | // 1.使用 recreate 来重启 Activity,效果差,有闪屏的缺陷 101 | // recreate(); 102 | 103 | // 2.使用常规 startActivity 来重启 Activity,有从左向右的切换动画 104 | // 稍微比 recreate 的效果好一点,但是这种并不是最佳的效果 105 | // startActivity(new Intent(this, LanguageActivity.class)); 106 | // finish(); 107 | 108 | // 3.我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果,相比前面的两种带来的体验是最佳的 109 | // startActivity(new Intent(this, MainActivity.class)); 110 | // overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 111 | // finish(); 112 | 113 | // 4. 我们可以充分运用 Activity 跳转动画,在跳转的时候设置一个渐变的效果 114 | Intent intent = new Intent(this, MainActivity.class); 115 | // Github 地址:https://github.com/getActivity/MultiLanguages/issues/55 116 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 117 | startActivity(intent); 118 | overridePendingTransition(R.anim.activity_alpha_in, R.anim.activity_alpha_out); 119 | finish(); 120 | } 121 | } 122 | 123 | @Override 124 | public void onResume() { 125 | super.onResume(); 126 | mWebView.onResume(); 127 | mWebView.resumeTimers(); 128 | 129 | if (mSystemLanguageView == null) { 130 | return; 131 | } 132 | mSystemLanguageView.setText(MultiLanguages.getLanguageString(this, 133 | MultiLanguages.getSystemLanguage(this), R.string.current_language)); 134 | } 135 | 136 | @Override 137 | public void onPause() { 138 | super.onPause(); 139 | mWebView.onPause(); 140 | mWebView.pauseTimers(); 141 | } 142 | 143 | @Override 144 | protected void onDestroy() { 145 | super.onDestroy(); 146 | //清除历史记录 147 | mWebView.clearHistory(); 148 | //停止加载 149 | mWebView.stopLoading(); 150 | //加载一个空白页 151 | mWebView.loadUrl("about:blank"); 152 | mWebView.setWebChromeClient(null); 153 | mWebView.setWebViewClient(new WebViewClient()); 154 | //移除WebView所有的View对象 155 | mWebView.removeAllViews(); 156 | //销毁此的WebView的内部状态 157 | mWebView.destroy(); 158 | } 159 | 160 | @Override 161 | public void onTitleClick(TitleBar titleBar) { 162 | Intent intent = new Intent(Intent.ACTION_VIEW); 163 | intent.setData(Uri.parse(titleBar.getTitle().toString())); 164 | startActivity(intent); 165 | } 166 | 167 | public static class LanguagesViewClient extends WebViewClient { 168 | 169 | @TargetApi(Build.VERSION_CODES.N) 170 | @Override 171 | public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { 172 | return shouldOverrideUrlLoading(view, request.getUrl().toString()); 173 | } 174 | 175 | @Override 176 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 177 | String scheme = Uri.parse(url).getScheme(); 178 | if (scheme == null) { 179 | return false; 180 | } 181 | switch (scheme) { 182 | // 如果这是跳链接操作 183 | case "http": 184 | case "https": 185 | view.loadUrl(url, generateLanguageRequestHeader(view.getContext())); 186 | break; 187 | default: 188 | break; 189 | } 190 | return true; 191 | } 192 | } 193 | 194 | /** 195 | * 给 WebView 请求头添加语种环境 196 | */ 197 | @NonNull 198 | public static Map generateLanguageRequestHeader(Context context) { 199 | Map map = new HashMap<>(1); 200 | // Android 13 上面语种失效的问题解决方案 201 | // https://developer.android.google.cn/about/versions/13/features/app-languages?hl=zh-cn#consider-header 202 | map.put("Accept-Language", String.valueOf(MultiLanguages.getAppLanguage(context))); 203 | return map; 204 | } 205 | } -------------------------------------------------------------------------------- /app/src/main/res/anim/activity_alpha_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/anim/activity_alpha_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 18 | 19 | 25 | 26 | 30 | 31 | 38 | 39 | 45 | 46 | 52 | 53 | 59 | 60 | 66 | 67 | 68 | 69 | 74 | 75 | 79 | 80 | 86 | 87 | 88 | 93 | 94 | 98 | 99 | 104 | 105 | 106 | 112 | 113 | 117 | 118 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MultiLanguages 4 | English 5 | 6 | Current Activity language: 7 | Current Application language: 8 | Current System language: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-v23/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 国际化 4 | 简体中文 5 | 6 | 当前 Activity 语种: 7 | 当前 Application 语种: 8 | 当前 System 语种: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 國際化 4 | 繁體中文 5 | 6 | 當前 Activity 語種: 7 | 當前 Application 語種: 8 | 當前 System 語種: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | #000000 5 | #FF0033 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 国际化 4 | 跟随系统 5 | 6 | 当前 Activity 语种: 7 | 当前 Application 语种: 8 | 当前 System 语种: 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | // 阿里云云效仓库:https://maven.aliyun.com/mvn/guide 4 | maven { url 'https://maven.aliyun.com/repository/public' } 5 | maven { url 'https://maven.aliyun.com/repository/google' } 6 | // 华为开源镜像:https://mirrors.huaweicloud.com/ 7 | maven { url 'https://repo.huaweicloud.com/repository/maven/' } 8 | // JitPack 远程仓库:https://jitpack.io 9 | maven { url 'https://jitpack.io' } 10 | mavenCentral() 11 | google() 12 | // noinspection JcenterRepositoryObsolete 13 | jcenter() 14 | } 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:4.1.2' 17 | // Kotlin 插件:https://plugins.jetbrains.com/plugin/6954-kotlin 18 | // noinspection GradleDependency 19 | // classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31' 20 | } 21 | } 22 | 23 | allprojects { 24 | repositories { 25 | maven { url 'https://maven.aliyun.com/repository/public' } 26 | maven { url 'https://maven.aliyun.com/repository/google' } 27 | maven { url 'https://repo.huaweicloud.com/repository/maven/' } 28 | maven { url 'https://jitpack.io' } 29 | mavenCentral() 30 | google() 31 | // noinspection JcenterRepositoryObsolete 32 | jcenter() 33 | } 34 | 35 | // 读取 local.properties 文件配置 36 | def properties = new Properties() 37 | def localPropertiesFile = rootProject.file("local.properties") 38 | if (localPropertiesFile.exists()) { 39 | localPropertiesFile.withInputStream { inputStream -> 40 | properties.load(inputStream) 41 | } 42 | } 43 | 44 | String buildDirPath = properties.getProperty("build.dir") 45 | if (buildDirPath != null && buildDirPath != "") { 46 | // 将构建文件统一输出到指定的目录下 47 | setBuildDir(new File(buildDirPath, rootProject.name + "/build/${path.replaceAll(':', '/')}")) 48 | } else { 49 | // 将构建文件统一输出到项目根目录下的 build 文件夹 50 | setBuildDir(new File(rootDir, "build/${path.replaceAll(':', '/')}")) 51 | } 52 | } 53 | 54 | tasks.register('clean', Delete) { 55 | delete rootProject.buildDir 56 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # 表示使用 AndroidX 20 | android.useAndroidX = true 21 | # 表示将第三方库迁移到 AndroidX 22 | android.enableJetifier = true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | zipStoreBase = GRADLE_USER_HOME 2 | zipStorePath = wrapper/dists 3 | distributionBase = GRADLE_USER_HOME 4 | distributionPath = wrapper/dists 5 | distributionUrl = https\://services.gradle.org/distributions/gradle-6.5-all.zip -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 34 5 | 6 | defaultConfig { 7 | minSdkVersion 14 8 | versionCode 950 9 | versionName "9.5" 10 | } 11 | 12 | android.libraryVariants.configureEach { variant -> 13 | // aar 输出文件名配置 14 | variant.outputs.configureEach { output -> 15 | outputFileName = "${rootProject.name}-${android.defaultConfig.versionName}.aar" 16 | } 17 | } 18 | } 19 | 20 | afterEvaluate { 21 | // 排除 BuildConfig.class 和 R.class 22 | generateReleaseBuildConfig.enabled = false 23 | generateDebugBuildConfig.enabled = false 24 | generateReleaseResValues.enabled = false 25 | generateDebugResValues.enabled = false 26 | } 27 | 28 | tasks.withType(Javadoc).configureEach { 29 | options.addStringOption('Xdoclint:none', '-quiet') 30 | options.addStringOption('encoding', 'UTF-8') 31 | options.addStringOption('charSet', 'UTF-8') 32 | } 33 | 34 | tasks.register('sourcesJar', Jar) { 35 | from android.sourceSets.main.java.srcDirs 36 | classifier = 'sources' 37 | } 38 | 39 | tasks.register('javadoc', Javadoc) { 40 | source = android.sourceSets.main.java.srcDirs 41 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 42 | } 43 | 44 | tasks.register('javadocJar', Jar) { 45 | dependsOn javadoc 46 | classifier = 'javadoc' 47 | from javadoc.destinationDir 48 | } 49 | 50 | artifacts { 51 | archives javadocJar 52 | archives sourcesJar 53 | } -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\SDK\Studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/ActivityLanguages.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.os.Bundle; 6 | 7 | /** 8 | * author : Android 轮子哥 9 | * github : https://github.com/getActivity/MultiLanguages 10 | * time : 2021/01/21 11 | * desc : Activity 语种注入 12 | */ 13 | final class ActivityLanguages implements Application.ActivityLifecycleCallbacks { 14 | 15 | /* 16 | 这里解释一下,为什么要在 Activity 所有生命周期中刷新语种 17 | 这是因为发现有的手机厂商系统(例如 miui 系统)会偷摸修改 Activity 或 Application 绑定的语种 18 | Github 地址:https://github.com/getActivity/MultiLanguages/issues/52 19 | */ 20 | 21 | static void inject(Application application) { 22 | application.registerActivityLifecycleCallbacks(new ActivityLanguages()); 23 | } 24 | 25 | @Override 26 | public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) { 27 | refreshActivityAndApplicationLanguage(activity); 28 | } 29 | 30 | @Override 31 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) { 32 | refreshActivityAndApplicationLanguage(activity); 33 | } 34 | 35 | @Override 36 | public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) { 37 | refreshActivityAndApplicationLanguage(activity); 38 | } 39 | 40 | @Override 41 | public void onActivityPreStarted(Activity activity) { 42 | refreshActivityAndApplicationLanguage(activity); 43 | } 44 | 45 | @Override 46 | public void onActivityStarted(Activity activity) { 47 | refreshActivityAndApplicationLanguage(activity); 48 | } 49 | 50 | @Override 51 | public void onActivityPostStarted(Activity activity) { 52 | refreshActivityAndApplicationLanguage(activity); 53 | } 54 | 55 | @Override 56 | public void onActivityPreResumed(Activity activity) { 57 | refreshActivityAndApplicationLanguage(activity); 58 | } 59 | 60 | @Override 61 | public void onActivityResumed(Activity activity) { 62 | refreshActivityAndApplicationLanguage(activity); 63 | } 64 | 65 | @Override 66 | public void onActivityPostResumed(Activity activity) { 67 | refreshActivityAndApplicationLanguage(activity); 68 | } 69 | 70 | @Override 71 | public void onActivityPrePaused(Activity activity) { 72 | refreshActivityAndApplicationLanguage(activity); 73 | } 74 | 75 | @Override 76 | public void onActivityPaused(Activity activity) { 77 | refreshActivityAndApplicationLanguage(activity); 78 | } 79 | 80 | @Override 81 | public void onActivityPostPaused(Activity activity) { 82 | refreshActivityAndApplicationLanguage(activity); 83 | } 84 | 85 | @Override 86 | public void onActivityPreStopped(Activity activity) { 87 | refreshActivityAndApplicationLanguage(activity); 88 | } 89 | 90 | @Override 91 | public void onActivityStopped(Activity activity) { 92 | refreshActivityAndApplicationLanguage(activity); 93 | } 94 | 95 | @Override 96 | public void onActivityPostStopped(Activity activity) { 97 | refreshActivityAndApplicationLanguage(activity); 98 | } 99 | 100 | @Override 101 | public void onActivityPreSaveInstanceState(Activity activity, Bundle outState) { 102 | refreshActivityAndApplicationLanguage(activity); 103 | } 104 | 105 | @Override 106 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 107 | refreshActivityAndApplicationLanguage(activity); 108 | } 109 | 110 | @Override 111 | public void onActivityPostSaveInstanceState(Activity activity, Bundle outState) { 112 | refreshActivityAndApplicationLanguage(activity); 113 | } 114 | 115 | @Override 116 | public void onActivityPreDestroyed(Activity activity) { 117 | refreshActivityAndApplicationLanguage(activity); 118 | } 119 | 120 | @Override 121 | public void onActivityDestroyed(Activity activity) { 122 | refreshApplicationLanguage(activity.getApplication()); 123 | } 124 | 125 | @Override 126 | public void onActivityPostDestroyed(Activity activity) { 127 | refreshApplicationLanguage(activity.getApplication()); 128 | } 129 | 130 | /** 131 | * 刷新 Activity 和 Application 的语种 132 | */ 133 | private void refreshActivityAndApplicationLanguage(Activity activity) { 134 | if (activity == null) { 135 | return; 136 | } 137 | MultiLanguages.updateAppLanguage(activity); 138 | refreshApplicationLanguage(activity.getApplication()); 139 | } 140 | 141 | /** 142 | * 刷新 Application 的语种 143 | */ 144 | private void refreshApplicationLanguage(Application application) { 145 | if (application == null) { 146 | return; 147 | } 148 | MultiLanguages.updateAppLanguage(application); 149 | } 150 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/ConfigurationObserver.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Application; 4 | import android.content.ComponentCallbacks; 5 | import android.content.res.Configuration; 6 | 7 | /** 8 | * author : Android 轮子哥 9 | * github : https://github.com/getActivity/MultiLanguages 10 | * time : 2019/05/06 11 | * desc : 手机配置变化监听 12 | */ 13 | final class ConfigurationObserver implements ComponentCallbacks { 14 | 15 | /** 16 | * 注册系统语种变化监听 17 | */ 18 | static void register(Application application) { 19 | ConfigurationObserver configurationObserver = new ConfigurationObserver(application); 20 | application.registerComponentCallbacks(configurationObserver); 21 | } 22 | 23 | private final Application mApplication; 24 | 25 | private ConfigurationObserver(Application application) { 26 | mApplication = application; 27 | } 28 | 29 | /** 30 | * 手机的配置发生了变化 31 | */ 32 | @Override 33 | public void onConfigurationChanged(Configuration newConfig) { 34 | if (newConfig == null) { 35 | return; 36 | } 37 | // 如果当前是跟随系统语种,就则不往下执行 38 | if (MultiLanguages.isSystemLanguage(mApplication)) { 39 | return; 40 | } 41 | // 更新 Application 的配置,否则会出现横竖屏切换之后 Application 的 orientation 没有随之变化的问题 42 | LanguagesUtils.updateConfigurationChanged(mApplication, newConfig, MultiLanguages.getAppLanguage(mApplication)); 43 | } 44 | 45 | @Override 46 | public void onLowMemory() {} 47 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LanguagesConfig.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.text.TextUtils; 6 | import java.util.Locale; 7 | 8 | /** 9 | * author : Android 轮子哥 10 | * github : https://github.com/getActivity/MultiLanguages 11 | * time : 2019/05/03 12 | * desc : 语种配置保存类 13 | */ 14 | final class LanguagesConfig { 15 | 16 | private static final String KEY_LANGUAGE = "key_language"; 17 | private static final String KEY_COUNTRY = "key_country"; 18 | 19 | private static String sSharedPreferencesName = "language_setting"; 20 | 21 | /** 当前语种 */ 22 | private static volatile Locale sCurrentLocale; 23 | 24 | /** 默认语种 */ 25 | private static volatile Locale sDefaultLocale; 26 | 27 | static void setSharedPreferencesName(String name) { 28 | sSharedPreferencesName = name; 29 | } 30 | 31 | private static SharedPreferences getSharedPreferences(Context context) { 32 | return context.getSharedPreferences(sSharedPreferencesName, Context.MODE_PRIVATE); 33 | } 34 | 35 | /** 36 | * 读取 App 语种 37 | */ 38 | static Locale readAppLanguageSetting(Context context) { 39 | if (sCurrentLocale != null) { 40 | return sCurrentLocale; 41 | } 42 | 43 | String language = getSharedPreferences(context).getString(KEY_LANGUAGE, ""); 44 | String country = getSharedPreferences(context).getString(KEY_COUNTRY, ""); 45 | 46 | if (!TextUtils.isEmpty(language)) { 47 | sCurrentLocale = new Locale(language, country); 48 | return sCurrentLocale; 49 | } 50 | 51 | if (sDefaultLocale != null) { 52 | sCurrentLocale = sDefaultLocale; 53 | return sCurrentLocale; 54 | } 55 | 56 | sCurrentLocale = LanguagesUtils.getLocale(context); 57 | 58 | return sCurrentLocale; 59 | } 60 | 61 | /** 62 | * 保存 App 语种设置 63 | */ 64 | static void saveAppLanguageSetting(Context context, Locale locale) { 65 | sCurrentLocale = locale; 66 | getSharedPreferences(context).edit() 67 | .putString(KEY_LANGUAGE, locale.getLanguage()) 68 | .putString(KEY_COUNTRY, locale.getCountry()) 69 | .apply(); 70 | } 71 | 72 | /** 73 | * 清除语种设置 74 | */ 75 | static void clearLanguageSetting(Context context) { 76 | sCurrentLocale = MultiLanguages.getSystemLanguage(context); 77 | getSharedPreferences(context).edit() 78 | .remove(KEY_LANGUAGE) 79 | .remove(KEY_COUNTRY) 80 | .apply(); 81 | } 82 | 83 | /** 84 | * 是否跟随系统 85 | */ 86 | public static boolean isSystemLanguage(Context context) { 87 | if (sDefaultLocale != null) { 88 | return false; 89 | } 90 | 91 | String language = getSharedPreferences(context).getString(KEY_LANGUAGE, ""); 92 | return TextUtils.isEmpty(language); 93 | } 94 | 95 | /** 96 | * 设置默认的语种 97 | */ 98 | public static void setDefaultLanguage(Locale locale) { 99 | if (sCurrentLocale != null) { 100 | // 这个 API 需要越早调用越好,建议放在 Application 静态代码块中初始化 101 | // 当然也可以在 Application 调用 super.attachBaseContext 方法之前 102 | throw new IllegalStateException("Please set this before application initialization"); 103 | } 104 | sDefaultLocale = locale; 105 | } 106 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LanguagesUtils.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.LocaleManager; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.content.res.Resources; 7 | import android.os.Build; 8 | import android.os.LocaleList; 9 | import java.util.Locale; 10 | 11 | /** 12 | * author : Android 轮子哥 13 | * github : https://github.com/getActivity/MultiLanguages 14 | * time : 2019/05/03 15 | * desc : 国际化工具类 16 | */ 17 | @SuppressWarnings("deprecation") 18 | final class LanguagesUtils { 19 | 20 | /** 21 | * 获取语种对象 22 | */ 23 | static Locale getLocale(Context context) { 24 | return getLocale(context.getResources().getConfiguration()); 25 | } 26 | 27 | static Locale getLocale(Configuration config) { 28 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 29 | return config.getLocales().get(0); 30 | } else { 31 | return config.locale; 32 | } 33 | } 34 | 35 | /** 36 | * 设置语种对象 37 | */ 38 | static void setLocale(Configuration config, Locale locale) { 39 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 40 | LocaleList localeList = new LocaleList(locale); 41 | config.setLocales(localeList); 42 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 43 | config.setLocale(locale); 44 | } else { 45 | config.locale = locale; 46 | } 47 | } 48 | 49 | /** 50 | * 获取系统的语种对象 51 | */ 52 | static Locale getSystemLocale(Context context) { 53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 54 | // 在 Android 13 上,不能用 Resources.getSystem() 来获取系统语种了 55 | // Android 13 上面新增了一个 LocaleManager 的语种管理类 56 | // 因为如果调用 LocaleManager.setApplicationLocales 会影响获取到的结果不准确 57 | // 所以应该得用 LocaleManager.getSystemLocales 来获取会比较精准 58 | LocaleManager localeManager = context.getSystemService(LocaleManager.class); 59 | if (localeManager != null) { 60 | return localeManager.getSystemLocales().get(0); 61 | } 62 | } 63 | 64 | return LanguagesUtils.getLocale(Resources.getSystem().getConfiguration()); 65 | } 66 | 67 | /** 68 | * 设置默认的语种环境(日期格式化会用到) 69 | */ 70 | static void setDefaultLocale(Context context) { 71 | Configuration configuration = context.getResources().getConfiguration(); 72 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 73 | LocaleList.setDefault(configuration.getLocales()); 74 | } else { 75 | Locale.setDefault(configuration.locale); 76 | } 77 | } 78 | 79 | /** 80 | * 绑定当前 App 的语种 81 | */ 82 | static Context attachLanguages(Context context, Locale locale) { 83 | Resources resources = context.getResources(); 84 | Configuration config = new Configuration(resources.getConfiguration()); 85 | setLocale(config, locale); 86 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 87 | context = context.createConfigurationContext(config); 88 | } 89 | resources.updateConfiguration(config, resources.getDisplayMetrics()); 90 | return context; 91 | } 92 | 93 | /** 94 | * 更新 Resources 语种 95 | */ 96 | static void updateLanguages(Resources resources, Locale locale) { 97 | Configuration config = resources.getConfiguration(); 98 | setLocale(config, locale); 99 | resources.updateConfiguration(config, resources.getDisplayMetrics()); 100 | } 101 | 102 | /** 103 | * 更新手机配置信息变化 104 | */ 105 | static void updateConfigurationChanged(Context context, Configuration newConfig, Locale appLanguage) { 106 | Configuration config = new Configuration(newConfig); 107 | // 绑定当前语种到这个新的配置对象中 108 | setLocale(config, appLanguage); 109 | Resources resources = context.getResources(); 110 | // 更新上下文的配置信息 111 | resources.updateConfiguration(config, resources.getDisplayMetrics()); 112 | } 113 | 114 | /** 115 | * 生成某个语种下的 Resources 对象 116 | */ 117 | static Resources generateLanguageResources(Context context, Locale locale) { 118 | Configuration config = new Configuration(); 119 | setLocale(config, locale); 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 121 | return context.createConfigurationContext(config).getResources(); 122 | } 123 | return new Resources(context.getAssets(), context.getResources().getDisplayMetrics(), config); 124 | } 125 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LocaleChangeReceiver.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Application; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import java.util.Locale; 9 | 10 | /** 11 | * author : Android 轮子哥 12 | * github : https://github.com/getActivity/MultiLanguages 13 | * time : 2023/11/24 14 | * desc : 语种变化广播 15 | */ 16 | final class LocaleChangeReceiver extends BroadcastReceiver { 17 | 18 | /** 系统语种 */ 19 | private static volatile Locale sSystemLanguage; 20 | 21 | static void register(Application application) { 22 | sSystemLanguage = LanguagesUtils.getSystemLocale(application); 23 | IntentFilter filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); 24 | application.registerReceiver(new LocaleChangeReceiver(application), filter); 25 | } 26 | 27 | private final Application mApplication; 28 | 29 | public LocaleChangeReceiver(Application application) { 30 | mApplication = application; 31 | } 32 | 33 | @Override 34 | public void onReceive(Context context, Intent intent) { 35 | if (intent == null) { 36 | return; 37 | } 38 | 39 | String action = intent.getAction(); 40 | 41 | if (action == null) { 42 | return; 43 | } 44 | 45 | if (!Intent.ACTION_LOCALE_CHANGED.equals(action)) { 46 | return; 47 | } 48 | 49 | if (sSystemLanguage == null) { 50 | return; 51 | } 52 | 53 | Locale latestSystemLocale = MultiLanguages.getSystemLanguage(mApplication); 54 | if (MultiLanguages.equalsCountry(latestSystemLocale, sSystemLanguage)) { 55 | return; 56 | } 57 | 58 | notifySystemLocaleChange(sSystemLanguage, latestSystemLocale); 59 | } 60 | 61 | /** 62 | * 通知系统语种发生变化 63 | */ 64 | public void notifySystemLocaleChange(Locale oldLocale, Locale newLocale) { 65 | sSystemLanguage = newLocale; 66 | 67 | // 如果当前的语种是跟随系统变化的,那么就需要重置一下当前 App 的语种 68 | if (LanguagesConfig.isSystemLanguage(mApplication)) { 69 | LanguagesConfig.clearLanguageSetting(mApplication); 70 | } 71 | 72 | OnLanguageListener listener = MultiLanguages.getOnLanguagesListener(); 73 | if (listener == null) { 74 | return; 75 | } 76 | listener.onSystemLocaleChange(oldLocale, newLocale); 77 | } 78 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/LocaleContract.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * author : Android 轮子哥 7 | * github : https://github.com/getActivity/MultiLanguages 8 | * time : 2022/04/08 9 | * desc : 语种契约类 10 | * doc : https://blog.csdn.net/liuhhaiffeng/article/details/54706027 11 | */ 12 | public final class LocaleContract { 13 | 14 | /** 中文 */ 15 | private static volatile Locale sChineseLocale; 16 | 17 | public static Locale getChineseLocale() { 18 | if (sChineseLocale == null) { 19 | sChineseLocale = Locale.CHINESE; 20 | } 21 | return sChineseLocale; 22 | } 23 | 24 | /** 简体中文 */ 25 | private static volatile Locale sSimplifiedChineseLocale; 26 | 27 | public static Locale getSimplifiedChineseLocale() { 28 | if (sSimplifiedChineseLocale == null) { 29 | sSimplifiedChineseLocale = Locale.SIMPLIFIED_CHINESE; 30 | } 31 | return sSimplifiedChineseLocale; 32 | } 33 | 34 | /** 繁体中文 */ 35 | private static volatile Locale sTraditionalChineseLocale; 36 | 37 | public static Locale getTraditionalChineseLocale() { 38 | if (sTraditionalChineseLocale == null) { 39 | sTraditionalChineseLocale = Locale.TRADITIONAL_CHINESE; 40 | } 41 | return sTraditionalChineseLocale; 42 | } 43 | 44 | public static Locale getTaiWanLocale() { 45 | return getTraditionalChineseLocale(); 46 | } 47 | 48 | /** 新加坡 */ 49 | private static volatile Locale sSingaporeLocale ; 50 | 51 | public static Locale getSingaporeLocale() { 52 | if (sSimplifiedChineseLocale == null) { 53 | sSingaporeLocale = new Locale("zh", "SG"); 54 | } 55 | return sSingaporeLocale; 56 | } 57 | 58 | /** 英语 */ 59 | private static volatile Locale sEnglishLocale; 60 | 61 | public static Locale getEnglishLocale() { 62 | if (sEnglishLocale == null) { 63 | sEnglishLocale = Locale.ENGLISH; 64 | } 65 | return sEnglishLocale; 66 | } 67 | 68 | /** 法语 */ 69 | private static volatile Locale sFrenchLocale; 70 | 71 | public static Locale getFrenchLocale() { 72 | if (sFrenchLocale == null) { 73 | sFrenchLocale = Locale.FRENCH; 74 | } 75 | return sFrenchLocale; 76 | } 77 | 78 | /** 德语 */ 79 | private static volatile Locale sGermanLocale; 80 | 81 | public static Locale getGermanLocale() { 82 | if (sGermanLocale == null) { 83 | sGermanLocale = Locale.GERMAN; 84 | } 85 | return sGermanLocale; 86 | } 87 | 88 | /** 意大利语 */ 89 | private static volatile Locale sItalianLocale; 90 | 91 | public static Locale getItalianLocale() { 92 | if (sItalianLocale == null) { 93 | sItalianLocale = Locale.ITALIAN; 94 | } 95 | return sItalianLocale; 96 | } 97 | 98 | /** 日语 */ 99 | private static volatile Locale sJapaneseLocale; 100 | 101 | public static Locale getJapaneseLocale() { 102 | if (sJapaneseLocale == null) { 103 | sJapaneseLocale = Locale.JAPANESE; 104 | } 105 | return sJapaneseLocale; 106 | } 107 | 108 | /** 韩语 */ 109 | private static volatile Locale sKoreanLocale; 110 | 111 | public static Locale getKoreanLocale() { 112 | if (sKoreanLocale == null) { 113 | sKoreanLocale = Locale.KOREAN; 114 | } 115 | return sKoreanLocale; 116 | } 117 | 118 | /** 越南语 */ 119 | private static volatile Locale sVietnameseLocale; 120 | 121 | public static Locale getVietnameseLocale() { 122 | if (sVietnameseLocale == null) { 123 | sVietnameseLocale = new Locale("vi"); 124 | } 125 | return sVietnameseLocale; 126 | } 127 | 128 | /** 荷兰语 */ 129 | private static volatile Locale sDutchLocale; 130 | 131 | public static Locale getDutchLocale() { 132 | if (sDutchLocale == null) { 133 | sDutchLocale = new Locale("af"); 134 | } 135 | return sDutchLocale; 136 | } 137 | 138 | /** 阿尔巴尼亚语 */ 139 | private static volatile Locale sAlbanianLocale; 140 | 141 | public static Locale getAlbanianLocale() { 142 | if (sAlbanianLocale == null) { 143 | sAlbanianLocale = new Locale("sq"); 144 | } 145 | return sAlbanianLocale; 146 | } 147 | 148 | /** 阿拉伯语 */ 149 | private static volatile Locale sArabicLocale; 150 | 151 | public static Locale getArabicLocale() { 152 | if (sArabicLocale == null) { 153 | sArabicLocale = new Locale("ar"); 154 | } 155 | return sArabicLocale; 156 | } 157 | 158 | /** 亚美尼亚语 */ 159 | private static volatile Locale sArmenianLocale; 160 | 161 | public static Locale getArmenianLocale() { 162 | if (sArmenianLocale == null) { 163 | sArmenianLocale = new Locale("hy"); 164 | } 165 | return sArmenianLocale; 166 | } 167 | 168 | /** 阿塞拜疆语 */ 169 | private static volatile Locale sAzerbaijaniLocale; 170 | 171 | public static Locale getAzerbaijaniLocale() { 172 | if (sAzerbaijaniLocale == null) { 173 | sAzerbaijaniLocale = new Locale("az"); 174 | } 175 | return sAzerbaijaniLocale; 176 | } 177 | 178 | /** 巴斯克语 */ 179 | private static volatile Locale sBasqueLocale; 180 | 181 | public static Locale getBasqueLocale() { 182 | if (sBasqueLocale == null) { 183 | sBasqueLocale = new Locale("eu"); 184 | } 185 | return sBasqueLocale; 186 | } 187 | 188 | /** 白俄罗斯 */ 189 | private static volatile Locale sBelarusianLocale; 190 | 191 | public static Locale getBelarusianLocale() { 192 | if (sBelarusianLocale == null) { 193 | sBelarusianLocale = new Locale("be"); 194 | } 195 | return sBelarusianLocale; 196 | } 197 | 198 | /** 保加利亚 */ 199 | private static volatile Locale sBulgariaLocale; 200 | 201 | public static Locale getBulgariaLocale() { 202 | if (sBulgariaLocale == null) { 203 | sBulgariaLocale = new Locale("bg"); 204 | } 205 | return sBulgariaLocale; 206 | } 207 | 208 | /** 加泰罗尼亚 */ 209 | private static volatile Locale sCatalonianLocale; 210 | 211 | public static Locale getCatalonianLocale() { 212 | if (sCatalonianLocale == null) { 213 | sCatalonianLocale = new Locale("ca"); 214 | } 215 | return sCatalonianLocale; 216 | } 217 | 218 | /** 克罗埃西亚 */ 219 | private static volatile Locale sCroatiaLocale; 220 | 221 | public static Locale getCroatiaLocale() { 222 | if (sCroatiaLocale == null) { 223 | sCroatiaLocale = new Locale("hr"); 224 | } 225 | return sCroatiaLocale; 226 | } 227 | 228 | /** 捷克 */ 229 | private static volatile Locale sCzechRepublicLocale; 230 | 231 | public static Locale getCzechRepublicLocale() { 232 | if (sCzechRepublicLocale == null) { 233 | sCzechRepublicLocale = new Locale("cs"); 234 | } 235 | return sCzechRepublicLocale; 236 | } 237 | 238 | /** 丹麦文 */ 239 | private static volatile Locale sDanishLocale; 240 | 241 | public static Locale getDanishLocale() { 242 | if (sDanishLocale == null) { 243 | sDanishLocale = new Locale("da"); 244 | } 245 | return sDanishLocale; 246 | } 247 | 248 | /** 迪维希语 */ 249 | private static volatile Locale sDhivehiLocale; 250 | 251 | public static Locale getDhivehiLocale() { 252 | if (sDhivehiLocale == null) { 253 | sDhivehiLocale = new Locale("div"); 254 | } 255 | return sDhivehiLocale; 256 | } 257 | 258 | /** 荷兰 */ 259 | private static volatile Locale sNetherlandsLocale; 260 | 261 | public static Locale getNetherlandsLocale() { 262 | if (sNetherlandsLocale == null) { 263 | sNetherlandsLocale = new Locale("nl"); 264 | } 265 | return sNetherlandsLocale; 266 | } 267 | 268 | /** 法罗语 */ 269 | private static volatile Locale sFaroeseLocale; 270 | 271 | public static Locale getFaroeseLocale() { 272 | if (sFaroeseLocale == null) { 273 | sFaroeseLocale = new Locale("fo"); 274 | } 275 | return sFaroeseLocale; 276 | } 277 | 278 | /** 爱沙尼亚 */ 279 | private static volatile Locale sEstoniaLocale; 280 | 281 | public static Locale getEstoniaLocale() { 282 | if (sEstoniaLocale == null) { 283 | sEstoniaLocale = new Locale("et"); 284 | } 285 | return sEstoniaLocale; 286 | } 287 | 288 | /** 波斯语 */ 289 | private static volatile Locale sFarsiLocale; 290 | 291 | public static Locale getFarsiLocale() { 292 | if (sFarsiLocale == null) { 293 | sFarsiLocale = new Locale("fa"); 294 | } 295 | return sFarsiLocale; 296 | } 297 | 298 | /** 芬兰语 */ 299 | private static volatile Locale sFinnishLocale; 300 | 301 | public static Locale getFinnishLocale() { 302 | if (sFinnishLocale == null) { 303 | sFinnishLocale = new Locale("fi"); 304 | } 305 | return sFinnishLocale; 306 | } 307 | 308 | /** 加利西亚 */ 309 | private static volatile Locale sGaliciaLocale; 310 | 311 | public static Locale getGaliciaLocale() { 312 | if (sGaliciaLocale == null) { 313 | sGaliciaLocale = new Locale("gl"); 314 | } 315 | return sGaliciaLocale; 316 | } 317 | 318 | /** 格鲁吉亚州 */ 319 | private static volatile Locale sGeorgiaLocale; 320 | 321 | public static Locale getGeorgiaLocale() { 322 | if (sGeorgiaLocale == null) { 323 | sGeorgiaLocale = new Locale("ka"); 324 | } 325 | return sGeorgiaLocale; 326 | } 327 | 328 | /** 希腊 */ 329 | private static volatile Locale sGreeceLocale; 330 | 331 | public static Locale getGreeceLocale() { 332 | if (sGreeceLocale == null) { 333 | sGreeceLocale = new Locale("el"); 334 | } 335 | return sGreeceLocale; 336 | } 337 | 338 | /** 古吉拉特语 */ 339 | private static volatile Locale sGujaratiLocale; 340 | 341 | public static Locale getGujaratiLocale() { 342 | if (sGujaratiLocale == null) { 343 | sGujaratiLocale = new Locale("gu"); 344 | } 345 | return sGujaratiLocale; 346 | } 347 | 348 | /** 希伯来 */ 349 | private static volatile Locale sHebrewLocale; 350 | 351 | public static Locale getHebrewLocale() { 352 | if (sHebrewLocale == null) { 353 | sHebrewLocale = new Locale("he"); 354 | } 355 | return sHebrewLocale; 356 | } 357 | 358 | /** 北印度语 */ 359 | private static volatile Locale sHindiLocale; 360 | 361 | public static Locale getHindiLocale() { 362 | if (sHindiLocale == null) { 363 | sHindiLocale = new Locale("hi"); 364 | } 365 | return sHindiLocale; 366 | } 367 | 368 | /** 匈牙利 */ 369 | private static volatile Locale sHungaryLocale; 370 | 371 | public static Locale getHungaryLocale() { 372 | if (sHungaryLocale == null) { 373 | sHungaryLocale = new Locale("hu"); 374 | } 375 | return sHungaryLocale; 376 | } 377 | 378 | /** 冰岛语 */ 379 | private static volatile Locale sIcelandicLocale; 380 | 381 | public static Locale getIcelandicLocale() { 382 | if (sIcelandicLocale == null) { 383 | sIcelandicLocale = new Locale("is"); 384 | } 385 | return sIcelandicLocale; 386 | } 387 | 388 | /** 印尼 */ 389 | private static volatile Locale sIndonesiaLocale; 390 | 391 | public static Locale getIndonesiaLocale() { 392 | if (sIndonesiaLocale == null) { 393 | sIndonesiaLocale = new Locale("id"); 394 | } 395 | return sIndonesiaLocale; 396 | } 397 | 398 | /** 卡纳达语 */ 399 | private static volatile Locale sKannadaLocale; 400 | 401 | public static Locale getKannadaLocale() { 402 | if (sKannadaLocale == null) { 403 | sKannadaLocale = new Locale("kn"); 404 | } 405 | return sKannadaLocale; 406 | } 407 | 408 | /** 哈萨克语 */ 409 | private static volatile Locale sKazakhLocale; 410 | 411 | public static Locale getKazakhLocale() { 412 | if (sKazakhLocale == null) { 413 | sKazakhLocale = new Locale("kk"); 414 | } 415 | return sKazakhLocale; 416 | } 417 | 418 | /** 贡根文 */ 419 | private static volatile Locale sKonkaniLocale; 420 | 421 | public static Locale getKonkaniLocale() { 422 | if (sKonkaniLocale == null) { 423 | sKonkaniLocale = new Locale("kok"); 424 | } 425 | return sKonkaniLocale; 426 | } 427 | 428 | /** 吉尔吉斯斯坦 */ 429 | private static volatile Locale sKyrgyzLocale; 430 | 431 | public static Locale getKyrgyzLocale() { 432 | if (sKyrgyzLocale == null) { 433 | sKyrgyzLocale = new Locale("ky"); 434 | } 435 | return sKyrgyzLocale; 436 | } 437 | 438 | /** 拉脱维亚 */ 439 | private static volatile Locale sLatviaLocale; 440 | 441 | public static Locale getLatviaLocale() { 442 | if (sLatviaLocale == null) { 443 | sLatviaLocale = new Locale("lv"); 444 | } 445 | return sLatviaLocale; 446 | } 447 | 448 | /** 立陶宛 */ 449 | private static volatile Locale sLithuaniaLocale; 450 | 451 | public static Locale getLithuaniaLocale() { 452 | if (sLithuaniaLocale == null) { 453 | sLithuaniaLocale = new Locale("lt"); 454 | } 455 | return sLithuaniaLocale; 456 | } 457 | 458 | /** 马其顿 */ 459 | private static volatile Locale sMacedoniaLocale; 460 | 461 | public static Locale getMacedoniaLocale() { 462 | if (sMacedoniaLocale == null) { 463 | sMacedoniaLocale = new Locale("mk"); 464 | } 465 | return sMacedoniaLocale; 466 | } 467 | 468 | /** 马来 */ 469 | private static volatile Locale sMalayLocale; 470 | 471 | public static Locale getMalayLocale() { 472 | if (sMalayLocale == null) { 473 | sMalayLocale = new Locale("ms"); 474 | } 475 | return sMalayLocale; 476 | } 477 | 478 | /** 马拉地语 */ 479 | private static volatile Locale sMarathiLocale; 480 | 481 | public static Locale getMarathiLocale() { 482 | if (sMarathiLocale == null) { 483 | sMarathiLocale = new Locale("mr"); 484 | } 485 | return sMarathiLocale; 486 | } 487 | 488 | /** 蒙古 */ 489 | private static volatile Locale sMongoliaLocale; 490 | 491 | public static Locale getMongoliaLocale() { 492 | if (sMongoliaLocale == null) { 493 | sMongoliaLocale = new Locale("mn"); 494 | } 495 | return sMongoliaLocale; 496 | } 497 | 498 | /** 挪威 */ 499 | private static volatile Locale sNorwayLocale; 500 | 501 | public static Locale getNorwayLocale() { 502 | if (sNorwayLocale == null) { 503 | sNorwayLocale = new Locale("no"); 504 | } 505 | return sNorwayLocale; 506 | } 507 | 508 | /** 波兰 */ 509 | private static volatile Locale sPolandLocale; 510 | 511 | public static Locale getPolandLocale() { 512 | if (sPolandLocale == null) { 513 | sPolandLocale = new Locale("pl"); 514 | } 515 | return sPolandLocale; 516 | } 517 | 518 | /** 葡萄牙语 */ 519 | private static volatile Locale sPortugueseLocale; 520 | 521 | public static Locale getPortugalLocale() { 522 | if (sPortugueseLocale == null) { 523 | sPortugueseLocale = new Locale("pt"); 524 | } 525 | return sPortugueseLocale; 526 | } 527 | 528 | /** 旁遮普语(印度和巴基斯坦语) */ 529 | private static volatile Locale sPunjabLocale; 530 | 531 | public static Locale getPunjabLocale() { 532 | if (sPunjabLocale == null) { 533 | sPunjabLocale = new Locale("pa"); 534 | } 535 | return sPunjabLocale; 536 | } 537 | 538 | /** 罗马尼亚语 */ 539 | private static volatile Locale sRomanianLocale; 540 | 541 | public static Locale getRomanianLocale() { 542 | if (sRomanianLocale == null) { 543 | sRomanianLocale = new Locale("ro"); 544 | } 545 | return sRomanianLocale; 546 | } 547 | 548 | /** 俄国 */ 549 | private static volatile Locale sRussiaLocale; 550 | 551 | public static Locale getRussiaLocale() { 552 | if (sRussiaLocale == null) { 553 | sRussiaLocale = new Locale("ru"); 554 | } 555 | return sRussiaLocale; 556 | } 557 | 558 | /** 梵文 */ 559 | private static volatile Locale sSanskritLocale; 560 | 561 | public static Locale getSanskritLocale() { 562 | if (sSanskritLocale == null) { 563 | sSanskritLocale = new Locale("sa"); 564 | } 565 | return sSanskritLocale; 566 | } 567 | 568 | /** 斯洛伐克 */ 569 | private static volatile Locale sSlovakiaLocale; 570 | 571 | public static Locale getSlovakiaLocale() { 572 | if (sSlovakiaLocale == null) { 573 | sSlovakiaLocale = new Locale("sk"); 574 | } 575 | return sSlovakiaLocale; 576 | } 577 | 578 | /** 斯洛文尼亚 */ 579 | private static volatile Locale sSloveniaLocale; 580 | 581 | public static Locale getSloveniaLocale() { 582 | if (sSloveniaLocale == null) { 583 | sSloveniaLocale = new Locale("sl"); 584 | } 585 | return sSloveniaLocale; 586 | } 587 | 588 | /** 西班牙语 */ 589 | private static volatile Locale sSpanishLocale; 590 | 591 | public static Locale getSpainLocale() { 592 | if (sSpanishLocale == null) { 593 | sSpanishLocale = new Locale("es"); 594 | } 595 | return sSpanishLocale; 596 | } 597 | 598 | /** 斯瓦希里语 */ 599 | private static volatile Locale sSwahiliLocale; 600 | 601 | public static Locale getSwahiliLocale() { 602 | if (sSwahiliLocale == null) { 603 | sSwahiliLocale = new Locale("sw"); 604 | } 605 | return sSwahiliLocale; 606 | } 607 | 608 | /** 瑞典 */ 609 | private static volatile Locale sSwedenLocale; 610 | 611 | public static Locale getSwedenLocale() { 612 | if (sSwedenLocale == null) { 613 | sSwedenLocale = new Locale("sv"); 614 | } 615 | return sSwedenLocale; 616 | } 617 | 618 | /** 叙利亚语 */ 619 | private static volatile Locale sSyriacLocale; 620 | 621 | public static Locale getSyriacLocale() { 622 | if (sSyriacLocale == null) { 623 | sSyriacLocale = new Locale("syr"); 624 | } 625 | return sSyriacLocale; 626 | } 627 | 628 | /** 坦米尔 */ 629 | private static volatile Locale sTamilLocale; 630 | 631 | public static Locale getTamilLocale() { 632 | if (sTamilLocale == null) { 633 | sTamilLocale = new Locale("ta"); 634 | } 635 | return sTamilLocale; 636 | } 637 | 638 | /** 泰语 */ 639 | private static volatile Locale sThailandLocale; 640 | 641 | public static Locale getThailandLocale() { 642 | if (sThailandLocale == null) { 643 | sThailandLocale = new Locale("th"); 644 | } 645 | return sThailandLocale; 646 | } 647 | 648 | /** 鞑靼语 */ 649 | private static volatile Locale sTatarLocale; 650 | 651 | public static Locale getTatarLocale() { 652 | if (sTatarLocale == null) { 653 | sTatarLocale = new Locale("tt"); 654 | } 655 | return sTatarLocale; 656 | } 657 | 658 | /** 泰卢固语 */ 659 | private static volatile Locale sTeluguLocale; 660 | 661 | public static Locale getTeluguLocale() { 662 | if (sTeluguLocale == null) { 663 | sTeluguLocale = new Locale("te"); 664 | } 665 | return sTeluguLocale; 666 | } 667 | 668 | /** 土耳其语 */ 669 | private static volatile Locale sTurkishLocale; 670 | 671 | public static Locale getTurkishLocale() { 672 | if (sTurkishLocale == null) { 673 | sTurkishLocale = new Locale("tr"); 674 | } 675 | return sTurkishLocale; 676 | } 677 | 678 | /** 乌克兰 */ 679 | private static volatile Locale sUkraineLocale; 680 | 681 | public static Locale getUkraineLocale() { 682 | if (sUkraineLocale == null) { 683 | sUkraineLocale = new Locale("uk"); 684 | } 685 | return sUkraineLocale; 686 | } 687 | 688 | /** 乌尔都语 */ 689 | private static volatile Locale sUrduLocale; 690 | 691 | public static Locale getUrduLocale() { 692 | if (sUrduLocale == null) { 693 | sUrduLocale = new Locale("ur"); 694 | } 695 | return sUrduLocale; 696 | } 697 | 698 | /** 乌兹别克语 */ 699 | private static volatile Locale sUzbekLocale; 700 | 701 | public static Locale getUzbekLocale() { 702 | if (sUzbekLocale == null) { 703 | sUzbekLocale = new Locale("uz"); 704 | } 705 | return sUzbekLocale; 706 | } 707 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/MultiLanguages.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import android.app.Application; 4 | import android.app.LocaleManager; 5 | import android.content.Context; 6 | import android.content.res.Resources; 7 | import android.os.Build; 8 | import android.os.LocaleList; 9 | import android.os.Looper; 10 | import android.os.MessageQueue; 11 | import android.text.TextUtils; 12 | import java.util.Locale; 13 | 14 | /** 15 | * author : Android 轮子哥 16 | * github : https://github.com/getActivity/MultiLanguages 17 | * time : 2019/05/03 18 | * desc : 语种切换框架 19 | */ 20 | @SuppressWarnings("unused") 21 | public final class MultiLanguages { 22 | 23 | /** 应用上下文对象 */ 24 | private static Application sApplication; 25 | 26 | /** 语种变化监听对象 */ 27 | private static OnLanguageListener sLanguageListener; 28 | 29 | /** 30 | * 初始化多语种框架 31 | */ 32 | public static void init(Application application) { 33 | init(application, true); 34 | } 35 | 36 | public static void init(final Application application, boolean inject) { 37 | if (sApplication != null) { 38 | // 如果框架已经初始化过了,则不往下执行 39 | return; 40 | } 41 | sApplication = application; 42 | LanguagesUtils.setDefaultLocale(application); 43 | if (inject) { 44 | ActivityLanguages.inject(application); 45 | } 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 47 | LocaleManager localeManager = application.getSystemService(LocaleManager.class); 48 | if (localeManager != null) { 49 | if (isSystemLanguage(application)) { 50 | // 在没有设置过 setApplicationLocales 方法,里面的默认值会为 LocaleList.getEmptyLocaleList() 51 | // 当设置过一次的 setApplicationLocales 为其他值,在 Android 13 的手机设置中修改语种(系统或者应用)的时候 52 | // 就会导致 context.registerComponentCallbacks 中的 onConfigurationChanged 监听方法没有触发到 53 | // 如果当前应用指定了某种语种,监听方法没有回调是正常的,但是如果当前语种是跟随系统的模式,那么不回调就是有问题的了 54 | // 这是因为系统对 setApplicationLocales 的结果进行了持久化操作,所以这里要重新设置一下,并传入 LocaleList.getEmptyLocaleList() 55 | // Github issue:https://github.com/getActivity/MultiLanguages/issues/37 56 | localeManager.setApplicationLocales(LocaleList.getEmptyLocaleList()); 57 | } else { 58 | localeManager.setApplicationLocales(new LocaleList(getAppLanguage(application))); 59 | } 60 | } 61 | } 62 | // 等所有的任务都执行完了,再设置对系统语种的监听,用户不可能在这点间隙的时间完成切换语言的 63 | // 经过实践证明 IdleHandler 会在第一个 Activity attachBaseContext 之后调用的,所以没有什么问题 64 | Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { 65 | @Override 66 | public boolean queueIdle() { 67 | ConfigurationObserver.register(application); 68 | LocaleChangeReceiver.register(application); 69 | return false; 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * 在上下文的子类中重写 attachBaseContext 方法(用于更新 Context 的语种) 76 | */ 77 | public static Context attach(Context context) { 78 | Locale locale = getAppLanguage(context); 79 | if (LanguagesUtils.getLocale(context).equals(locale)) { 80 | return context; 81 | } 82 | return LanguagesUtils.attachLanguages(context, locale); 83 | } 84 | 85 | /** 86 | * 更新 Context 的语种 87 | */ 88 | public static void updateAppLanguage(Context context) { 89 | updateAppLanguage(context, context.getResources()); 90 | } 91 | 92 | /** 93 | * 更新 Resources 的语种 94 | */ 95 | public static void updateAppLanguage(Context context, Resources resources) { 96 | if (resources == null) { 97 | return; 98 | } 99 | if (LanguagesUtils.getLocale(resources.getConfiguration()).equals(getAppLanguage(context))) { 100 | return; 101 | } 102 | LanguagesUtils.updateLanguages(resources, getAppLanguage(context)); 103 | } 104 | 105 | /** 106 | * 获取 App 的语种 107 | */ 108 | public static Locale getAppLanguage(Context context) { 109 | if (isSystemLanguage(context)) { 110 | return getSystemLanguage(context); 111 | } else { 112 | return LanguagesConfig.readAppLanguageSetting(context); 113 | } 114 | } 115 | 116 | /** 117 | * 设置 App 的语种 118 | * 119 | * @return 语种是否发生改变了 120 | */ 121 | public static boolean setAppLanguage(Context context, Locale newLocale) { 122 | //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 123 | // context.getSystemService(LocaleManager.class).setApplicationLocales(new LocaleList(newLocale)); 124 | //} 125 | // 这里解释一下,在 Android 13 上为什么不用 LocaleManager.setApplicationLocales 来设置语种,原因如下: 126 | // 1. 调用此 API 会自动重启 Activity,而框架是将重启操作放到了外层给开发者去重启 127 | // 2. 上面说了,调用此 API 会重启 Activity,重启也就算了,还顺带闪了一下,这个不能忍 128 | LanguagesConfig.saveAppLanguageSetting(context, newLocale); 129 | if (LanguagesUtils.getLocale(context).equals(newLocale)) { 130 | return false; 131 | } 132 | 133 | Locale oldLocale = LanguagesUtils.getLocale(context); 134 | // 更新 Context 的语种 135 | LanguagesUtils.updateLanguages(context.getResources(), newLocale); 136 | if (context != sApplication) { 137 | // 更新 Application 的语种 138 | LanguagesUtils.updateLanguages(sApplication.getResources(), newLocale); 139 | } 140 | 141 | LanguagesUtils.setDefaultLocale(context); 142 | if (sLanguageListener != null) { 143 | sLanguageListener.onAppLocaleChange(oldLocale, newLocale); 144 | } 145 | return true; 146 | } 147 | 148 | /** 149 | * 获取系统的语种 150 | */ 151 | public static Locale getSystemLanguage(Context context) { 152 | return LanguagesUtils.getSystemLocale(context); 153 | } 154 | 155 | /** 156 | * 是否跟随系统的语种 157 | */ 158 | public static boolean isSystemLanguage(Context context) { 159 | return LanguagesConfig.isSystemLanguage(context); 160 | } 161 | 162 | /** 163 | * 跟随系统语种 164 | * 165 | * @return 语种是否发生改变了 166 | */ 167 | public static boolean clearAppLanguage(Context context) { 168 | LanguagesConfig.clearLanguageSetting(context); 169 | if (LanguagesUtils.getLocale(context).equals(getSystemLanguage(sApplication))) { 170 | return false; 171 | } 172 | 173 | LanguagesUtils.updateLanguages(context.getResources(), getSystemLanguage(sApplication)); 174 | LanguagesUtils.setDefaultLocale(context); 175 | if (context != sApplication) { 176 | // 更新 Application 的语种 177 | LanguagesUtils.updateLanguages(sApplication.getResources(), getSystemLanguage(sApplication)); 178 | } 179 | return true; 180 | } 181 | 182 | /** 183 | * 设置默认的语种(越早设置越好) 184 | */ 185 | public static void setDefaultLanguage(Locale locale) { 186 | LanguagesConfig.setDefaultLanguage(locale); 187 | } 188 | 189 | /** 190 | * 对比两个语言是否是同一个语种(比如:中文有简体和繁体,但是它们都属于同一个语种) 191 | */ 192 | public static boolean equalsLanguage(Locale locale1, Locale locale2) { 193 | return TextUtils.equals(locale1.getLanguage(), locale2.getLanguage()); 194 | } 195 | 196 | /** 197 | * 对比两个语言是否是同一个地方的(比如:中国大陆用的中文简体,中国台湾用的中文繁体) 198 | */ 199 | public static boolean equalsCountry(Locale locale1, Locale locale2) { 200 | return equalsLanguage(locale1, locale2) && 201 | TextUtils.equals(locale1.getCountry(), locale2.getCountry()); 202 | } 203 | 204 | /** 205 | * 获取某个语种下的 String 206 | */ 207 | public static String getLanguageString(Context context, Locale locale, int id) { 208 | return generateLanguageResources(context, locale).getString(id); 209 | } 210 | 211 | /** 212 | * 生成某个语种下的 Resources 对象 213 | */ 214 | public static Resources generateLanguageResources(Context context, Locale locale) { 215 | return LanguagesUtils.generateLanguageResources(context, locale); 216 | } 217 | 218 | /** 219 | * 设置语种变化监听器 220 | */ 221 | public static void setOnLanguageListener(OnLanguageListener listener) { 222 | sLanguageListener = listener; 223 | } 224 | 225 | /** 226 | * 设置保存的 SharedPreferences 文件名(请在 Application 初始化之前设置,可以放在 Application 中的代码块或者静态代码块) 227 | */ 228 | public static void setSharedPreferencesName(String name) { 229 | LanguagesConfig.setSharedPreferencesName(name); 230 | } 231 | 232 | /** 233 | * 获取语种变化监听对象 234 | */ 235 | static OnLanguageListener getOnLanguagesListener() { 236 | return sLanguageListener; 237 | } 238 | } -------------------------------------------------------------------------------- /library/src/main/java/com/hjq/language/OnLanguageListener.java: -------------------------------------------------------------------------------- 1 | package com.hjq.language; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * author : Android 轮子哥 7 | * github : https://github.com/getActivity/MultiLanguages 8 | * time : 2021/01/18 9 | * desc : 语种变化监听器 10 | */ 11 | public interface OnLanguageListener { 12 | 13 | /** 14 | * 当前应用语种发生变化时回调 15 | * 16 | * @param oldLocale 旧语种 17 | * @param newLocale 新语种 18 | */ 19 | void onAppLocaleChange(Locale oldLocale, Locale newLocale); 20 | 21 | /** 22 | * 手机系统语种发生变化时回调 23 | * 24 | * @param oldLocale 旧语种 25 | * @param newLocale 新语种 26 | */ 27 | void onSystemLocaleChange(Locale oldLocale, Locale newLocale); 28 | } -------------------------------------------------------------------------------- /picture/demo_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/picture/demo_code.png -------------------------------------------------------------------------------- /picture/dynamic_figure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getActivity/MultiLanguages/fa343639fea04f7b86c074d7259fab1ee78ea69f/picture/dynamic_figure.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------