Android 手写实现插件化换肤框架 兼容Android10 Android11

Posted 安卓开发-顺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 手写实现插件化换肤框架 兼容Android10 Android11相关的知识,希望对你有一定的参考价值。

目录

一、收集所有需要换肤的view及相关属性

二、统一为所有Activity设置工厂(兼容Android9以上)

三、加载皮肤包资源

四、处理支持库或者自定义view的换肤

五、处理状态栏换肤

六、对代码动态设置颜色、背景的业务场景进行单独处理


实现插件化换肤,有以下几个关键问题要处理

  1. 收集所有需要换肤的view及相关属性
  2. 统一处理所有Activity的换肤工作(每一个Activity都要进行换肤处理)
  3. 加载皮肤包资源
  4. 处理支持库或者自定义view的换肤
  5. 处理状态栏换肤
  6. 对代码动态设置颜色、背景的业务场景进行单独处理

接下来,我们将完成以上6步的内容,继续阅读前,建议先阅读我的另外两篇文章作为基础知识:

Activity setContentView背后的一系列源码分析

Android 让反射变的简单

先看下效果 

换肤前:

 

换肤后:

 

 所有源码会上传到github,建议下载源码,对照源码来阅读本篇文章,下载地址:

GitHub - ZS-ZhangsShun/EasySkinSwitch: 插件化换肤框架

一、收集所有需要换肤的view及相关属性

 通过系统预留的用于创建view的Factory来接管view的创建,接管的目的:

1、创建出view后对其进行相关颜色、背景的属性的设置,从而完成换肤操

2、把需要换肤的view缓存起来 用户动态的触发换肤时,直接对这些view进行换肤操作。

说明:所有的view都会通过LayoutInflater类来完成创建,而在创建之前系统会先判断是否有预设的

工厂,如果有则会先通过工厂来创建,详见其tryCreateView方法(代码是android-31 Android12)

 因此我们写一个工厂SkinLayoutInflaterFactory来实现Factory2,实现其onCreateView方法,在这个方法里接管view的创建工作

/**
 * view 生产工厂类,用于代替系统进行view生产 生产过程参考系统源码即可
 * 1、在生成过程中对需要换肤的view进行缓存
 * 2、如果已经设置了相关皮肤,生成完view后立刻对其进行相应皮肤的设置
 */
