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

为什么会有换肤的需求

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

所以你无需手动配置<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: 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属性等。

开源项目传送门

如果你要深入理解完整的换肤流程,请阅读skins的源代码,[https://github.com/dora4/dview-skins] 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/18508.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android Surface对应的Buffer怎么传递给HWC

Android Surface对应的Buffer怎么传递给HWC 引言 因为要预研Android Video overlay&#xff0c;需要将SurfaceView对应的GraphicBuffer从drm_hwcomposer中剥离出来&#xff0c;这就需要们了解SurfaceView对应的GraphicBuffer的前世今生&#xff0c;以及它的数据流向以及在各个…

轻兔推荐 —— vfox

简介 vfox 是一个跨平台且可扩展的版本管理工具&#xff0c;终于有一个可以管理所有运行环境的工具了 - 支持一键安装 Java、Node.js、Flutter、.Net、Golang、PHP、Python等多种环境 - 支持一键切换不同版本 特点 支持Windows(非WSL)、Linux、macOS! 支持不同项目不同版本、…

(四)事件系统

视频链接:尚硅谷2024最新版微信小程序 文章目录 事件绑定和事件对象事件分类以及阻止事件冒泡事件传参-data-*自定义数据事件传参-mark 自定义数据事件绑定和事件对象 小程序中绑定事件与在网页开发中绑定事件几乎一致,只不过在小程序不能通过 on 的方式绑定事件,也没有 cli…

C# 9.0的init访问器

不控制可变性 下面是我们最常见的属性声明方式&#xff0c;允许属性在类的内部和外部都可以读取和修改 public int Id { get; set; }namespace Demo {public class Company{public int Id { get; set; }public Company(){}public Company(int id){Id id; // 可以在构造函数中…

22.Volatile原理

文章目录 Volatile原理1.Volatile语义中的内存屏障1.1.volatile写操作的内存屏障1.1.1.StoreStore 屏障1.1.2.StoreLoad 屏障 1.2.volatile读操作的内存屏障1.2.1.LoadStore屏障1.2.2.LoadLoad屏障 2.volatile不具备原子性2.1.原理 Volatile原理 1.Volatile语义中的内存屏障 在…

用于生成 Avatar 的文本引导式情感和运动控制-InstructAvatar

网址 https://wangyuchi369.github.io/InstructAvatar/ 用于生成 Avatar 的文本引导式情感和运动控制 官网翻译 最近的会说话的头像生成模型在实现与音频的真实和准确的嘴唇同步方面取得了长足的进步&#xff0c;但在控制和传达头像的详细表情和情感方面往往存在不足&#…

APM2.8如何做加速度校准

加速度的校准建议准备一个六面平整&#xff0c;边角整齐的方形硬纸盒或者塑料盒&#xff0c;如下图所示&#xff0c;我们将以它作为APM校准时的水平垂直姿态参考&#xff0c;另外当然还需要一块水平的桌面或者地面 首先用双面泡沫胶或者螺丝将APM主板正面向上固定于方形盒子上&…

JavaScrip原型对象

参考 JavaScrip原型对象 | LogDicthttps://www.logdict.com/archives/javascripyuan-xing-mo-shi

每天写两道(二)LRU缓存、

146.LRU 缓存 . - 力扣&#xff08;LeetCode&#xff09; 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存…

如何使用Python和大模型进行数据分析和文本生成

如何使用Python和大模型进行数据分析和文本生成 Python语言以其简洁和强大的特性&#xff0c;成为了数据科学、机器学习和人工智能开发的首选语言之一。随着大模型&#xff08;Large Language Models, LLMs&#xff09;如GPT-4的崛起&#xff0c;我们能够利用这些模型实现诸多…

Revit——(2)模型的编辑、轴网和标高

目录 一、关闭缩小的隐藏窗口 二、标高&#xff08;可创建平面&#xff0c;其他标高线复制即可&#xff09; 三、轴网 周围的四个圈和三角表示四个里面&#xff0c;可以移动&#xff0c;不要删除 一、关闭缩小的隐藏窗口 二、标高&#xff08;可创建平面&#xff0c;其他标…

计算机体系结构期末快速复习

文章目录 前言CPI&#xff0c;MIPS&#xff08;大题1&#xff09;加速比&#xff08;大题2&#xff09;流水线&#xff08;大题3&#xff09;CRAY-1向量机&#xff08;大题4&#xff09;Tomasulo算法&#xff08;大题5&#xff09;概念简答题计算机系统结构的经典定义什么是透明…

深入分析 Android Activity (二)

文章目录 深入分析 Android Activity (二)1. Activity 的启动模式&#xff08;Launch Modes&#xff09;1.1 标准模式&#xff08;standard&#xff09;1.2 单顶模式&#xff08;singleTop&#xff09;1.3 单任务模式&#xff08;singleTask&#xff09;1.4 单实例模式&#xf…

利用边缘计算网关的工业设备数据采集方案探讨-天拓四方

随着工业4.0时代的到来&#xff0c;工业设备数据采集成为了实现智能制造、提升生产效率的关键环节。传统的数据采集方案往往依赖于中心化的数据处理方式&#xff0c;但这种方式在面对海量数据、实时性要求高的工业场景时&#xff0c;往往显得力不从心。因此&#xff0c;利用边缘…

CSS实现一个雨滴滑落效果

使用纯CSS来实现一个真实的雨滴滑落效果可能会有些挑战&#xff0c;因为CSS主要关注于静态样式和简单的动画效果。然而&#xff0c;你可以使用CSS动画和keyframes来模拟一个雨滴滑落的简化效果。 以下是一个基本的示例&#xff0c;展示如何使用CSS来模拟雨滴从顶部滑落到底部的…

AI学习指南数学工具篇-MATLAB中的凸优化工具

AI学习指南数学工具篇-MATLAB中的凸优化工具 在人工智能领域&#xff0c;凸优化是一个非常重要的数学工具&#xff0c;它在机器学习、深度学习、数据分析等领域都有着广泛的应用。而MATLAB作为一款强大的数学工具软件&#xff0c;提供了丰富的凸优化工具和函数&#xff0c;为用…

二叉树的链式结构(二叉树)与顺序结构(堆)---数据结构

一、树的概念与结构 1、树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。我们常把它叫做树&#xff0c;是因为它看起来像一棵倒挂的树&#xff0c;它的根是朝上的&#xff0c;而叶是朝下的。 下面…

给我一个用断言结果执行下一步的例子

在使用 pytest 和 Selenium 进行自动化测试时&#xff0c;通常我们会根据断言的结果来决定测试流程的走向。如果断言失败&#xff0c;测试通常会停止执行后续的步骤&#xff0c;因为失败意味着被测系统没有按照预期工作。然而&#xff0c;有时候我们可能需要在断言失败后执行特…

每日复盘-20240528

今日重点关注&#xff1a; 20240528 六日涨幅最大: ------1--------300956--------- 英力股份 五日涨幅最大: ------1--------301361--------- 众智科技 四日涨幅最大: ------1--------301361--------- 众智科技 三日涨幅最大: ------1--------301361--------- 众智科技 二日涨…

前端编程语言——JS背景知识、JS基础语法、算数运算符和关系运算符(1)

0、前言&#xff1a; JS全称是JavaScript&#xff0c;是一种脚本语言&#xff0c;诞生于1995年&#xff0c;JS是由ECMAScript&#xff08;包含js语法&#xff09;、BOM&#xff08;Brower Oject Model&#xff0c;和浏览器相关操作&#xff09;、DOM&#xff08;Document Obje…