一个神奇的框架——Skins换肤框架

作者:dora

为什么会有换肤的需求

app的换肤,可以降低app用户的审美疲劳。再好的UI设计,一直不变的话,也会对用户体验大打折扣,即使表面上不说,但心里或多或少会有些难受。所以app的界面要适当的改版啊,要不然可难受死用户了,特别是UI设计还相对较丑的。

换肤是什么

换肤是将app的背景色、文字颜色以及资源图片,一键进行全部切换的过程。这里就包括了图片资源和颜色资源。

Skins怎么使用

Skins就是一个解决这样一种换肤需求的框架。

// 添加以下代码到项目根目录下的build.gradle
allprojects {
    repositories {
        maven { url "https://jitpack.io" }
    }
}
// 添加以下代码到app模块的build.gradle
dependencies {
    // skins依赖了dora框架,所以你也要implementation dora
    implementation("com.github.dora4:dora:1.1.12")
    implementation 'com.github.dora4:dview-skins:1.4'
}

我以更换皮肤颜色为例,打开res/colors.xml。


@color/cyan
#d23c3e
#ff8400
#161616
#009944
#0284e9
@color/cyan
#8c00d6

将所有需要换肤的颜色,添加skin_前缀和_skinname后缀,不加后缀的就是默认皮肤。
然后在启动页应用预设的皮肤类型。在布局layout文件中使用默认皮肤的资源名称,像这里就是R.color.skin_theme_color,框架会自动帮你替换。要想让框架自动帮你替换,你需要让所有要换肤的Activity继承BaseSkinActivity。

private fun applySkin() {
    val manager = PreferencesManager(this)
    when (manager.getSkinType()) {
        0 -> {
        }
        1 -> {
            SkinManager.changeSkin("cyan")
        }
        2 -> {
            SkinManager.changeSkin("orange")
        }
        3 -> {
            SkinManager.changeSkin("black")
        }
        4 -> {
            SkinManager.changeSkin("green")
        }
        5 -> {
            SkinManager.changeSkin("red")
        }
        6 -> {
            SkinManager.changeSkin("blue")
        }
        7 -> {
            SkinManager.changeSkin("purple")
        }
    }
}

另外还有一个情况是在代码中使用换肤,那么跟布局文件中定义是有一些区别的。

val skinThemeColor = SkinManager.getLoader().getColor("skin_theme_color")

这个skinThemeColor拿到的就是当前皮肤下的真正的skin_theme_color颜色,比如R.color.skin_theme_color_orange的颜色值“#ff8400”或R.id.skin_theme_color_blue的颜色值“#0284e9”。
SkinLoader还提供了更简洁设置View颜色的方法。

override fun setImageDrawable(imageView: ImageView, resName: String) {
    val drawable = getDrawable(resName) ?: return
    imageView.setImageDrawable(drawable)
}

override fun setBackgroundDrawable(view: View, resName: String) {
    val drawable = getDrawable(resName) ?: return
    view.background = drawable
}

override fun setBackgroundColor(view: View, resName: String) {
    val color = getColor(resName)
    view.setBackgroundColor(color)
}

框架原理解析

先看BaseSkinActivity的源码。

package dora.skin.base

import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.collection.ArrayMap
import androidx.core.view.LayoutInflaterCompat
import androidx.core.view.LayoutInflaterFactory
import androidx.databinding.ViewDataBinding
import dora.BaseActivity
import dora.skin.SkinManager
import dora.skin.attr.SkinAttr
import dora.skin.attr.SkinAttrSupport
import dora.skin.attr.SkinView
import dora.skin.listener.ISkinChangeListener
import dora.util.LogUtils
import dora.util.ReflectionUtils
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*

