Bootstrap

Android 插件化换肤方案

效果

在这里插入图片描述

实现流程

  1. 实现LayoutInflater.Factory2这个接口,实现onCreateView方法(主要仿照系统原来LayoutInflater.createView()方法的实现),此处可以拿到页面中所有的View,判断有没有需要换肤的View,并且保存下来;
  2. 在Activity创建成功后,setContentView()前把我们自定义的Factory2设置给当前Activity的LayoutInflater,这样LayoutInflater在加载View的时候就会使用我们自定义的Factory2来加载;
  3. 在点击换肤按钮时,使用AssetManager加载皮肤包里面的资源文件得到皮肤包里面的资源;
  4. 循环遍历需要换肤的View,设置需要换肤的属性的值为皮肤包里面的资源,换肤完成;
  1. 此处需要了解Activity的页面View的创建流程,主要就是LayoutInflater里面的 createView()方法;
  2. Android 资源文件的加载,主要就是AssetManager里面的addAssetPath()的方法,APP里面的所有资源文 件都是通过AssetsManager来获取的,Resources类只是包装了一下,最终都是使用AssetsManager来获取的
    对这两块感兴趣的同学,可以自己去了解一下

步骤一:实现LayoutInflater.Factory2接口,保存需要换肤的View

class SkinLayoutInflaterFactory(val activity: Activity) :LayoutInflater.Factory2, Observer {
    //所有View的类包名
    private val mClassPrefixList =
            arrayOf("android.widget.", "android.webkit.", "android.app.", "android.view.")

    //View的两个参数的构造函数格式
    private val mConstructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
    //缓存View的构造函数
    private val mConstructorMap = HashMap<String, Constructor<out View?>>()

    // 当选择新皮肤后需要替换View与之对应的属性
    // 页面属性管理器
    private var skinAttribute: SkinAttribute? = SkinAttribute()

    /**
     * 创建View的方法
     */
    override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
        var view: View? = createSDKView(name, context, attrs)
        if (null == view) {
            view = createView(name, context, attrs)
        }
        //这就是我们加入的逻辑
        if (null != view) {
            //判断这个view需不需要换肤(有没有需要换肤的属性),如果有,则记录下来
            //使用skinAttribute 类来保存相关属性
            skinAttribute!!.look(view, attrs)
        }
        return view
    }

    private fun createSDKView(name: String, context: Context, attrs: AttributeSet): View? {
        //如果包含 . 则不是SDK中的view 可能是自定义view包括support库中的View
        if (-1 != name.indexOf('.')) {
            return null
        }
        //不包含就要在解析的 节点 name前,拼上: android.widget. 等尝试去反射
        for (i in mClassPrefixList.indices) {
            val view = createView(mClassPrefixList.get(i) + name,context, attrs)
            if (view != null) {
                return view
            }
        }
        return null
    }

    /**
     * 根据View的构造方法,反射创建对应的View
     */
    private fun createView(name: String, context: Context, attrs: AttributeSet): View? {
        //反射获取View 两个参数的构造方法
        val constructor = findConstructor(context, name)
        constructor?.let {
            try {
                //反射创建对象
                return it.newInstance(context, attrs)
            } catch (e: Exception) {
            }
        }

        return null
    }


    /**
     * 反射获取View的两个参数的构造方法,并且缓存起来,仿照系统写法
     */
    private fun findConstructor(context: Context, name: String): Constructor<out View?>? {
        var constructor: Constructor<out View?>? = mConstructorMap.get(name)
        if (constructor == null) {
            try {
                val clazz = context.classLoader.loadClass(name).asSubclass(View::class.java)
                constructor = clazz.getConstructor(*mConstructorSignature)
                mConstructorMap.put(name, constructor)
            } catch (e: Exception) {
            }
        }
        return constructor
    }


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

    /**
     * 接收到换肤事件,修改所有view的皮肤
     */
    override fun update(o: Observable?, arg: Any?) {
        //修改状态栏的颜色
        //SkinThemeUtils.updateStatusBarColor(activity)
        //换肤
        skinAttribute!!.applySkin()
    }

    /**
     * 清楚数据
     */
    fun clear(){
        skinAttribute = null
    }

}

