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

news/2024/12/2 22:30:08/

作者: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 doraimplementation("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) ?: returnimageView.setImageDrawable(drawable)
}override fun setBackgroundDrawable(view: View, resName: String) {val drawable = getDrawable(resName) ?: returnview.background = drawable
}override fun setBackgroundColor(view: View, resName: String) {val color = getColor(resName)view.setBackgroundColor(color)
}

框架原理解析

先看BaseSkinActivity的源码。

package dora.skin.baseimport 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 = viewNameif (name == "view") {name = attrs.getAttributeValue(null, "class")}return try {constructorArgs[0] = contextconstructorArgs[1] = attrsif (-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 LayoutInflaternull} finally {// Don't retain references on context.constructorArgs[0] = nullconstructorArgs[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 itval clazz = context.classLoader.loadClass(if (prefix != null) prefix + name else name).asSubclass(View::class.java)constructor = clazz.getConstructor(*constructorSignature)constructorMap[name] = constructor}constructor!!.isAccessible = trueconstructor.newInstance(*constructorArgs)} catch (e: Exception) {// We do not want to catch these, lets return null and let the actual LayoutInflaternull}}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? = nullval 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.applicationimport android.app.Application
import android.content.Context
import dora.skin.SkinManagerclass 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<SkinAttr> {val skinAttrs: MutableList<SkinAttr> = ArrayList()var skinAttr: SkinAttrfor (i in 0 until attrs.attributeCount) {val attrName = attrs.getAttributeName(i)val attrValue = attrs.getAttributeValue(i)val attrType = getSupportAttrType(attrName) ?: continueif (attrValue.startsWith("@")) {val ref = attrValue.substring(1)if (TextUtils.isEqualTo(ref, "null")) {// 跳过@nullcontinue}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.attrimport android.view.View
import android.widget.ImageView
import android.widget.TextView
import dora.skin.SkinLoader
import dora.skin.SkinManagerenum 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) ?: returnview.setImageDrawable(drawable)}}};abstract fun apply(view: View, resName: String)/*** 获取资源管理器。*/val loader: SkinLoaderget() = 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


http://www.ppmy.cn/news/976641.html

相关文章

Socket基本原理详解

socket的概念 故事要从一个插头说起。 插头与插座 当我将插头插入插座&#xff0c;那看起来就像是将两者连起来了。 风扇与电力系统建立"连接" 而插座的英文&#xff0c;又叫socket。 巧了&#xff0c;我们程序员搞网络编程时也会用到一个叫socket的东西。 其实两者…

SQL 简介

SQL 简介 简介 SQL&#xff08;Structured Query Language&#xff0c;结构化查询语言&#xff09;是一种用于管理和操作关系型数据库的标准化语言。它允许用户通过使用各种指令来创建、修改和查询数据库中的数据。 SQL具有几个主要组成部分&#xff1a; 数据定义语言&#…

从零实现深度学习框架——RNN实现支持PackedSequence

引言 本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。 💡系列文章完整目录: 👉点此👈 要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽…

超详细,自动化测试allure测试报告实战(总结)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 allure可以输出非…

Linux系统进程概念详解

这里写目录标题 冯诺依曼体系结构操作系统(Operator System)1.概念2.目的3.管理4.系统调用和库函数概念 进程1.概念2.描述进程-PCB3.查看进程4.通过系统调用获取进程标示符5.通过系统调用创建进程-fork 进程状态1.Linux内核源代码2.进程状态查看 进程优先级1.基本概念2.查看系统…

Jmeter接口/性能测试,Jmeter使用教程(超细整理)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、线程组 线程组…

京东技术专家首推:Spring 微服务架构设计,GitHub 星标 128K

前言 本书提供了实现大型响应式微服务的实用方法和指导原则&#xff0c;并通过示例全面 讲解如何构建微服务。本书深入介绍了 Spring Boot、Spring Cloud、 Docker、Mesos 和 Marathon&#xff0c;还会教授如何用 Spring Boot 部署自治服务&#xff0c;而 无须使用重量级应用服…

吉林大学计算机软件考研经验贴

文章目录 简介政治英语数学专业课 简介 本人23考研&#xff0c;一战上岸吉林大学软件工程专硕&#xff0c;政治72分&#xff0c;英一71分&#xff0c;数二144分&#xff0c;专业课967综合146分&#xff0c;总分433分&#xff0c;上图&#xff1a; 如果学弟学妹需要专业课资料…