【Android基础】系统语言和主题切换

本文介绍了车载Android系统的语言切换和主题切换的触发以及适配
以下各个api的开发与测试均在车载设备上,可能在手机上不一定生效。
并且主动触发系统环境切换的方法,只有系统权限的app才可以调用。
触发入口调用
一般厂商,都会自己定义一个切换入口,像系统设置,或者桌面的通知页面。这里面去调用系统的api,来达到切换的目的。
主题切换
主题切换可以直接使用系统的UiModeManager即可。
private val uimodeManager =
appContext.getSystemService(UI_MODE_SERVICE) as UiModeManager
例如深浅主题切换调用:
// UiModeManager.java
/**
* Sets the system-wide night mode.
*
* @param mode the night mode to set
* @see #getNightMode()
* @see #setApplicationNightMode(int)
*/
public void setNightMode(@NightMode int mode) {
if (mService != null) {
try {
mService.setNightMode(mode);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
我们自定义的触发按钮,按下时可以这样设置:
mMainView.findViewById<Button>(R.id.btn_night).setOnClickListener {
uimodeManager.nightMode = UiModeManager.MODE_NIGHT_YES
}
mMainView.findViewById<Button>(R.id.btn_light).setOnClickListener {
uimodeManager.nightMode = UiModeManager.MODE_NIGHT_NO
}
语言切换
通过反射调用ActivityManager的updatePersistentConfiguration方法,即可实现系统语言切换。
/**
* 切换语言
* @param language 语言
*/
fun changeLanguageSettings(language: Locale) {
try {
val activityManagerNative = Class.forName("android.app.ActivityManager")
val am = activityManagerNative.getMethod("getService").invoke(activityManagerNative)
val config = am?.javaClass?.getMethod("getConfiguration")
?.invoke(am) as Configuration
config.setLocale(language)
config.javaClass.getDeclaredField("userSetLocale").setBoolean(config, true)
am.javaClass.getMethod("updatePersistentConfiguration", config.javaClass)
.invoke(am, config)
BackupManager.dataChanged("com.android.providers.settings")
} catch (e: Exception) {
e.message?.let { error(it) }
}
}
触发按钮调用:
mMainView.findViewById<Button>(R.id.btn_chinese).setOnClickListener {
changeLanguageSettings(Locale.SIMPLIFIED_CHINESE)
}
mMainView.findViewById<Button>(R.id.btn_english).setOnClickListener {
changeLanguageSettings(Locale.ENGLISH)
}
适配方app
在上面的app完成触发之后,系统会将环境切换到对应的深浅模式,或者对应的语言状态下,这时候其他的app就需要响应刷新自己的页面。
在开发过程中,可以按下面的几种方法来适配。
资源目录设置
首先不管是Activity应用还是浮窗应用,我们都需要在res资源目录下添加对应的语言和主题的资源目录。
语言目录
以英文为例,新建value-en目录,将翻译之后的strings.xml复制进去即可,字段的名称和中文目录是一致的。
主题目录
深色的主题资源放在带-night后缀的目录下。 例如图片等资源,放置于drawable-mdpi-night目录下,color字段放置于values-night目录下。和语言切换一样,图片,颜色等文件名称和浅色主题一致,切换的时候使用 R 类会自己索引。
逻辑代码
Activity型应用
切换主题和语言时,Activity都会销毁重建,按照触发顺序,大致为:
- onConfigurationChanged:当系统配置发生变化时,例如屏幕方向改变或主题切换,Activity会首先调用onConfigurationChanged方法。你可以重写这个方法来处理配置变化,例如重新加载资源或更新UI。
- onSaveInstanceState:在Activity可能被销毁之前,系统会调用onSaveInstanceState方法,允许你保存一些关键的状态信息,以便在Activity重新创建时恢复。
- 全部的流程如下
{thread:main(2) MainActivity:431 onPause}
{thread:main(2) MainActivity:495 onStop}
{thread:main(2) MainActivity:170 onSaveInstanceState}
{thread:main(2) MainActivity:500 onDestroy}
{thread:main(2) MainActivity:131 onCreate}
{thread:main(2) MainActivity:180 initData}
{thread:main(2) MainActivity:400 initView}
{thread:main(2) MainActivity:155 onStart}
{thread:main(2) MainActivity:175 onResume}
{thread:main(2) MainActivity:481 onWindowFocusChanged} onWindowFocusChanged hasFocus: true
重建之后,Activity会按照提前设置好的资源目录进行资源的获取,自动地刷新界面。切到浅色主题,就会拿取drawable-mdpi目录下的资源,深色主题则会拿取drawable-mdpi-night目录下的资源。
Service加浮窗型应用
在车机开发中,经常会设计一些临时性的悬浮窗app,例如天气,时间,快捷车控等功能。
这类app一般的架构为,开机之后,会启动一个Service,然后在Service中获取WindowManager,来进行悬浮窗的添加移除等管理操作。
// LanguageService.kt
private val mWmParams = WindowManager.LayoutParams().apply {
//设置可以显示在状态栏上
flags = (WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
//设置窗口长宽数据
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP
x = 600
y = 0
format = PixelFormat.TRANSLUCENT
}
private val mWindowManager = appContext.getSystemService(WINDOW_SERVICE) as WindowManager
private lateinit var mMainView: View
fun showWindow(){
mMainView = LayoutInflater.from(appContext).inflate(R.layout.layout_language_change, null, false)
mWindowManager.addView(mMainView, mWmParams)
}
不像Activity都是自动化,高层级的悬浮窗app的生命周期比较复杂,需要我们自己去管理。
这时候系统不会自动去重走生命周期,刷新资源了,我们需要手动去切换主题和语言。
首先,需要在service中复写onConfigurationChanged方法。系统在语言和主题切换时,会调用这个方法。主题其他类型的配置切换,例如旋转屏幕等,也会走这个方法。
所以需要设置一个主题(语言)管理类,对比前后的状态,是否这个触发的类型是主题(语言)切换。
监听到了变化之后,有两种方案:
手动刷新置换资源
第一种,对于界面简单的浮窗界面,可以直接在onConfigurationChanged中,重新加载资源,然后重新设置布局。这种方法不用考虑窗口的状态,直接对每个View进行定点刷新,不容易出问题。
语言切换:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LogUtils.i(TAG, "onConfigurationChanged")
mMainView.findViewById<Button>(R.id.btn_chinese).text = getString(R.string.chinese)
mMainView.findViewById<Button>(R.id.btn_english).text = getString(R.string.english)
}
主题切换:
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LogUtils.i(TAG, "onConfigurationChanged")
mMainView.setBackgroundColor(
ResourcesCompat.getColor(this.resources, R.color.theme_test, null)
)
mMainView.findViewById<ImageView>(R.id.iv_close)
.setImageResource(R.drawable.ic_close_dialog)
}
置空重建
第二种方案比较适合复杂的View,内部组件众多,挨个手动替换比较麻烦。
这时就可以模仿Activity的切换方式,直接移除掉之前的view,将其置空后,重新创建一个view,设置好子View的监听方法,然后重新添加到windowManager中。
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LogUtils.i(TAG, "onConfigurationChanged")
mWindowManager.removeView(mMainView)
mMainView = null
mMainView =
LayoutInflater.from(appContext)
.inflate(R.layout.layout_language_change, null, false)
initViews()
mWindowManager.addView(mMainView, mWmParams)
}
这种方法简单粗暴,但是必须要管控好窗口的状态和置空的时机,否则可能会导致内存泄漏。
而且这种方法也会导致窗口的闪烁,最好在系统切换时有一个过度的效果动画。