为什么会有换肤的需求

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 name="skin_theme_color">@color/cyan</color>
<color name="skin_theme_color_red">#d23c3e</color>
<color name="skin_theme_color_orange">#ff8400</color>
<color name="skin_theme_color_black">#161616</color>
<color name="skin_theme_color_green">#009944</color>
<color name="skin_theme_color_blue">#0284e9</color>
<color name="skin_theme_color_cyan">@color/cyan</color>
<color name="skin_theme_color_purple">#8c00d6</color>

将一切需求换肤的色彩,增加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<T : ViewDataBinding> : BaseActivity<T>(),
    ISkinChangeListener, LayoutInflaterFactory {
    private val constructorArgs = arrayOfNulls<Any>(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<SkinAttr>) {
        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<String, Constructor<out View>> = 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) {
    }
}

所以你无需手动装备<meta-data android:name="dora.lifecycle.config.SkinsGlobalConfig" android:value="GlobalConfig"/>,Skins现已自动帮你装备好了。那么我顺便问个问题,BaseSkinActivity中最要害的一行代码是哪行?LayoutInflaterCompat.setFactory(layoutInflater, this)这行代码是整个换肤流程最要害的一行代码。咱们来干涉一下一切Activity onCreateView时的布局加载过程。咱们在SkinAttrSupport.getSkinAttrs中自己解析了AttributeSet。

    /**
     * 从xml的特点集合中获取皮肤相关的特点。
     */
    fun getSkinAttrs(attrs: AttributeSet, context: Context): MutableList<SkinAttr> {
        val skinAttrs: MutableList<SkinAttr> = 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特点等。

开源项目传送门

如果你要深化了解完整的换肤流程,请阅览skins的源代码,[github.com/dora4/dview…] 。