class SkinLayoutInflaterFactory : LayoutInflater.Factory2, Observer 

    var activity: Activity? = null
    var skinCache: SkinCache? = null

    //记录View的构造函数结构,通过两个参数的构造函数去反射view 这里参考系统源码
    private val mConstructorSignature = arrayOf(
        Context::class.java, AttributeSet::class.java
    )

    //缓存view的构造函数 参考系统源码
    private val sConstructorMap = HashMap<String, Constructor<out View>>()

    private val mClassPrefixList = arrayOf(
        "android.widget.",
        "android.webkit.",
        "android.app.",
        "android.view."
    )

    constructor(activity: Activity) 
        this.activity = activity
        skinCache = SkinCache()
    


    /**
     * 对于Factory2 系统会回调4个参数的方法 在源码中可以看到
     * 详见 LayoutInflater --- tryCreateView 方法
     */
    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? 
        //先尝试按照系统的view("android.widget.xxx", "android.webkit.xxx", "android.app.xxx", "android.view.xxx")去创建
        var view: View? = tryCreateView(name, context, attrs)
        if (null == view) 
            //如果不是这几个包下的view 直接通过反射创建
            view = createView(name, context, attrs)
        
        if (null != view) 
            LogUtil.i("success create view $name ")
            //1.如果是可以换肤的view则缓存相关信息
            val skinView = skinCache?.checkAndCache(view, attrs)
            //2.判断是否有设置的皮肤,有则对支持换肤的view进行换肤
            if (!ResourcesManager.isDefaultSkin) 
                skinView?.applySkin()
            
         else 
            LogUtil.i("view is null $attrs.getAttributeName(0)")
        
        return view
    

    private fun tryCreateView(name: String, context: Context, attrs: AttributeSet): View? 
        //如果包含 . 可能是自定义view,或者谷歌出的一些支持库或者Material Design里面的view等
        //总之 就是 xxx.xxx.xxx这种格式的view
        if (-1 != name.indexOf('.')) 
            return null
        
        //不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
        for (i in mClassPrefixList.indices) 
            val view = createView(mClassPrefixList[i].toString() + name, context, attrs)
            if (view != null) 
                return view
            
        
        return null
    

    /**
     * 通过反射创建view 参考系统源码
     */
    private fun createView(name: String, context: Context, attrs: AttributeSet): View? 
        val constructor: Constructor<out View>? = findConstructor(context, name)
        try 
            return constructor?.newInstance(context, attrs)
         catch (e: Exception) 
        
        return null
    

    private fun findConstructor(context: Context, name: String): Constructor<out View>? 
        var constructor: Constructor<out View>? = sConstructorMap[name]
        if (constructor == null) 
            try 
                val clazz = context.classLoader.loadClass(name).asSubclass(View::class.java)
                constructor =
                    clazz.getConstructor(mConstructorSignature[0], mConstructorSignature[1])
                sConstructorMap[name] = constructor
             catch (e: Exception) 
            
        
        return constructor
    

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? 
        return null
    

    /**
     * 观察者模式 有通知来就执行 用户手动点击换肤 会通过
     * SkinManager.notifyObservers通知过来
     */
    override fun update(o: Observable?, arg: Any?) 
        SkinThemeUtils.updateStatusBarColor(activity!!, R.color.status_bar)
        skinCache?.applySkin()
        //MainActivity 里面动态设置底部tab样式需要单独处理
        if (activity is SkinViewSupportInter) 
            (activity as SkinViewSupportInter).applySkin()
        
    

    /**
     * 释放当前缓存的资源
     */
    fun destroy() 
        sConstructorMap.clear()
        skinCache?.clear()
    

 这里实现了observer,这样每一个工厂都做为一个观察者,用户点击换肤时只需要通知一下这些观察者让他们执行换肤任务即可。

二、统一为所有Activity设置工厂(兼容Android9以上)

因为每一个Activity都需要一个Factory来接管view的创建工作,因此我们可以利用系统的ActivityLifecycleCallbacks机制来对Activity统一添加Factory。

说明:在ActivityLifecycleCallbacks的onActivityCreated中执行Factory的设置工作,刚好能赶在Activity的setContentView方法前面, 因为setContentView方法最终会调到LayoutInflater里面去创建view,所以我们这样操作不耽误当前页面的换肤工作。

我们写一个SkinActLifeCallback继承自Application.ActivityLifecycleCallbacks

class SkinActLifeCallback : Application.ActivityLifecycleCallbacks 
    private val mLayoutInflaterFactories: ArrayMap<Activity, SkinLayoutInflaterFactory> = ArrayMap()
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) 
        //在此处对Activity进行view生产工厂的设置,将会在setContentView之前执行此处代码
        //原理是:Activity的源码中在onCreate方法中会执行dispatchActivityCreated方法
        //然后就会回调到这里来,这一步在我们写的Activity的onCreate方法中的super.onCreate(savedInstanceState)里来执行的
        //而setContentView是在super.onCreate(savedInstanceState)之后调用
        //因为系统只允许设置一次factory 所以这里通过反射修改对应的系统变量 实现多次设置 但是 9.0以上不允许在反射修改mFactorySet 的值
        //因此我们这里直接通过反射给进行赋值 下面这个方法就是源码设置Factory2的地方 我们反射实现mFactorySet = true后面部分的代码
