A magical framework - Skins skinning framework

Author: dora

Why there is a need for a skin change

App skinning can reduce the aesthetic fatigue of app users. No matter how good the UI design is, if it remains unchanged, the user experience will be greatly reduced. Even if you don’t say it on the surface, you will feel more or less uncomfortable in your heart. Therefore, the interface of the app should be properly revised, otherwise it will be uncomfortable for users, especially the UI design is relatively ugly.

what is skinning

Skinning is the process of switching the app's background color, text color, and resource images with one click. This includes image resources and color resources.

How to use Skins

Skins is a framework that addresses such a need for skinning.

// 添加以下代码到项目根目录下的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'
}

I take changing the skin color as an example, open 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>

Add the skin_ prefix and _skinname suffix to all the colors that need to be changed. The default skin is the one without the suffix.
Then apply the preset skin type on the startup page. Use the resource name of the default skin in the layout file, like here is R.color.skin_theme_color, the framework will automatically replace it for you. If you want the framework to automatically replace it for you, you need to make all the activities to be skinned inherit from 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")
        }
    }
}

In addition, there is another case where skinning is used in the code, which is different from the definition in the layout file.

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

What this skinThemeColor gets is the real skin_theme_color color under the current skin, such as the color value "#ff8400" of R.color.skin_theme_color_orange or the color value "#0284e9" of R.id.skin_theme_color_blue.
SkinLoader also provides a more concise way to set the View color.

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)
}

Framework principle analysis

First look at the source code of 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)
    }
}

We can see that BaseSkinActivity inherits from dora.BaseActivity, so the dora framework must be relied on. Some people say, then I don't use the functions of the dora framework, can I not rely on the dora framework? My answer is, it is not recommended. Skins adopts the feature of Dora lifecycle injection, which is dependency as configuration.

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) {
    }
}

So you don't need to configure it manually, Skins has automatically configured it for you. So let me ask a question by the way, which is the most critical line of code in BaseSkinActivity? The line of code LayoutInflaterCompat.setFactory(layoutInflater, this) is the most critical line of code in the entire skinning process. Let's intervene in the layout loading process of all Activity onCreateView. We parsed the AttributeSet ourselves in SkinAttrSupport.getSkinAttrs.

    /**
     * 从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
    }

We only intervene in the loading process of resources starting with skin_, so the attributes we need are parsed, and finally the list of SkinAttr is returned.

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()
}

The current skins framework only defines several main skinning properties. After you understand the principle, you can also expand it yourself, such as the button property of RadioButton.

Android study notes

Android Performance Optimization: https://qr18.cn/FVlo89
Android Vehicle: https://qr18.cn/F05ZCM
Android Reverse Security Study Notes: https://qr18.cn/CQ5TcL
Android Framework Principles: https://qr18.cn/AQpN4J
Android Audio and Video: https://qr18.cn/Ei3VPD
Jetpack (including Compose): https://qr18.cn/A0gajp
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
OkHttp Source Code Analysis Notes: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Knowledge Body: https://qr18.cn/CyxarU
Android Core Notes: https://qr21.cn/CaZQLo
Android Past Interview Questions: https://qr18.cn/CKV8OZ
2023 Latest Android Interview Question Collection: https://qr18.cn/CgxrRy
Android Vehicle Development Job Interview Exercises: https://qr18.cn/FTlyCJ
Audio and Video Interview Questions:https://qr18.cn/AcV6Ap

Guess you like

Origin blog.csdn.net/weixin_61845324/article/details/131917624