这个类实现了Observer 接口,在皮肤需要改变时进行换肤

SkinAttribute类里面保存了需要换肤的相关View和属性:

class SkinAttribute {
    //需要换肤的属性
    private val mAttributes: MutableList<String> = ArrayList()
    init {
        mAttributes.add("background")
        mAttributes.add("src")
        mAttributes.add("textColor")
        mAttributes.add("drawableLeft")
        mAttributes.add("drawableTop")
        mAttributes.add("drawableRight")
        mAttributes.add("drawableBottom")
    }

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


    /**
     * 记录下一个VIEW身上哪几个属性需要换肤
     */ 
    fun look(view: View, attrs: AttributeSet) {
        val mSkinPars: MutableList<SkinPair> = ArrayList()
        for (i in 0 until attrs.attributeCount) {
            //获得属性名  textColor/background
            val attributeName = attrs.getAttributeName(i)
            if (mAttributes.contains(attributeName)) {
                // #
                // ?722727272
                // @722727272
                val attributeValue = attrs.getAttributeValue(i)
                // 比如color 以#开头表示写死的颜色 不可用于换肤
                if (attributeValue.startsWith("#")) {
                    continue
                }
                var resId: Int
                // 以 ?开头的表示使用 属性
                resId = if (attributeValue.startsWith("?")) {
                    val attrId = attributeValue.substring(1).toInt()
                    getResId(view.context, intArrayOf(attrId)).get(0)
                } else {
                    // 正常以 @ 开头
                    attributeValue.substring(1).toInt()
                }
                val skinPair = SkinPair(attributeName, resId)
                mSkinPars.add(skinPair)
            }
        }
        if (!mSkinPars.isEmpty() || view is SkinViewSupport) {
            val skinView = SkinView(view, mSkinPars)
            // 如果选择过皮肤 ,调用 一次 applySkin 加载皮肤的资源
            skinView.applySkin()
            mSkinViews.add(skinView)
        }
    }


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


    /**
     * 获得theme中的属性中定义的 资源id
     * @param context
     * @param attrs
     * @return
     */
    fun getResId(context: Context, attrs: IntArray): IntArray {
        val resIds = IntArray(attrs.size)
        val a = context.obtainStyledAttributes(attrs)
        for (i in attrs.indices) {
            resIds[i] = a.getResourceId(i, 0)
        }
        a.recycle()
        return resIds
    }


}

internal class SkinView(
    var view: View,
    //这个View的能被 换肤的属性与它对应的id 集合
    var skinPairs: List<SkinPair>
                       ) {
    /**
     * 对一个View中的所有的属性进行修改
     * 最终换肤的方法
     */
    fun applySkin() {
        //对实现了SkinViewSupport接口的自定义View进行换肤
        applySkinSupport()
        //对当前View进行换肤
        for (skinPair in skinPairs) {
            var left: Drawable? = null
            var top: Drawable? = null
            var right: Drawable? = null
            var bottom: Drawable? = null
            when (skinPair.attributeName) {
                "background" -> {
                    val background: Any? = SkinResources.getBackground(skinPair.resId)
                    background?.let {
                        //背景可能是 @color 也可能是 @drawable
                        if (it is Int) {
                            view.setBackgroundColor(it)
                        } else {
                            ViewCompat.setBackground(view, it as Drawable)
                        }
                    }
                }
                "src" -> {
                    val background: Any? = SkinResources.getBackground(skinPair.resId)
                    background?.let {
                        if (it is Int) {
                            (view as ImageView).setImageDrawable(
                                ColorDrawable((it as Int?)!!))
                        } else {
                            (view as ImageView).setImageDrawable(it as Drawable?)
                        }
                    }
                }
                "textColor" -> (view as TextView).setTextColor(SkinResources.getColorStateList(skinPair.resId))
                "drawableLeft" -> left = SkinResources.getDrawable(skinPair.resId)
                "drawableTop" -> top = SkinResources.getDrawable(skinPair.resId)
                "drawableRight" -> right = SkinResources.getDrawable(skinPair.resId)
                "drawableBottom" -> bottom = SkinResources.getDrawable(skinPair.resId)
                else -> {
                }
            }
            if (null != left || null != right || null != top || null != bottom) {
                (view as TextView).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)
            }
        }
    }

    /**
     * 对实现了SkinViewSupport接口的自定义View进行换肤
     */
    private fun applySkinSupport() {
        if (view is SkinViewSupport) {
            (view as SkinViewSupport).applySkin()
        }
    }
}