//        fun setFactory2(factory: Factory2?) 
//            check(!mFactorySet)  "A factory has already been set on this LayoutInflater" 
//            if (factory == null) 
//                throw NullPointerException("Given factory can not be null")
//            
//            mFactorySet = true
//            if (mFactory == null) 
//                mFactory2 = factory
//                mFactory = mFactory2
//             else 
//                mFactory2 = LayoutInflater.FactoryMerger(factory, factory, mFactory, mFactory2)
//                mFactory = mFactory2
//            
//        
        val factory2 = SkinLayoutInflaterFactory(activity)
        LogUtil.i("cur sdk version is $Build.VERSION.SDK_INT")
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) 
            //因为系统只允许设置一次factory 所以这里通过反射修改对应的系统变量 实现多次设置
            try 
                val javaClass = LayoutInflater::class.java
                val declaredField = javaClass.getDeclaredField("mFactorySet")
                declaredField.isAccessible = true
                declaredField.set(activity.layoutInflater, false)
             catch (e: java.lang.Exception) 
                e.printStackTrace()
            

            //此种方式不兼容5.0以下
//        activity.layoutInflater.factory2 = null
            //这种设置方式更好,系统对5.0以下做了兼容处理
            LayoutInflaterCompat.setFactory2(activity.layoutInflater, factory2)
         else 
            //兼容Android9.0以上
            val layoutInflater = activity.layoutInflater
            val javaClass = LayoutInflater::class.java
            try 
                val mFactory2 = javaClass.getDeclaredField("mFactory2")
                val mFactory = javaClass.getDeclaredField("mFactory")
                mFactory2.isAccessible = true
                mFactory.isAccessible = true
                val mFactoryValue = mFactory.get(layoutInflater)
                val mFactory2Value = mFactory2.get(layoutInflater)
                if (mFactoryValue == null) 
                    mFactory2.set(layoutInflater, factory2)
                    mFactory.set(layoutInflater, factory2)
                 else 
                    val clazz = Class.forName("android.view.LayoutInflater\\$FactoryMerger")
                    val size2 = clazz.declaredConstructors.size
                    LogUtil.i("size2=$size2")

                    val constructor = clazz.getConstructor(
                        LayoutInflater.Factory::class.java,
                        Factory2::class.java,
                        LayoutInflater.Factory::class.java,
                        Factory2::class.java
                    )
                    constructor.isAccessible = true
                    val newInstance = constructor.newInstance(
                        factory2,
                        factory2,
                        mFactoryValue as LayoutInflater.Factory,
                        mFactory2Value as Factory2
                    )
                    mFactory2.set(layoutInflater, newInstance)
                    mFactory.set(layoutInflater, newInstance)
                
             catch (e: Exception) 
                try 
                    val mFactory2 = javaClass.getDeclaredField("mFactory2")
                    val mFactory = javaClass.getDeclaredField("mFactory")
                    mFactory2.isAccessible = true
                    mFactory.isAccessible = true
                    mFactory2.set(layoutInflater, factory2)
                    mFactory.set(layoutInflater, factory2)
                 catch (e: Exception) 
                    e.printStackTrace()
                
            
        


        //设置完工厂 还要将工厂作为观察者缓存起来,动态调用换肤功能时,可以通知所有工厂进行换肤工作
        //这里利用系统自带的观察者类Observable来实现 SkinManager 继承自Observable
        //SkinLayoutInflaterFactory 实现 Observal接口
        mLayoutInflaterFactories[activity] = factory2
        SkinManager.addObserver(factory2)
    

    override fun onActivityStarted(activity: Activity) 
    

    override fun onActivityResumed(activity: Activity) 
    

    override fun onActivityPaused(activity: Activity) 
    

    override fun onActivityStopped(activity: Activity) 
    

    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) 
    

    override fun onActivityDestroyed(activity: Activity) 
        //在这里把作为观察者的工厂移除
        val factory2: SkinLayoutInflaterFactory? = mLayoutInflaterFactories.remove(activity)
        //释放资源
        factory2?.destroy()
        SkinManager.deleteObserver(factory2)
    

 (1)这里的关键是通过反射来把第一步创建的Factory2设置到LayoutInflater里面去,9.0以上不允许在反射修改mFactorySet 的值 ,因此我们这里针对9.0以上直接通过反射把我们的工厂赋值给Factory2,和Factory

