Android主题切换
Android主题切换是很多成熟产品必备功能。记得刚工作时候,公司规划后台切换主题功能,作为菜鸟程序员,吓的我后背发凉。要是放在现在,我给他三四个方案去挑。所以,不要气馁,交给时间,走一步,再走一步。
1 常用主题切换方式
1.1 使用Android原生方式
其实Android系统已经提供了一套黑白主题切换方式。上图所示,直接使用限制符方式。
- values 白天主题
- values-night 黑色主题
其他drawable等资源也可以使用添加night限制符方式区分深色主题。
我使用的是Android12设备,打开/关闭深夜模式,资源自动生效,无需做其他处理。
优点: 无需修改代码,使用方便。
缺点: APK发版后,不能动态切换主题。
1.2 资源包修改主题
首先,看下资源调用方式
可以看到资源调用使用了Resources对象,说明咱们的资源就是使用该对象进行处理。
其实,Android资源文件加载最终由Resources对象中AssetManager进行控制。另外,AssetManager
还提供了加载APK资源的能力,可以让咱们加载其他APK文件。
所以,咱们最终可以使用AssetManager加载APK,并且创建新的Resources对象用来持有不同资源文件。
使用三方APK切换原APK自带资源核心:
首先,使用AssetManager通过路径加载APK:
/**
* 创建AssetManager并加载指定的APK路径
*/
@SuppressLint("PrivateApi")
private fun createAssetManager(apkPath: String): AssetManager? {
return try {
val assetManager = AssetManager::class.java.newInstance()
val addAssetPathMethod: Method = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
addAssetPathMethod.invoke(assetManager, apkPath)
assetManager
} catch (e: Exception) {
e.printStackTrace()
null
}
}
其次,创建三方Resources对象。
private fun loadResource(context: Context, path: String) {
val assetManager = createAssetManager(context.filesDir.path+"/"+path)
currentResource?.let {
val metrics: DisplayMetrics = it.displayMetrics
val config: Configuration = it.configuration
resourcesOther = assetManager?.run {
Resources(assetManager, metrics, config)
}
}
}
在应用的Resources和创建的三方Resources对象进行切换,便可以达到切换主题效果。
优点: 可以灵活进行APK更换,可适配多种类型主题。
缺点: 需要额外提供APK,占用内存。Android开发工作任务变多。
2 资源包修改主题
下面是使用资源包修改主题Demo,仅供参考。
2.1 主题切换基本框架
SkinManager.kt
import android.content.Context
/**
* 为外部提供接口
*/
object SkinManager {
private val resourceManager by lazy { ResourceManager() }
private val changeListenerList = mutableListOf<SkinChangeListener>()
fun init(context: Context,path: String) {
resourceManager.init(context,path)
}
fun addChangeListener(listener: SkinChangeListener) {
changeListenerList.add(listener)
}
fun removeChangeListener(listener: SkinChangeListener) {
changeListenerList.remove(listener)
}
private fun notifySkinChange(skinName: Int) {
changeListenerList.forEach {
it.onSkinChange(skinName)
}
}
fun switchTheme(type: Int) {
resourceManager.switchTheme(type)
notifySkinChange(type)
}
fun getColor(resId: Int): Int {
return resourceManager.getColor(resId)
}
}
ResourceManager.kt
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import android.util.DisplayMetrics
import java.lang.reflect.Method
class ResourceManager {
private val TAG = "ResourceManager"
private var currentResource : Resources? = null
private var resources : Resources? = null
private var resourcesOther : Resources? = null
var appContext: Context? = null
fun init(context: Context, path: String) {
appContext = context
currentResource = context.resources
resources= currentResource
loadResource(context,path)
}
private fun loadResource(context: Context, path: String) {
val assetManager = createAssetManager(context.filesDir.path+"/"+path)
currentResource?.let {
val metrics: DisplayMetrics = it.displayMetrics
val config: Configuration = it.configuration
resourcesOther = assetManager?.run {
Resources(assetManager, metrics, config)
}
}
}
@SuppressLint("DiscouragedApi")
fun getColor(id: Int): Int {
Log.d("ResourceManager", " $currentResource getColor: $id")
return try {
// 获取资源名称
val resourceName = resources?.getResourceEntryName(id)
val resourceType = resources?.getResourceTypeName(id)
Log.d("ResourceManager", "getColor:resourceName = $resourceName resourceType = $resourceType ")
// 在当前主题中查找同名资源
val newResId = currentResource?.getIdentifier(resourceName ?: "", resourceType ?: "", appContext?.packageName)
newResId?.let { currentResource?.getColor(it, null) } ?: resources?.getColor(id, null)!!
}catch(e: Exception) {
e.printStackTrace()
resources?.getColor(id, null)!!
}
}
/**
* 创建AssetManager并加载指定的APK路径
*/
@SuppressLint("PrivateApi")
private fun createAssetManager(apkPath: String): AssetManager? {
return try {
val assetManager = AssetManager::class.java.newInstance()
val addAssetPathMethod: Method = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
addAssetPathMethod.invoke(assetManager, apkPath)
assetManager
} catch (e: Exception) {
e.printStackTrace()
null
}
}
fun switchTheme(type:Int) {
when(type) {
0 -> {
currentResource = resources
}
1 -> {
currentResource = resourcesOther
}
}
}
}
上面代码 context.filesDir.path 就是加载如图:/data/data/包名/files/ 目录内容。通过
AssetManager对象加载事先准备好的APK资源文件,创建一个新的Resources对象进行管理资源。
SkinChangeListener.kt
interface SkinChangeListener {
fun onSkinChange(skinName: Int)
}
2.2 使用demo
布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:background="@color/main_bg">
<!-- <com.kt.android.widget.CustomRecyclerView-->
<!-- android:id="@+id/recyclerview"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="match_parent"/>-->
<TextView
android:id="@+id/switch_button"
android:text="主题切换"
style="@style/CustomButton"/>
</LinearLayout>
MainActivity.kt
import android.os.Bundle
import com.bo.baselibrary.base.base.BaseActivity
import com.bo.baselibrary.utils.jump
import com.example.skinmanager.SkinChangeListener
import com.example.skinmanager.SkinManager
import com.kt.android.databind.DataBindActivity
import com.kt.android.databinding.ActivityMainBinding
import com.kt.android.http.HttpActivity
import com.kt.android.ipc.IPCActivity
import com.kt.android.mvvm.DiceRollActivity
import com.kt.android.notification.NotificationActivity
import com.kt.android.test.test
import com.kt.android.view.ViewActivity
import com.kt.android.widget.CustomRecyclerView
class MainActivity : BaseActivity<ActivityMainBinding>(), SkinChangeListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var b = true
binding.switchButton.setOnClickListener {
b = !b
SkinManager.switchTheme(if (b) 1 else 0)
}
}
override fun onSkinChange(skinName: Int) {
binding.main.setBackgroundColor(SkinManager.getColor(R.color.main_bg))
}
override fun onResume() {
super.onResume()
SkinManager.addChangeListener(this)
}
override fun onPause() {
super.onPause()
SkinManager.removeChangeListener(this)
}
}
App.kt
package com.kt.android
import android.app.Application
import com.example.skinmanager.SkinManager
class App : Application(){
override fun onCreate() {
super.onCreate()
SkinManager.init(this, "android_dark-debug.apk")
}
}
2.2.1 资源文件
创建项目,只需保留资源文件。编译出一个APK包,将包放在指定的应用目录,便可以通过上述资源切换框架使用啦!
下面为原生项目自带资源内容。SkinManager中提供了一个主题切换的方法switchTheme,通过传递不同参数便可以切换不同resources对象。当切换主题后main_bg,就会根据不同的resources去加载不同资源。
对于资源包加载方式可以看出来是可以非常灵活进行定制化开发,还可以通过与后端获取APK形式对主题进行切换。