/**
 * 需要换肤的属性名和资源id
 */
internal class SkinPair(
    //属性名
    var attributeName: String,
    //对应的资源id
    var resId: Int
                       )

look() 方法是查找这个View 有没有需要换肤的属性
applySkin() 换肤的方法

SkinViewSupport是一个接口,View实现了这个接口,也会执行换肤,主要用于自定义View的情况

interface SkinViewSupport {
    //换肤方法
    fun applySkin()
}

步骤二:在Activity创建成功后,setContentView()前把我们自定义的Factory2设置给当前Activity的LayoutInflater,这样LayoutInflater在加载View的时候就会使用我们自定义的Factory2来加载

我们需要在setContentView()前设置Factory2,有两种方法:

  1. 使用BaseActvity
  2. 我们可以使用ActivityLifecycleCallbacks这个接口,这个接口会可以监听所有Activity 的 onCreate、onRestart、onResume等生命周期

执行顺序:onCreate方法先与onActivityCreated调用。onCreate方法里super之前的代码先执行,其次执行onActivityCreated的代码,最后执行onCreate方法里super之后的代码。

此处我们采用方案二
新建一个类ApplicationActivityLifecycle,实现ActivityLifecycleCallbacks接口:

class ApplicationActivityLifecycle(val mObserable: Observable) : ActivityLifecycleCallbacks {
    //保存当前Activity 和 LayoutInflaterFactory 的对应关系
    private val mLayoutInflaterFactories = ArrayMap<Activity, SkinLayoutInflaterFactory>()

    /**
     * Activity 创建完成回调
     */
    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        //使用factory2 设置布局加载工程
        val skinLayoutInflaterFactory = SkinLayoutInflaterFactory(activity)

        /**
         * 更新布局视图
         */
        //获得Activity的布局加载器
        val layoutInflater = activity.layoutInflater
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
            try {
                //Android 布局加载器 使用 mFactorySet 标记是否设置过Factory
                //如设置过则会抛出异常
                //设置 mFactorySet 标签为false
                val field = LayoutInflater::class.java.getDeclaredField("mFactorySet")
                field.isAccessible = true
                field.setBoolean(layoutInflater, false)
                LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        } else {
            //安卓9以上版本,mFactorySet 不存在了,所有直接mFactory2设置反射mFactory2
            try {
                val field = LayoutInflater::class.java.getDeclaredField("mFactory2")
                field.isAccessible = true
                field[layoutInflater] = skinLayoutInflaterFactory
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        //保存Activity 和 skinLayoutInflaterFactory的对应关系
        mLayoutInflaterFactories[activity] = skinLayoutInflaterFactory
        //添加观察者
        mObserable.addObserver(skinLayoutInflaterFactory)
    }

    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 observer = mLayoutInflaterFactories.remove(activity)  as SkinLayoutInflaterFactory
        observer.clear()
        SkinManager.deleteObserver(observer)
    }

}

步骤三:在点击换肤按钮时,使用AssetManager加载皮肤包里面的资源文件得到皮肤包里面的资源;

SkinManager 是皮肤管理类,主要用来注册ApplicationActivityLifecycle,加载皮肤等

object SkinManager: Observable() {
    /**
     * Activity生命周期回调
     */
    private var skinActivityLifecycle: ApplicationActivityLifecycle? = null
    private var mContext: Application? = null