(2)同时将每个Activity的工厂作为观察者保存起来

三、加载皮肤包资源

(1)对于加载我们自己皮肤包资源这一块,要利用pms来解析我们的皮肤包(.apk文件)得到资源相关信息,然后利用系统AssetManager去加载资源

(2)注意:加载资源需要先将我们的皮肤包路径添加到系统中,建议调用AssetManager的addAssetPath或者addAssetPathInternal方法,这里仍然需要需要通过反射来调用

(3)皮肤包中的所有资源名称和宿主app包中的资源名称是完全一致的,例如主页背景颜色都叫R.color.home_bg_color,只不过对应的值不一样(例如一个是#ff00ff 一个是 0000ff)

(4)在加载资源时我们可以通过宿主app的资源id 得到资源名称和类型(resources.getResourceEntryName()、resources.getResourceTypeName()),然后在通过名称和类型找到皮肤包中对应资源id (resources.getIdentifier())

以上就是皮肤包资源的加载流程和思路,下面上代码:

写一个SkinManager类来管理皮肤资源,通过loadSkin方法来加载指定路径下的皮肤

/**
 * 继承自Observable 的目的是使用系统写好的这套观察者模式
 * 后面可以直接调用notifyObservers方法来通知各个Activity的观察者(SkinLayoutInflaterFactory)去更新皮肤
 */
object SkinManager : Observable() 
    /**
     * 初始化换肤相关代码
     */
    fun init(app: Application) 
        //注册Activity生命周期监听
        app.registerActivityLifecycleCallbacks(SkinActLifeCallback())
        //初始化资源
        ResourcesManager.init(app)
        //初始化全局变量
        SkinConstants.appContext = app
        SkinConstants.spCommon =
            app.getSharedPreferences(SkinConstants.SP_TAG, Context.MODE_PRIVATE)
        //如果用户有设置皮肤 则加载
        val skinPath = SkinConstants.spCommon!!.getString(SkinConstants.SP_SKIN_PATH, "")
        if (!TextUtils.isEmpty(skinPath)) 
            loadSkin(skinPath)
        
    

    /**
     * 记载皮肤并应用
     *
     * @param skinPath 皮肤路径 如果为空则使用默认皮肤
     */
    fun loadSkin(skinPath: String?) 
        if (SkinConstants.appContext == null) 
            throw Exception("please call SkinManager.init(appContext) first")
        

        if (TextUtils.isEmpty(skinPath)) 
            //还原默认皮肤
            ResourcesManager.reset()
            //保存
            SkinConstants.spCommon!!.edit().putString(SkinConstants.SP_SKIN_PATH, "").apply()
         else 
            try 
                //宿主app的 resources;
                val appResource: Resources = SkinConstants.appContext!!.resources
                //反射创建AssetManager 与 Resource
                val assetManager = AssetManager::class.java.newInstance()
                //资源路径设置 目录或压缩包
                val addAssetPath: Method = assetManager.javaClass.getMethod(
                    "addAssetPath",
                    String::class.java
                )
                addAssetPath.invoke(assetManager, skinPath)

                //根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resources
                val skinResource =
                    Resources(assetManager, appResource.displayMetrics, appResource.configuration)

                //获取外部Apk(皮肤包) 包名
                val mPm = SkinConstants.appContext!!.packageManager
                val info = mPm.getPackageArchiveInfo(skinPath!!, PackageManager.GET_ACTIVITIES)
                val packageName = info?.packageName
                if (TextUtils.isEmpty(packageName)) 
                    Log.i(
                        SkinConstants.TAG, "loadSkin error ---------------------------------> \\n" +
                                "skin packageName is null.Are you sure there is an available skin package APK file\\n" +
                                "the path is $skinPath"
                    )
                
                ResourcesManager.applySkin(skinResource, packageName)

                //保存
                SkinConstants.spCommon!!.edit().putString(SkinConstants.SP_SKIN_PATH, skinPath)
                    .apply()
             catch (e: Exception) 
                e.printStackTrace()
            
        
        //通知采集的View 更新皮肤
        //被观察者改变 通知所有观察者
        setChanged()
        notifyObservers(null)
    

该类继承了Observable类,可以作为被观察者来通知所有的LayoutInflaterFactory(观察者)去进行换肤工作。

另外该类提供了初始化的方法,用来初始化相关资源和第二部分提到的SkinActLifeCallback,需要在程序的Application中调用

ResourceManager是具体资源加载的执行者,代码如下:

/**
 * 资源管理器
 */
object ResourcesManager 
    var mSkinPkgName: String? = null
    var isDefaultSkin = true

    // app原始的resource
    var mAppResources: Resources? = null

    // 皮肤包的resource
    var mSkinResources: Resources? = null

    fun init(context: Context) 
        mAppResources = context.resources
    

    fun applySkin(resources: Resources?, pkgName: String ?) 
        mSkinResources = resources
        mSkinPkgName = pkgName
        //是否使用默认皮肤
        isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null
    

    fun reset() 
        mSkinResources = null
        mSkinPkgName = ""
        isDefaultSkin = true
    

    /**
     * 在加载资源时我们可以通过宿主app的资源id 得到资源名称和类型,然后在通过名称和类型找到皮肤包中对应资源id
     */
    private fun getIdFromSkinResource(resId: Int): Int 
        if (isDefaultSkin) 
            return resId
        
        val resName = mAppResources!!.getResourceEntryName(resId)
        val resType = mAppResources!!.getResourceTypeName(resId)
        LogUtil.i("resId = $resId resName = $resName resType= $resType")
        return mSkinResources!!.getIdentifier(resName, resType, mSkinPkgName)
    

    /**
     * 输入主APP的ID,到皮肤APK文件中去找到对应ID的颜色值
     * @param resId
     * @return
     */
    fun getColor(resId: Int): Int 
        if (isDefaultSkin) 
            return mAppResources!!.getColor(resId)
        
        val skinId: Int = getIdFromSkinResource(resId)
        return if (skinId == 0) 
            mAppResources!!.getColor(resId)
         else mSkinResources!!.getColor(skinId)
    

    fun getColorStateList(resId: Int): ColorStateList? 
        if (isDefaultSkin) 
            return mAppResources!!.getColorStateList(resId)
        
        val skinId: Int = getIdFromSkinResource(resId)
        return if (skinId == 0) 
            mAppResources!!.getColorStateList(resId)
         else mSkinResources!!.getColorStateList(skinId)
    

    fun getDrawable(resId: Int): Drawable 
        if (isDefaultSkin) 
            return mAppResources!!.getDrawable(resId)
        
        //通过 app的resource 获取id 对应的 资源名 与 资源类型
        //找到 皮肤包 匹配 的 资源名资源类型 的 皮肤包的 资源 ID
        val skinId: Int = getIdFromSkinResource(resId)
        return if (skinId == 0) 
            mAppResources!!.getDrawable(resId)
         else mSkinResources!!.getDrawable(skinId)
    


    /**
     * 可能是Color 也可能是drawable
     *
     * @return
     */
    fun getBackground(resId: Int): Any 
        val resourceTypeName = mAppResources!!.getResourceTypeName(resId)
        return if ("color" == resourceTypeName) 
            getColor(resId)
         else 
            // drawable
            getDrawable(resId)
        
    

皮肤包写一个空工程只放资源文件就可以了

 在控制台Terminal窗口通过命令

gradlew skinonly:assembleRelease 或者

gradlew skinonly:assembleDebug 构建出apk文件即可

四、处理支持库或者自定义view的换肤

 实现方案:对于支持库或三方的view通过自定义view去继承这些view,例如我们写一个SkinTabLayout 去 继承 com.google.android.material.tabs.TabLayout 然后在SkinTabLayout内部写具体的换肤逻辑即可。

首先写一个换肤的接口:

/**
 * 自定义view 需要实现此接口 以实现自身的换肤功能
 */
interface SkinViewSupportInter 
    fun applySkin()

自定义一个SkinFloatActionView ,继承自系统的com.google.android.material.floatingactionbutton.FloatingActionButton,实现刚才的SkinViewSupportInter接口,这个控件长什么样子?看图:

代码实现: 

/**
 * 自定义view以实现换肤功能
 */
class SkinFloatActionView : FloatingActionButton, SkinViewSupportInter 

    var bgTintColorId: Int = 0

    constructor(context: Context) : this(context, null) 
    

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 

    

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) 
        val obtainStyledAttributes =
            context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, 0)
        bgTintColorId =
            obtainStyledAttributes.getResourceId(R.styleable.FloatingActionButton_backgroundTint, 0)
    

    override fun applySkin() 
        if (bgTintColorId != 0) 
            backgroundTintList = ResourcesManager.getColorStateList(bgTintColorId)
        
    

 按照此思路,所有的xxx.xxx.xxxView  都可以这样做来解决换肤的问题,这个view的换肤逻辑是何时被触发的呢?有两个地方会触发。

