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

相关推荐
狂热开发者17 分钟前
车载中控要给鸿蒙用户,能不能和 Android 共用一个二维码?
android·华为·harmonyos
.Bryce.1 小时前
T2-LLM评测不等于Agent评测
android·java·人工智能·后端·gpt·agent·1024程序员节
美狐美颜SDK开放平台2 小时前
视频美颜SDK如何应对快速转头与大幅度面部动作?
android·人工智能·ios·音视频·直播美颜sdk·视频美颜sdk
2601_968900772 小时前
玻璃深加工的工艺窗口:4 道工序的温度与时间参数边界
android·java·javascript
好的佩奇012 小时前
Dart 之类修饰符
android·flutter·dart
YB13752 小时前
Jetpack Compose 动画Modifier.animateContentSize()
android
事圆则缓2 小时前
Android 版本号、API Level 和 JDK 版本有什么关系?
android
zhangguojia73 小时前
Android Activity 生命周期结束流程:从 finish() 到 onDestroy() 的跨进程状态调度
android
Jewel1053 小时前
文字为什么总是不居中!记录一个bug
android·flutter·ios
mmsx4 小时前
Android 并发考古:newThread 与 RxJava 共存,以及 GlobalScope 的三宗罪
android·echarts·rxjava