    /**
     * 初始化 必须在Application中先进行初始化
     * @param application
     * @param skinPath 默认的皮肤路径
     */
    fun init(application: Application,skinPath:String?) {
        mContext = application
        //资源管理类 用于从 app/皮肤 中加载资源
        SkinResources.init(application.applicationContext)
        //注册Activity生命周期,并设置被观察者
        skinActivityLifecycle = ApplicationActivityLifecycle(this)
        application.registerActivityLifecycleCallbacks(skinActivityLifecycle)
        //加载上次使用保存的皮肤
        loadSkin(skinPath)
    }


    /**
     * 记载皮肤并应用
     *
     * @param skinPath 皮肤路径 如果为空则使用默认皮肤
     */
    fun loadSkin(skinPath: String?) {
        if (TextUtils.isEmpty(skinPath)) {
            //还原默认皮肤
            SkinResources.reset()
        } else {
            try {
                //宿主app的 resources;
                val appResource = mContext!!.resources
                //
                //反射创建AssetManager 与 Resource
                val assetManager = AssetManager::class.java.newInstance()
                //资源路径设置 目录或压缩包
                val addAssetPath =
                        assetManager.javaClass.getMethod("addAssetPath", String::class.java)
                addAssetPath.invoke(assetManager, skinPath)

                //根据当前的设备显示器信息 与 配置(横竖屏、语言等) 创建Resources
                val skinResource = Resources(assetManager, appResource.displayMetrics,
                                             appResource.configuration)
                //获取外部Apk(皮肤包) 包名
                val mPm = mContext!!.packageManager
                val info = mPm.getPackageArchiveInfo(skinPath!!, PackageManager.GET_ACTIVITIES)
                val packageName = info!!.packageName
                SkinResources.applySkin(skinResource, packageName)
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
        //通知采集的View 更新皮肤
        //被观察者改变 通知所有观察者
        setChanged()
        notifyObservers(null)
    }
}

最后还有一个皮肤资源管理类,保存当前默认的资源与皮肤的资源,获取皮肤资源里面的颜色、图片资源ID

object SkinResources {
    //皮肤包的包名
    private var mSkinPkgName: String? = null

    //当前是否默认皮肤
    private var isDefaultSkin = true

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

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

    /**
     * 初始化方法
     */
    fun init(context: Context) {
        mAppResources = context.resources
    }

    /**
     * 重置为默认
     */
    fun reset() {
        mSkinResources = null
        mSkinPkgName = ""
        isDefaultSkin = true
    }

    /**
     * 换肤
     * 设置当前皮肤的属性
     */
    fun applySkin(resources: Resources?, pkgName: String?) {
        mSkinResources = resources
        mSkinPkgName = pkgName
        //是否使用默认皮肤
        isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null
    }

    /**
     * 1.通过原始app中的resId(R.color.XX)获取到自己的 名字
     * 2.根据名字和类型获取皮肤包中的ID
     */
    fun getIdentifier(resId: Int): Int {
        if (isDefaultSkin) {
            return resId
        }
        val resName = mAppResources!!.getResourceEntryName(resId)
        val resType = mAppResources!!.getResourceTypeName(resId)
        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 = getIdentifier(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 = getIdentifier(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 = getIdentifier(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)
        }
    }
}

最后我们在Application进行初始化,在Activity里面添加一个按钮,进行换肤:
初始化:

class MyApplication: Application() {
    override fun onCreate() {
        super.onCreate()
        SkinManager.init(this,null)
    }
}

换肤:

fun change(view: View?) {
        //换肤,皮肤包是独立的apk包,可以来自网络下载
        if(isDark){
            SkinManager.loadSkin(null)
        }else{
            SkinManager.loadSkin("/data/data/com.ping.xskin/darktheme-debug.apk")
        }
        isDark = !isDark
         
    }

皮肤包就是一个APK,在里面加上和默认资源相同名字的图片、颜色和其它的资源文件,打包APK后,放到指定的路径使用SkinManager.loadSkin 进行加载就可以了

项目代码:XSink

后续修改:
1.状态栏的适配
2.皮肤包的压缩

;