1、是view创建完成后会判断是否要换肤

2、是用户点击换肤按钮触发换肤

下面来屡屡这个流程:

先说场景一,view创建完成后判断是否要换肤

在上面的SkinLayoutInflaterFactory中创建完view的时候会做两个非常重要的事情,

   a.如果是可以换肤的view则缓存相关信息,用SkinCache来处理缓存(代码后面贴上)

   b.判断是否有设置的皮肤,有则对支持换肤的view进行换肤

 SkinCache代码:

/**
 * 用于缓存需要换肤的view
 */
class SkinCache 
    private val mAttributes: MutableList<String> = mutableListOf(
        "background",
        "src",
        "textColor",
        "drawableLeft",
        "drawableTop",
        "drawableRight",
        "drawableBottom"
    )

    //记录换肤需要操作的View与属性信息
    private val mSkinViews: MutableList<SkinView> = mutableListOf()

    /**
     * 检测并缓存view
     * 如果不支持换肤 则不缓存
     */
    fun checkAndCache(view: View, attrs: AttributeSet): SkinView? 
        val mSkinPars: MutableList<SkinPair> = ArrayList()

        for (i in 0 until attrs.attributeCount) 
            //获得属性名  textColor/background
            val attributeName: String = attrs.getAttributeName(i)
            if (mAttributes.contains(attributeName)) 
                // #1545634635
                // ?722727272
                // @722727272
                val attributeValue: String = attrs.getAttributeValue(i)
                // 比如color 以#开头表示写死的颜色 不可用于换肤
                if (attributeValue.startsWith("#")) 
                    continue
                
                // 以 ?开头的表示使用 属性
                val resId: Int = if (attributeValue.startsWith("?")) 
                    val attrId = attributeValue.substring(1).toInt()
                    SkinThemeUtils.getResId(view.context, intArrayOf(attrId)).get(0)
                 else 
                    // 正常以 @ 开头
                    attributeValue.substring(1).toInt()
                
                val skinPair = SkinPair(attributeName, resId)
                mSkinPars.add(skinPair)
            
        

        if (mSkinPars.isNotEmpty() || view is SkinViewSupportInter) 
            val skinView = SkinView(view, mSkinPars)
            mSkinViews.add(skinView)
            return skinView
         else 
            return null
        
    

    /**
     * 对所有的view中的所有的属性进行皮肤修改
     */
    fun applySkin() 
        for (mSkinView in mSkinViews) 
            mSkinView.applySkin()
        
    

    fun clear() 
        mAttributes.clear()
        mSkinViews.clear()
    

 如下图,在checkAndCache方法中如果当前view被设置了能够换肤的属性(textColor、background等)或者是SkinViewSupportInter(就是实现了这个接口),则包装成一个SkinView并缓存到集合中(mSkinViews)

 紧接着往下执行,就会触发SkinView的换肤工作,在贴一遍这个图,66行的skinView不为空就是支持换肤

 SkinView代码如下:

