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形式对主题进行切换。

相关推荐
帅锅锅0073 小时前
Android.mk 编辑脚本
android
火柴就是我4 小时前
Android 记录View绘制坐标抖动问题
android
余衫马4 小时前
Ubuntu24.04 安卓模拟器安装指南
android·容器·模拟器
诺诺Okami5 小时前
Android Framework-WMS-动画-初步认识
android
用户2018792831675 小时前
Try-Catch-Finally中的Return执行顺序:一个外卖配送的故事
android
wj0718421545 小时前
Android 内存优化 第2篇
android
用户2018792831675 小时前
浅析Hanlder消息队列处理各种消息的流程
android
用户2018792831675 小时前
浅析Hanlder处理延时消息的流程
android
用户095 小时前
Android面试基础篇(一):基础架构与核心组件深度剖析
android·面试·kotlin