Android主题切换

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:

kotlin 复制代码
/**
 * 创建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对象。

kotlin 复制代码
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

kotlin 复制代码
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

kotlin 复制代码
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

kotlin 复制代码
interface SkinChangeListener {

    fun onSkinChange(skinName: Int)

}

2.2 使用demo

布局

xml 复制代码
<?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

kotlin 复制代码
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

kotlin 复制代码
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形式对主题进行切换。

相关推荐
儿歌八万首16 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-194319 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
Jinkxs19 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin
&有梦想的咸鱼&19 小时前
Kotlin委托机制的底层实现深度解析(74)
android·开发语言·kotlin
LDORntKQH19 小时前
基于深度强化学习的混合动力汽车能量管理策略 1.利用DQN算法控制电池和发动机发电机组的功率分配 2
android
冬奇Lab19 小时前
Android 15 ServiceManager与Binder服务注册深度解析
android·源码·源码阅读
2501_9160088921 小时前
深入解析iOS机审4.3原理与混淆实战方法
android·java·开发语言·ios·小程序·uni-app·iphone
独行soc1 天前
2026年渗透测试面试题总结-20(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
常利兵1 天前
2026年,Android开发已死?不,它正迎来黄金时代!
android
Risehuxyc1 天前
备份三个PHP程序
android·开发语言·php