/**
 * 每一个支持换肤的view 都包装组成一个SkinView
 */
class SkinView 
    lateinit var view: View
    lateinit var skinPairs: MutableList<SkinPair>

    constructor(view: View, skinPairs: MutableList<SkinPair>) 
        this.view = view
        this.skinPairs = skinPairs
    

    /**
     * 对当前view执行换肤操作
     */
    fun applySkin()
        //如果当前view是自定义view 则 执行其自己的除了常规的background、textColor等属性的换肤逻辑
        if (view is SkinViewSupportInter) 
            (view as SkinViewSupportInter).applySkin()
        
        for ((attributeName, resId) in skinPairs) 
            var left: Drawable? = null
            var top: Drawable? = null
            var right: Drawable? = null
            var bottom: Drawable? = null
            when (attributeName) 
                "background" -> 
                    val background: Any = ResourcesManager.getBackground(resId)
                    //背景可能是 @color 也可能是 @drawable
                    if (background is Int) 
                        view.setBackgroundColor(background)
                     else 
                        ViewCompat.setBackground(view, background as Drawable)
                    
                
                "src" -> 
                    val background: Any = ResourcesManager.getBackground(resId)
                    if (background is Int) 
                        (view as ImageView).setImageDrawable(ColorDrawable((background as Int?)!!))
                     else 
                        (view as ImageView).setImageDrawable(background as Drawable?)
                    
                
                "textColor" -> (view as TextView).setTextColor(
                    ResourcesManager.getColorStateList(
                        resId
                    )
                )
                "drawableLeft" -> left = ResourcesManager.getDrawable(resId)
                "drawableTop" -> top = ResourcesManager.getDrawable(resId)
                "drawableRight" -> right = ResourcesManager.getDrawable(resId)
                "drawableBottom" -> bottom = ResourcesManager.getDrawable(resId)
                else -> 
                
            
            if (null != left || null != right || null != top || null != bottom) 
                (view as TextView).setCompoundDrawablesWithIntrinsicBounds(
                    left, top, right,
                    bottom
                )
            
        
    