abstract class BaseSkinActivity : BaseActivity(),
    ISkinChangeListener, LayoutInflaterFactory {

    private val constructorArgs = arrayOfNulls(2)

    override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
        if (createViewMethod == null) {
            val methodOnCreateView = ReflectionUtils.findMethod(delegate.javaClass, false,
                "createView", *createViewSignature)
            createViewMethod = methodOnCreateView
        }
        var view: View? = ReflectionUtils.invokeMethod(delegate, createViewMethod, parent, name,
            context, attrs) as View?
        if (view == null) {
            view = createViewFromTag(context, name, attrs)
        }
        val skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context)
        if (skinAttrList.isEmpty()) {
            return view
        }
        injectSkin(view, skinAttrList)
        return view
    }

    private fun injectSkin(view: View?, skinAttrList: MutableList) {
        if (skinAttrList.isNotEmpty()) {
            var skinViews = SkinManager.getSkinViews(this)
            if (skinViews == null) {
                skinViews = arrayListOf()
            }
            skinViews.add(SkinView(view, skinAttrList))
            SkinManager.addSkinView(this, skinViews)
            if (SkinManager.needChangeSkin()) {
                SkinManager.apply(this)
            }
        }
    }

    private fun createViewFromTag(context: Context, viewName: String, attrs: AttributeSet): View? {
        var name = viewName
        if (name == "view") {
            name = attrs.getAttributeValue(null, "class")
        }
        return try {
            constructorArgs[0] = context
            constructorArgs[1] = attrs
            if (-1 == name.indexOf('.')) {
                // try the android.widget prefix first...
                createView(context, name, "android.widget.")
            } else {
                createView(context, name, null)
            }
        } catch (e: Exception) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            null
        } finally {
            // Don't retain references on context.
            constructorArgs[0] = null
            constructorArgs[1] = null
        }
    }

    @Throws(InflateException::class)
    private fun createView(context: Context, name: String, prefix: String?): View? {
        var constructor = constructorMap[name]
        return try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                val clazz = context.classLoader.loadClass(
                        if (prefix != null) prefix + name else name).asSubclass(View::class.java)
                constructor = clazz.getConstructor(*constructorSignature)
                constructorMap[name] = constructor
            }
            constructor!!.isAccessible = true
            constructor.newInstance(*constructorArgs)
        } catch (e: Exception) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        val layoutInflater = LayoutInflater.from(this)
        LayoutInflaterCompat.setFactory(layoutInflater, this)
        super.onCreate(savedInstanceState)
        SkinManager.addListener(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        SkinManager.removeListener(this)
    }

    override fun onSkinChanged(suffix: String) {
        SkinManager.apply(this)
    }

    companion object {
        val constructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)
        private val constructorMap: MutableMap> = ArrayMap()
        private var createViewMethod: Method? = null
        val createViewSignature = arrayOf(View::class.java, String::class.java,
                Context::class.java, AttributeSet::class.java)
    }
}

我们可以看到BaseSkinActivity继承自dora.BaseActivity,所以dora框架是必须要依赖的。有人说,那我不用dora框架的功能,可不可以不依赖dora框架?我的回答是,不建议。Skins对Dora生命周期注入特性采用的是,依赖即配置。

package dora.lifecycle.application

import android.app.Application
import android.content.Context
import dora.skin.SkinManager

class SkinsAppLifecycle : ApplicationLifecycleCallbacks {

    override fun attachBaseContext(base: Context) {
    }

    override fun onCreate(application: Application) {
        SkinManager.init(application)
    }

    override fun onTerminate(application: Application) {
    }
}

所以你无需手动配置,Skins已经自动帮你配置好了。那么我顺便问个问题,BaseSkinActivity中最关键的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最关键的一行代码。我们来干预一下所有Activity onCreateView时的布局加载过程。我们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。

    /**
     * 从xml的属性集合中获取皮肤相关的属性。
     */
    fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList {
        val skinAttrs: MutableList = ArrayList()
        var skinAttr: SkinAttr
        for (i in 0 until attrs.attributeCount) {
            val attrName = attrs.getAttributeName(i)
            val attrValue = attrs.getAttributeValue(i)
            val attrType = getSupportAttrType(attrName) ?: continue
            if (attrValue.startsWith("@")) {
                val ref = attrValue.substring(1)
                if (TextUtils.isEqualTo(ref, "null")) {
                    // 跳过@null
                    continue
                }
                val id = ref.toInt()
                // 获取资源id的实体名称
                val entryName = context.resources.getResourceEntryName(id)
                if (entryName.startsWith(SkinConfig.ATTR_PREFIX)) {
                    skinAttr = SkinAttr(attrType, entryName)
                    skinAttrs.add(skinAttr)
                }
            }
        }
        return skinAttrs
    }

我们只干预skin_开头的资源的加载过程,所以解析得到我们需要的属性,最后得到SkinAttr的列表返回。

package dora.skin.attr

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManager

enum class SkinAttrType(var attrType: String) {

    /**
     * 背景属性。
     */
    BACKGROUND("background") {
        override fun apply(view: View, resName: String) {
            val drawable = loader.getDrawable(resName)
            if (drawable != null) {
                view.setBackgroundDrawable(drawable)
            } else {
                val color = loader.getColor(resName)
                view.setBackgroundColor(color)
            }
        }
    },

    /**
     * 字体颜色。
     */
    TEXT_COLOR("textColor") {
        override fun apply(view: View, resName: String) {
            val colorStateList = loader.getColorStateList(resName) ?: return
            (view as TextView).setTextColor(colorStateList)
        }
    },

    /**
     * 图片资源。
     */
    SRC("src") {
        override fun apply(view: View, resName: String) {
            if (view is ImageView) {
                val drawable = loader.getDrawable(resName) ?: return
                view.setImageDrawable(drawable)
            }
        }
    };

    abstract fun apply(view: View, resName: String)

    /**
     * 获取资源管理器。
     */
    val loader: SkinLoader
        get() = SkinManager.getLoader()
}

当前skins框架只定义了几种主要的换肤属性,你理解原理后,也可以自己进行扩展,比如RadioButton的button属性等。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

你可能感兴趣的:(Android,架构,移动开发,android,移动开发,Framework,架构,Skins)