android换肤整理

作者: 有点健忘 | 来源:发表于2019-03-07 11:05 被阅读5次

    来源这里https://www.jianshu.com/p/4c8d46f58c4f
    整理下,方便以后使用,刚写完简单测试没啥问题,以后发现问题再修改

    前言

    核心思路就是用到这个方法
    这个出来很久了,我只记得几年前用的时候就简单的修改页面字体的大小

    LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 
    

    换肤的方法

    1. 如果只是简单的,固定的,那么其实本地写几套主题就可以实现了
      也就是这种,布局里使用 ?attr/主题里的字段
    ?attr/colorPrimary
    

    然后不同的主题指定不同的颜色,图片,大小就行了

    <item name="colorPrimary">@color/colorPrimary</item>
    
    1. 就是根据开头帖子的内容,加载一个本地的apk文件,获取到他的resource
      然后利用下边的方法获取到资源,这种打包成apk的方便网络下载,可以随时添加皮肤
    mOutResource?.getIdentifier(resName, type, mOutPkgName)
    

    工具类

    本工具类使用到了LiveData,方便通知其他页面刷新,并且是用kt写的

    1. LiveDataUtil
      根据一个string的key值,存储相关的LiveData,完事获取LiveData也是通过这个key值。

    换肤操作里主要用了最后两个方法,
    getResourcesLiveData 获取LiveData<Resources>
    observerResourceChange:注册观察者

    import android.arch.lifecycle.LifecycleOwner
    import android.arch.lifecycle.MutableLiveData
    import android.arch.lifecycle.Observer
    import android.content.res.Resources
    
    object LiveDataUtil {
    
        private val bus = HashMap<String, MutableLiveData<Any>>()
    
    
        fun <T> with(key: String, type: Class<T>): MyLiveData<T> {
            if (!bus.containsKey(key)) {
                bus[key] = MyLiveData(key)
                println("create new============$key")
            }
            return bus[key] as MyLiveData<T>
        }
    
        fun with(key: String): MyLiveData<Any> {
            return with(key, Any::class.java)
        }
    
        fun observer(key: String,lifecycleOwner: LifecycleOwner,observer: Observer<Any>){
            with(key).observe(lifecycleOwner,observer)
        }
        fun <T> observer(key: String,type:Class<T>,lifecycleOwner: LifecycleOwner,observer: Observer<T>){
            with(key,type).observe(lifecycleOwner,observer)
        }
    
        fun  remove(key:String,observer: Observer<Any>){
            if(bus.containsKey(key)){
                bus[key]?.removeObserver(observer)
            }
        }
    
        fun clearBus(){
            bus.keys.forEach {
                bus.remove(it)
            }
        }
        class MyLiveData<T> (var key:String):MutableLiveData<T>(){
            override fun removeObserver(observer: Observer<T>) {
                super.removeObserver(observer)
                if(!hasObservers()){
                    bus.remove(key)//多个页面添加了观察者,一个页面销毁这个livedata还需要的,除非所有的观察者都没了 ,才清除这个。
                }
                println("remove===========$key=====${hasObservers()}")
            }
        }
    
    
        fun getResourcesLiveData():MutableLiveData<Resources>{
            return  with(SkinLoadUtil.resourceKey,Resources::class.java)
        }
        fun  observerResourceChange(lifecycleOwner: LifecycleOwner,observer: Observer<Resources>){
            getResourcesLiveData().observe(lifecycleOwner,observer)
        }
    }
    
    1. SkinLoadUtil
      根据传入的apk的sdcard路径,通过反射获取这个apk的assetManager,进而生成对应的resource
      拿到resource也就可以拿到这个apk的资源文件了
      public int getIdentifier(String name, String defType, String defPackage)
    import android.content.Context
    import android.graphics.drawable.Drawable
    import android.content.res.AssetManager
    import android.content.pm.PackageManager
    import android.content.res.Resources
    import java.io.File
    
    
    class SkinLoadUtil private constructor() {
        lateinit var mContext: Context
    
        companion object {
            val instance = SkinLoadUtil()
            val resourceKey = "resourceKey"
        }
    
        fun init(context: Context) {
            this.mContext = context.applicationContext
    
        }
    
        private var mOutPkgName: String? = null// TODO: 外部资源包的packageName
        private var mOutResource: Resources? = null// TODO: 资源管理器
        fun getResources(): Resources? {
            return mOutResource
        }
    
    
        fun load(path: String) {//path 是apk在sdcard的路径
            val file = File(path)
            if (!file.exists()) {
                return
            }
            //取得PackageManager引用
            val mPm = mContext.getPackageManager()
            //“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么?
            val mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES)
            try {
                mOutPkgName = mInfo.packageName//先把包名存起来
                val assetManager: AssetManager//资源管理器
                //TODO: 关键技术点3 通过反射获取AssetManager 用来加载外面的资源包
                assetManager = AssetManager::class.java.newInstance()//反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态
                //addAssetPath方法可以加载外部的资源包
                val addAssetPath = assetManager.javaClass.getMethod("addAssetPath", String::class.java)//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用
                addAssetPath.invoke(assetManager, path)//反射执行方法
                mOutResource = Resources(assetManager, //参数1,资源管理器
                        mContext.getResources().getDisplayMetrics(), //这个好像是屏幕参数
                        mContext.getResources().getConfiguration())//资源配置//最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件
                LiveDataUtil.getResourcesLiveData().postValue(mOutResource)
    
            } catch (e: Exception) {
                e.printStackTrace()
            }
    
        }
    
        //清楚加载的皮肤,替换为当前apk的resource,这里的context使用Application的
        fun clearSkin(context: Context) {
            mOutResource = context.resources
            mOutPkgName = context.packageName
            LiveDataUtil.getResourcesLiveData().postValue(mOutResource )
        }
    
    
        fun getResId(resName: String, type: String): Int {
            return mOutResource?.getIdentifier(resName, type, mOutPkgName) ?: 0
        }
    
    
        //type 有可能是mipmap
        fun getDrawable(resName: String, type: String = "drawable"): Drawable? {
            val res = getResId(resName, type)
            if (res > 0) {
                return mOutResource?.getDrawable(res);
            }
            return null;
        }
    
    
        fun getColor(resName: String): Int {
            val res = getResId(resName, "color")
            if (res <= 0) {
                return -1
            }
            return mOutResource?.getColor(res) ?: -1
        }
    
        fun getDimen(resName: String, original: Int): Int {
            val res = getResId(resName, "dimen")
            if (res <= 0) {
                return original
            }
            return mOutResource?.getDimensionPixelSize(res) ?: original
        }
    
        fun getString(resName: String): String? {
            val res = getResId(resName, "string")
            if (res <= 0) {
                return null
            }
            return mOutResource?.getString(res)
        }
    }
    
    1. CustomFactory
    import android.content.Context
    import android.content.res.Resources
    import android.graphics.drawable.ColorDrawable
    import android.graphics.drawable.Drawable
    import android.support.v7.app.AppCompatDelegate
    import android.text.TextUtils
    import android.util.AttributeSet
    import android.view.LayoutInflater
    import android.view.View
    import android.widget.ImageView
    import android.widget.TextView
    import java.util.*
    
    class CustomFactory(var delegate: AppCompatDelegate) : LayoutInflater.Factory2 {
        private var mOutResource: Resources? = null// TODO: 资源管理器
        fun resourceChange(resources: Resources?) {
            mOutResource = resources
            loadSkin()
        }
    
        private var inflater: LayoutInflater? = null
        private var startContent = false;//我们的view都是在系统id为android:id/content的控件里的,所以在这之后才处理。
        override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
            if (parent != null && parent.id == android.R.id.content) {
                startContent = true;
            }
            var view = delegate.createView(parent, name, context, attrs);
            if (!startContent) {
                return view
            }
            if (view == null) {
                //目前测试两种情况为空:
                // 1.自定义的view,系统的或者自己写的,看xml里,带包名的控件
                //2. 容器类组件,继承ViewGroup的,比如LinearLayout,RadioGroup,ScrollView,WebView
    
                //不为空的,就是系统那些基本控件,
                if (inflater == null) {
                    inflater = LayoutInflater.from(context)
                }
                val index = name.indexOf(".")
                var prefix = ""
                if (index == -1) {
                    if (TextUtils.equals("WebView", name)) {
                        prefix = "android.webkit."
                    } else {
                        prefix = "android.widget."
                    }
                }
                try {
                    view = inflater!!.createView(name, prefix, attrs)
                } catch (e: Exception) {
                    //api26以下createView方法有bug,里边用到了一个context是空的,所以这里进行异常处理,通过反射,重新设置context
                    try {
                        reflect(context, attrs)
                        view = inflater!!.createView(name, prefix, attrs)
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
            if (view != null && !TextUtils.equals("fragment", name)) {
                val map = hashMapOf<String, String>()
                repeat(attrs.attributeCount) {
                    val name = attrs.getAttributeName(it)
                    val value = attrs.getAttributeValue(it)
    //                println("attrs===========$name==${value}")
                    if (value.startsWith("@")) {//我们只处理@开头的资源文件
                        map.put(name, value)
                    }
                    mOutResource?.apply {
                        //切换皮肤以后,部分ui才开始加载,这时候就要用新的resource来加载了
                        handleKeyValue(view, name, value)
                    }
                }
                views.put(view, map)
            }
    
            println("$name==========$view")
            return view;
        }
    
        private fun reflect(mContext: Context, attrs: AttributeSet) {
            try {
                var filed = LayoutInflater::class.java.getDeclaredField("mConstructorArgs")
                filed.isAccessible = true;
                filed.set(inflater, arrayOf(mContext, attrs))
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    
        override fun onCreateView(name: String?, context: Context?, attrs: AttributeSet?): View? {
            return null
        }
    
    
        val views = hashMapOf<View, HashMap<String, String>>()
    
    
        private fun handleKeyValue(view: View, key: String, value: String) {
            if (value.startsWith("@")) {
                var valueInt = 0
                try {
                    valueInt = value.substring(1).toInt()
                } catch (e: Exception) {
                    //处理@style/xxxx这种,类型转换就错了,我们也不需要处理这种。
                }
                if (valueInt <= 0) {
                    return
                }
    
                val type = view.resources.getResourceTypeName(valueInt)
                //type:资源类型,也可以说是res下的那些目录表示的,drawable,mipmap,color,layout,string
                val resName = view.resources.getResourceEntryName(valueInt)
                //resName: xxxx.png ,那么那么就是xxxx, string,color,就是资源文件里item里的name
    //            println("key/value===$key / $value=====type;$type====${resName}")
                
                //下边这个处理下background属性,src(ImageView用的),可以是color,也可以是图片drawable或mipmap
                when (type) {
                    "drawable", "mipmap" -> {
                        when (key) {
                            "background" -> {
                                getDrawable(resName, type) {
                                    view.background = it
                                }
                            }
                            "src" -> {
                                if (view is ImageView) {
                                    getDrawable(resName, type) {
                                        view.setImageDrawable(it)
                                    }
                                }
                            }
                        }
                    }
                    "color" -> {
                        when (key) {
                            "background" -> {
                                getColor(resName) {
                                    view.setBackgroundColor(it)
                                }
                            }
                            "src" -> {
                                if (view is ImageView) {
                                    getColor(resName) {
                                        view.setImageDrawable(ColorDrawable(it))
                                    }
                                }
                            }
                        }
                    }
                }
                //处理下TextView的字体颜色,大小,文字内容,有啥别的可以继续添加
                if (view is TextView) {
                    when (key) {
                        "textColor" -> {
                            getColor(resName) {
                                view.setTextColor(it)
                            }
                        }
                        "textSize" -> {
                            getDimen(resName, view.resources.getDimensionPixelSize(valueInt)) {
                                view.setTextSize(it.toFloat())
                            }
                        }
                        "text" -> {
                            getString(resName) {
                                view.text = it
                            }
                        }
                    }
                }
                //下边这2个,二选一即可,一个回调,一个空的方法,用来处理自己app里自定义view,
                //使用回调就不需要重写这个类了,不用回调那就重写这个类处理handleCustomView方法
                customHandleCallback?.invoke(view, key, valueInt, type, resName)
                handleCustomView(view, key, valueInt, type, resName)
            }
        }
    
        var customHandleCallback: ((view: View, key: String, valueInt: Int, type: String, resName: String) -> Unit)? = null
        open fun handleCustomView(view: View, key: String, valueInt: Int, type: String, resName: String) {
            
            //这个是app里自定义的类,简单处理下。
    //        if (view is TextViewWithMark) {
    //            if (TextUtils.equals("sage_mark_bg_color", key)) {
    //                getColor(resName) {
    //                    view.markBgColor = it
    //                }
    //            }
    //            if (TextUtils.equals("sage_mark_content", key)) {
    //                getString(resName) {
    //                    view.markContent = it
    //                }
    //            }
    //        }
        }
    
        fun getDrawable(resName: String, type: String = "drawable", action: (Drawable) -> Unit) {
            val drawable = SkinLoadUtil.instance.getDrawable(resName, type)
            drawable?.apply {
                action(this)
            }
        }
    
        fun getColor(resName: String, action: (Int) -> Unit) {
            val c = SkinLoadUtil.instance.getColor(resName)
            if (c != -1) {
                action(c)
            }
        }
    
        fun getDimen(resName: String, original: Int, action: (Int) -> Unit) {
            val size = SkinLoadUtil.instance.getDimen(resName, original)
            action(size)
        }
    
        fun getString(resName: String, action: (String) -> Unit) {
            val str = SkinLoadUtil.instance.getString(resName)
            str?.apply {
                action(this)
            }
        }
    
        fun loadSkin() {
            println("loadSkin===========${views.size}")
            views.keys.forEach {
                val map = views.get(it) ?: return
                val view = it;
                map.keys.forEach {
                    val value = map.get(it)
                    println("loadSin:$view==========$it==$value")
                    handleKeyValue(view, it, value!!)
                }
            }
        }
    
    
    }
    
    1. 使用
      Application的onCreate方法里添加如下代码,初始化context
    SkinLoadUtil.instance.init(this)
    
    

    然后在activity的基类里添加如下的代码

        open var registerSkin=true// 决定页面是否支持换肤
         var customFactory:CustomFactory?=null//,如果你要继承这个类重写代码的话,那这里改成子类名字即可
        override fun onCreate(savedInstanceState: Bundle?) {
            if(registerSkin){
                customFactory= CustomFactory(delegate).apply {
                    resourceChange(SkinLoadUtil.instance.getResources())
    //                customHandleCallback={view, key, valueInt, type, resName ->
    //回调处理自定义的view,
    //                }
                }
                LayoutInflaterCompat.setFactory2(layoutInflater,customFactory!!)
                LiveDataUtil.observerResourceChange(this, Observer {
                    customFactory?.resourceChange(it)
                })
            }
            super.onCreate(savedInstanceState)
        }
    

    下边是点击换肤按钮的操作
    主要就是获取到apk在sdcard的路径,传进来即可,我这里放在根目录了,实际中随意调整。
    这种好处是皮肤可以随时从服务器下载下来用。

            btn_skin1.setOnClickListener {
                SkinLoadUtil.instance.load(File(Environment.getExternalStorageDirectory(),"skin1.apk").absolutePath)
            }
    
            btn_skin2.setOnClickListener {
                SkinLoadUtil.instance.load(File(Environment.getExternalStorageDirectory(),"skin2.apk").absolutePath)
            }
            btn_clear.setOnClickListener {
             //还原为默认的皮肤,清除已加载的皮肤
                SkinLoadUtil.instance.clearSkin(activity!!.applicationContext)
            }
    
    1. 新建个工程
      把不需要的目录啥都删了,就留下res下的即可
      然后就是添加和宿主app要换的资源,
      比如图片,就弄个同名的放在对应目录下
      比如下边这里要改的,修改为新的值就行了
    <string name="skin1_show">修改后的</string>
    <color name="item_index_text_color">#0000ff</color>
    <dimen name="item_index_title_size">14sp</dimen>
    

    记得把工程style.xml下默认添加的主题都删了,这样build.gradle下关联的库就可以删光了。打包出来的apk就只有资源文件的大小了。
    然后点击makeProject


    image.png

    然后在下图位置就能拿到apk拉,当然了你要带签名打包apk也随意。

    image.png

    相关文章

      网友评论

        本文标题:android换肤整理

        本文链接:https://www.haomeiwen.com/subject/nruxpqtx.html