我们看到applySkin方法一上来就先去执行我们这些自定义view 的自己的换肤逻辑

 因为自定义的view除了特殊的需要自身处理的属性外,也有可能设置了普通的background等属性,所以31行并没有else的逻辑,而是直接往下执行这些常规属性的换肤逻辑,这样就梳理完了第一种场景的换肤逻辑的触发。

在说场景二,用户点击换肤按钮触发换肤:

用户点击换肤时我们会执行SkinManager的loadSkin方法 

loadSkin方法中准备好相关资源后会去通知所有的观察者执行换肤

 SkinLayoutInflaterFactory 就是我们的观察者,会执行update方法

 以上就梳理完了触发换肤操作的两个场景。设置了皮肤以后APP退出后在进入走的是第一个场景的换肤逻辑。

五、处理状态栏换肤

这个相对比较简单,状态栏的颜色单独定义到colors.xml文件中,换肤时,单独调用一下状态栏的颜色设置即可(找到皮肤包里的颜色值)

我们写一个处理主题的工具类SkinThemeUtils,提供一个updateStatusBarColor的方法,仅仅需要42行一行代码即可。

 需要在哪里调用这个方法来设置主题呢?

(1)在BaseActivity中调用(针对换肤后退出app在进入的场景),BaseActivity相信大家项目里肯定会有一个吧。

(2)用户手动触发换肤时,我们通知观察者执行update的时候执行主题的更换

以上就是对状态栏的处理过程。 

六、对代码动态设置颜色、背景的业务场景进行单独处理

 这里举个例子,我们主页底部tab的图标和颜色都是根据用户点击来动态设置的

