手撸动态换肤框架(二)

我们已经学习了Activity setContentView 和Resource加载的过程   没看过可以先阅读一下手撸动态换肤框架(一)

接下来我们就直接手撸一个动态换肤框架

我们分析一下思路   首先,我们需要代理AppCompatActivity类 将我们需要换肤的View收集到一个数组中 然后创建一个使用新的皮肤包的代理Resource类  将所有的View改变背景和图片

心急的同学请直接转移源码

收集所有需要换肤的View

这边就可以使用我们上一章学习到的Factory2类 动态代理AppCompatActivity的加载过程  具体请看代码

package com.dsg.skindemo

import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import java.lang.reflect.Constructor

/**
 * @Project skinDemo
 * @author  DSG
 * @date    2020/6/8
 * @describe
 */
class SkinFactory : LayoutInflater.Factory2 {

    //我们并不需要代理所有的创建过程 所以很多事情都可以交给原先的Delegate类实现
    lateinit var delegate: AppCompatDelegate

    //收集需要换肤的View
    var skinList = arrayListOf<SkinView>()


    override fun onCreateView(
        parent: View?,
        name: String,
        context: Context,
        attrs: AttributeSet
    ): View? {
        //因为我们并不需要实现所有流程 所以createView可以交给原先的代理类完成
        var view = delegate.createView(parent, name, context, attrs)
        if (view == null) {
            //万一系统创建出来是空,那么我们来补救 这边也是参考了系统实现 我们之前应该也都分析了
            mConstructorArgs[0] = context
            try {
                if (-1 == name.indexOf('.')) { //不包含. 说明不带包名,那么我们帮他加上包名
                    view = createViewByPrefix(context, name, sClassPrefixList, attrs)
                } else { //包含. 说明 是权限定名的view name,
                    view = createViewByPrefix(context, name, null, attrs)
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        //关键点 收集需要换肤的View
        collectSkinView(context, attrs, view)

        return view
    }

    private fun collectSkinView(context: Context, attrs: AttributeSet, view: View?) {
        //这边判断了是否使用isSupport自定义属性 但是并没有对自定义View做很好的兼容 
        //对于自定义View  我们可以采用换肤接口来实现 具体实现可以大家自己尝试一下
        val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.Skinable)
        val isSupport = obtainStyledAttributes.getBoolean(R.styleable.Skinable_isSupport, false)
        if (isSupport) {
            val skinView = SkinView()
            //找到支持换肤的view
            val len = attrs.attributeCount
            val attrMap = hashMapOf<String, String>()
            for (i in 0 until len) { //遍历所有属性
                val attrName = attrs.getAttributeName(i)
                val attrValue = attrs.getAttributeValue(i)
                attrMap[attrName] = attrValue //全部存起来
            }

            if (view != null) {
                skinView.view = view
            }
            skinView.attrsMap = attrMap
            skinList.add(skinView)
        }
    }
    
    
    fun changeSkins() {
        //遍历换肤类 实现换肤
        for (skin in skinList) {
            skin.changeSkin()
        }
    }

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

    class SkinView {
        lateinit var view: View
        lateinit var attrsMap: HashMap<String, String>

        fun changeSkin() {
            if (!TextUtils.isEmpty(attrsMap["background"])) { //属性名,例如,这个background,text,textColor....
                val bgId = attrsMap["background"]!!.substring(1).toInt() //属性值,R.id.XXX ,int类型,
                // 这个值,在app的一次运行中,不会发生变化
                val attrType = view.resources.getResourceTypeName(bgId) // 属性类别:比如 drawable ,color
                if (TextUtils.equals(attrType, "drawable")) { //区分drawable和color
                    view.setBackgroundDrawable(SkinEngine.getDrawable(bgId)) //加载外部资源管理器,拿到外部资源的drawable
                } else if (TextUtils.equals(attrType, "color")) {
                    view.setBackgroundColor(SkinEngine.getColor(bgId))
                }
            }

            if (view is TextView) {
                if (!TextUtils.isEmpty(attrsMap["textColor"])) {
                    val textColorId = attrsMap["textColor"]!!.substring(1).toInt()
                    (view as TextView).setTextColor(SkinEngine.getColor(textColorId))
                }
            }

            //那么如果是自定义组件呢
            //那么如果是自定义组件呢
            if (view is ZeroView) { //那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set,
                // 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义
                // ....
            }
        }
    }


    val mConstructorSignature = arrayOf(Context::class.java, AttributeSet::class.java) //
    val mConstructorArgs = arrayOfNulls<Any>(2) //View的构造函数的2个"实"参对象
    private val sConstructorMap = HashMap<String, Constructor<out View?>>() //用映射,将View的反射构造函数都存起来
    private val sClassPrefixList = arrayOf(
        "android.widget.",
        "android.view.",
        "android.webkit."
    )

    /**
     * 反射创建View
     *
     * @param context
     * @param name
     * @param prefixs
     * @param attrs
     * @return
     */
    private fun createViewByPrefix(
        context: Context,
        name: String,
        prefixs: Array<String>?,
        attrs: AttributeSet
    ): View? {
        var constructor: Constructor<out View?>? = sConstructorMap.get(name)
        var clazz: Class<out View?>? = null
        if (constructor == null) {
            try {
                if (prefixs != null && prefixs.size > 0) {
                    for (prefix in prefixs) {
                        clazz = context.classLoader.loadClass(
                            if (prefix != null) prefix + name else name
                        ).asSubclass(View::class.java) //控件
                        if (clazz != null) break
                    }
                } else {
                    if (clazz == null) {
                        clazz = context.classLoader.loadClass(name)
                            .asSubclass(View::class.java)
                    }
                }
                if (clazz == null) {
                    return null
                }
                constructor = clazz.getConstructor(*mConstructorSignature) //拿到 构造方法,
            } catch (e: java.lang.Exception) {
                e.printStackTrace()
                return null
            }
            constructor.isAccessible = true //
            sConstructorMap.put(name, constructor) //然后缓存起来,下次再用,就直接从内存中去取
        }
        val args = mConstructorArgs
        args[1] = attrs
        try { //通过反射创建View对象
            return constructor.newInstance(*args)
        } catch (e: java.lang.Exception) {
            e.printStackTrace()
        }
        return null
    }
    //********************************************************************************
}

代码写的也比较详细   具体实现流程也就是收集需要换肤的View和属性值 然后在合适的时机调用换肤

自定义Resource类

我们思考一下这边需要做哪些事情   大致上来说   也就是使用原先的Resource和AssetManager   实现一个新的Resource   然后使用新的Resource来加载资源

package com.dsg.skindemo

import android.content.Context
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.content.res.Resources
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import java.io.File
import java.lang.Exception

/**
 * @Project skinDemo
 * @author  DSG
 * @date    2020/6/8
 * @describe
 */
object SkinEngine {

    lateinit var mContext: Context
    //外部皮肤包包名
    private var mOutPkgName: String? = null
    //自定义Resource
    private var mOutResource: Resources? = null

    fun init(context: Context) {
        this.mContext = context
    }

    fun load(path: String) {
        val file = File(path)
        if (!file.exists()) {
            return
        }
        try {
            val pm = mContext.packageManager
            val packageInfo = pm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES)
            mOutPkgName = packageInfo.packageName
            //通过反射获取AssetManager类
            val assetManager: AssetManager = AssetManager::class.java.newInstance()
            //反射调用addAssetPath方法 将皮肤Apk资源加入到AssetManger中
            val method = assetManager::class.java.getMethod("addAssetPath", String::class.java)
            method.invoke(assetManager, path)
            mOutResource = Resources(
                assetManager,
                mContext.resources.displayMetrics,
                mContext.resources.configuration
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }

    fun getDrawable(resId: Int): Drawable? {
        if (mOutResource == null) {
            return ContextCompat.getDrawable(mContext, resId)
        }
        //这边有一个问题 就是在皮肤apk和与原先资源文件中 resource.arsc文件中 同一资源的资源id会改变
        //但是我没有遇到这个问题 不知道是因为我的资源文件太少 还是因为R8优化导致
        //解决思路就是 id虽然不一样  但是type和name是一样的
        //通过id找到type和name 然后再找到资源
        val resName = mOutResource!!.getResourceEntryName(resId)
        val outResId = mOutResource!!.getIdentifier(resName, "drawable", mOutPkgName)
        return if (outResId == 0) {
            ContextCompat.getDrawable(mContext, resId)
        } else mOutResource!!.getDrawable(outResId)
    }

    fun getColor(resId: Int): Int {
        if (mOutResource == null) {
            return resId
        }
        val resName = mOutResource!!.getResourceEntryName(resId)
        val outResId = mOutResource!!.getIdentifier(resName, "color", mOutPkgName)
        return if (outResId == 0) {
            resId
        } else mOutResource!!.getColor(outResId)
    }
}

具体注释也写的比较详细   需要注意的点就是resource.arsc问题   然后在高版本中 addAssetPath已经被废弃了   可以使用setApkAssets替代

Hook AppCompatActivity代理

package com.dsg.skindemo

import android.os.Bundle
import android.view.LayoutInflater
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.LayoutInflaterCompat
import java.io.File

/**
 * @Project skinDemo
 * @author  DSG
 * @date    2020/6/8
 * @describe
 */
open class BaseActivity : AppCompatActivity() {
    var ifAllowChangeSkin = true

    lateinit var skinFactory: SkinFactory

    var currentSkin: String? = null
    var skins = arrayOf("skin1.apk", "skin2.apk")


    override fun onCreate(savedInstanceState: Bundle?) {
        //要在super.onCreate调用 否则会报错
        if (ifAllowChangeSkin) {
            skinFactory = SkinFactory()
            skinFactory.delegate = delegate
            val layoutInflater = LayoutInflater.from(this)
            if (layoutInflater.factory == null) {
                LayoutInflaterCompat.setFactory2(layoutInflater, skinFactory)
            }
        }
        super.onCreate(savedInstanceState)
    }

    override fun onResume() {
        super.onResume()
        if (null != currentSkin) {
            changeSkin(currentSkin!!) // 换肤操作必须在setContentView之后
        }
    }

    fun getPath(): String {
        val path: String
        path = if (null == currentSkin) {
            skins[0]
        } else if (skins[0] == currentSkin) {
            skins[1]
        } else if (skins[1] == currentSkin) {
            skins[0]
        } else {
            return "unknown skin"
        }
        return path
    }

    fun changeSkin(path: String) {
        if (ifAllowChangeSkin) {
            var file = File(getExternalFilesDir(""), path)
            SkinEngine.load(file.absolutePath)
            skinFactory.changeSkins()
            currentSkin = path
        }
    }
}

主要看onCreate方法

总结

换肤框架   基本原理就是使用Resource类   加载皮肤apk中的资源 来动态改变项目中的所有资源

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270