像这种情况就要单独处理,为此我们做了以下处理:

(1)设置Tab的颜色和图标时用我们封装好的ResourcesManager来获取资源id对应的值

(2) 当前所在的Activity实现SkinViewSupportInter接口,实现接口中的applySkin方法,当触发换肤时,在SkinLayoutInflaterFactory中触发即可

 

 代码动态设置样式的地方以此为例进行处理即可。

GitHub地址:GitHub - ZS-ZhangsShun/EasySkinSwitch: 插件化换肤框架

集成与使用步骤:

第一步:在project的build.gradle 文件中添加JitPack依赖

allprojects 
    repositories 
        ...
        maven  url 'https://jitpack.io' 
    

第二步: 在Module的build.gradle文件中添加对本库的依赖

dependencies 
    ...
    implementation 'com.github.ZS-ZhangsShun:EasySkinSwitch:1.0.0'

第三步:开始使用,步骤如下

(1)初始化,在Application的onCreate方法中执行以下代码

    SkinManager.init(this)

(2)针对支持库或自定义view(简单理解就是在布局文件里的这种 <xxx.xxx.xxxView)需要去实现SkinViewSupportInter接口,并实现其applySkin方法,示例如下:

    /**
    * 自定义view以实现换肤功能
    */
    class SkinFloatActionView : FloatingActionButton, SkinViewSupportInter 
      var bgTintColorId: Int = 0
      constructor(context: Context) : this(context, null) 
      
    
      constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) 
    
      
    
      constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
          context,
          attrs,
          defStyleAttr
      ) 
          val obtainStyledAttributes =
          context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, 0)
          bgTintColorId =
          obtainStyledAttributes.getResourceId(R.styleable.FloatingActionButton_backgroundTint, 0)
      
    
      override fun applySkin() 
          if (bgTintColorId != 0) 
              backgroundTintList = ResourcesManager.getColorStateList(bgTintColorId)
          
      
    

(3)对主题颜色进行单独换肤处理

    状态栏的颜色单独定义到colors.xml文件中,换肤时,单独调用一下状态栏的颜色设置即可
    建议在项目的BaseActivity的onCreate方法中调用库中的SkinThemeUtils.updateStatusBarColor方法如下:
    (R.color.status_bar 需要开发者自定义创建)

    /**
      * 基类Activity 统一设置主题啥的
      */
      open class BaseActivity : AppCompatActivity() 
          override fun onCreate(savedInstanceState: Bundle?) 
              super.onCreate(savedInstanceState)
              SkinThemeUtils.updateStatusBarColor(this, R.color.status_bar)
          
      

(4)代码中动态设置颜色、背景等皮肤相关的地方要单独进行处理

    例如,app工程中我们主页底部tab的图标和颜色都是根据用户点击来动态设置的,这里可以这样处理
    a.设置Tab的颜色和图标时用使用库中封装好的ResourcesManager来获取资源id对应的值
        ResourcesManager.getColor(xxx)
        ResourcesManager.getDrawable(xxx)
    b.当前所在的Activity实现SkinViewSupportInter接口,实现接口中的applySkin方法,当触发换肤时,回进行回调
    请参考app工程中MainActivity的实现方式

(5)需要换肤时调用SkinManager.loadSkin(皮肤包绝对路径)来换肤,如app工程NewsFragment所示:

    //换肤
    SkinManager.loadSkin(EasyVariable.mContext.cacheDir.absolutePath
        + File.separator + "skinonly.apk")
    
    //恢复默认皮肤
    SkinManager.loadSkin(null)

混淆配置

-keep com.zs.skinswitch.** *;

以上是关于Android 手写实现插件化换肤框架 兼容Android10 Android11的主要内容,如果未能解决你的问题,请参考以下文章

插件化换肤方案

Activity布局流程+资源加载过程+插件化换肤思路

Activity布局流程+资源加载过程+插件化换肤思路

04插件化换肤技术实战

04插件化换肤技术实战

04插件化换肤技术实战