Kotlin 实战 | 用语法糖干掉形状 xml 文件

简介: 项目中的各种描述形状的 xml 文件多如牛毛。虽然 xml 提供了可视化效果,但不能复用,读取耗时也是它的缺点。用 Kotlin 语法糖包装一下就可以和 xml 说再见。

这是“干掉 xml”系列的第二篇,上一篇干掉的是res/layout目录下的布局文件,这一篇想把res/drawable目录下的形状配置文件也干掉。

Android 中的 xml 资源文件将静态配置和动态代码解耦,便于集中管理。但它会拖累性能,不仅增大包体积,读 xml 也是 I/O 耗时操作。

实战项目中有 650+ 布局文件(2.9 MB),1000+ drawable 非图片文件(700 KB),如果这些文件都能被干掉,也可以为缩包做一点贡献。

“干掉 xml”系列文件目录如下:

  1. Android性能优化 | 把构建布局用时缩短 20 倍(下)
  2. 干掉 xml | 再也不用为各种形状写 xml 了

用 xml 静态配置形状

如下界面需要定义几个<shape>文件?

答案是 6 个。最气人的是,这 6 个文件内容几乎是相同的:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <corners android:radius="15dp" />
    <solid android:color="#FFEBF1" />
</shape>

唯一的区别仅是颜色和圆角不同。<shape>文件的尴尬之处在于它无法被复用。

用代码动态构建形状

<shape>标签对应的 Java 类是GradientDrawable,可以用代码动态构建如图形状:

// 构建GradientDrawable对象并设置属性
val drawable = GradientDrawable().apply {
    shape = GradientDrawable.RECTANGLE // 矩形
    cornerRadius = 10f // 圆角
    colors = intArrayOf(Color.parseColor("#ff00ff"),Color.parseColor("#800000ff")) //渐变色
    gradientType = GradientDrawable.LINEAR_GRADIENT // 渐变类型
    orientation = GradientDrawable.Orientation.LEFT_RIGHT // 渐变方向
    setStroke(2.dp,Color.parseColor("#ffff00")) // 描边宽度和颜色
}
// 将GradientDrawable对象设置为控件背景
findViewById<Text>(R.id.tvTitle)?.background = drawable

借助于apply(),这段代码还比较清晰易懂。如果能有 DSL 的加持,就可以做得更好。

构建布局 DSL

回顾下上一篇中动态构建布局的 DSL:

class MainActivity : AppCompatActivity() {
    // 用 DSL 动态构建布局
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // 为 TextView 设置圆角渐变形状
                background_res = R.drawable.shape
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 将布局设置为 content view
        setContentView(contentView)
    }
}

DSL 把原先定义在 xml 中的布局搬到了 Activity 中,减少了一个 xml 文件,避免了一次读文件的 I/O 操作。

构建布局 DSL 中的每一个被赋值的属性都是一个预定义的扩展属性,比如layout_width

// 为 View 扩展一个 Int 类型的属性
inline var View.layout_width: Int 
    get() {
        return 0
    }
    // 当扩展属性被赋值时,将其转化为 MarginLayoutParams 并设置为控件的布局参数
    set(value) {
        val w = if (value > 0) value.dp else value
        val h = layoutParams?.height ?: 0
        layoutParams = ViewGroup.MarginLayoutParams(w, h)
    }

// 为 Int 值扩展属性,将其转换为 DP 值
val Int.dp: Int
    get() {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            this.toFloat(),
            Resources.getSystem().displayMetrics
        ).toInt()
    }

关于动态构建布局 DSL 的详细讲解可以点击这里

新增形状属性

运用同样的思路为View新增一个“形状属性”,将构建GradientDrawable的细节隐藏(或者说增加构建代码的可读性):

inline var View.shape: GradientDrawable
    get() {
        return GradientDrawable()
    }
    set(value) {
        background = value
    }

View新增shape属性,它是GradientDrawable类型的。

inline fun shape(init: GradientDrawable.() -> Unit) = GradientDrawable().apply(init)

新增一个顶层方法,用于构建GradientDrawable实例。

直接GradientDrawable()不就能构建实例,为啥还要再新增一个方法做同样的事情?

这个方法的魔力在于它接收一个带接收者的lambda作为参数,即GradientDrawable.() -> Unit,这样一来就隐藏了“构建”和“设值”这两个动作的细节,使得可以用声明型的语句来完成构建:

class MainActivity : AppCompatActivity() {
    // 用 DSL 动态构建布局
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // 为 TextView 设置形状
                shape = shape {
                    shape = GradientDrawable.RECTANGLE
                    cornerRadius = 10f
                    colors = intArrayOf(Color.parseColor("#ff00ff"),Color.parseColor("#800000ff")) 
                    gradientType = GradientDrawable.LINEAR_GRADIENT 
                    orientation = GradientDrawable.Orientation.LEFT_RIGHT 
                    setStroke(2.dp,Color.parseColor("#ffff00"))
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 将布局设置为 content view
        setContentView(contentView)
    }
}

将原本通过R.drawable.shpae引用的静态资源转变为动态构建。

上面的代码虽然已经将布局和形状文件都干掉了,但为形状设值的过程还是有点啰嗦,可以通过预定义扩展属性来简化:

// 为 GradientDrawable 扩展圆角半径属性
inline var GradientDrawable.corner_radius: Int
    get() {
        return -1
    }
    set(value) {
        // 将半径值转化成dp值
        cornerRadius = value.dp.toFloat()
    }

// 为 GradientDrawable 扩展渐变色属性
inline var GradientDrawable.gradient_colors: List<String>
    get() {
        return emptyList()
    }
    set(value) {
        // 将 string 转换成颜色 Int 值
        colors = value.map { Color.parseColor(it) }.toIntArray()
    }

// 为 GradientDrawable 扩展描边属性
inline var GradientDrawable.strokeAttr: Stroke?
    get() {
        return null
    }
    set(value) {
        // 将描边数据实体类拆解并传递给 setStroke()
        value?.apply { setStroke(width.dp, Color.parseColor(color), dashWidth.dp, dashGap.dp) }
    }

// 描边数据实体类
data class Stroke(
    var width: Int = 0, 
    var color: String = "#000000", 
    var dashWidth: Float = 0f, 
    var dashGap: Float = 0f
)

// 给常量取一个较短的别名以增加可读性
val shape_rectangle = GradientDrawable.RECTANGLE
val gradient_type_linear = GradientDrawable.LINEAR_GRADIENT
val gradient_left_right = GradientDrawable.Orientation.LEFT_RIGHT

运用这些属性进一步简化构建过程:

class MainActivity : AppCompatActivity() {
    // 用 DSL 动态构建布局
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // 为 TextView 设置形状
                shape = shape {
                    corner_radius = 10
                    shape = shape_rectangle
                    gradientType = gradient_type_linear
                    orientation = gradient_left_right
                    gradient_colors = listOf("#ff00ff", "#800000ff")
                    strokeAttr = Stroke(2, "#ffff00")
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 将布局设置为 content view
        setContentView(contentView)
    }
}

新增颜色状态列表属性

实际业务场景中,按钮的背景颜色通常有好几种状态,最开始我通过这样的代码来控制背景色的变化:

val tv = findViewById<TextView>(R.id.tv)

fun onResponse(success: Boolean){
    if (success) {
        tv.bacground = R.drawable.success
    } else {
        tv.background = R.drawable.fail
    }
}

这样写,语义还是简单明了的,但缺点是 “控件单个属性的控制逻辑散落在各处”

假设分别有 2 个网络请求和 1 个界面按钮可以影响该控件的背景色,则tv.background = R.drawable.xxx会散落在 2 个网络请求和 1 个控件点击事件回调中。

这增加了后期维护的复杂度,因为“该控件在不同条件下会对应呈现什么样的背景色?”这个知识被撕碎了且散落在不同的地方,你得查找出所有的碎片拼凑起来才能知道故事的详情,以便进行修改。

(你一定经历过这样的遭遇,只是单纯地想把某控件的属性值修改一下,但死活不成功,因为其他 n 个地方也在修改它。。。这样的隐藏剧情,让人非常抓狂)

后来我学会了<selecotr>

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/bg_red" android:state_enabled="true" />
    <item android:drawable="@drawable/bg_blue" android:state_enabled="false" />
</selector>

<selector>将不同状态对应的颜色提前配置好并归总在一个文件中,这样改控件背景色变化的逻辑就一目了然,然后只需要在不同的业务场景处改变控件状态即可:

val tv = findViewById<TextView>(R.id.tv)
tv.background = R.drawable.selector

fun onResponse(success: Boolean){
    if (success) {
        tv.enable = true
    } else {
        tv.enbale = false
    }
}

是不是也可以将<selector>配置文件动态构建?

// 为 GradientDrawable 扩展颜色状态列表属性
inline var GradientDrawable.color_state_list: List<Pair<IntArray, String>>
    get() {
        return listOf(intArrayOf() to "#000000")
    }
    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
    set(value) {
        // 状态列表
        val states = mutableListOf<IntArray>()
        // 颜色列表
        val colors = mutableListOf<Int>()
        // 将属性值分别转换成状态列表和颜色列表
        value.forEach { pair ->
            states.add(pair.first)
            colors.add(Color.parseColor(pair.second))
        }
        // 创建 ColorStateList 对象 并调用setColor(ColorStateList colorStateList)
        color = ColorStateList(states.toTypedArray(), colors.toIntArray())
    }
    
// 给常量取一个较短的别名以增加可读性
val state_enable = android.R.attr.state_enabled
val state_disable = -android.R.attr.state_enabled
val state_pressed = android.R.attr.state_pressed
val state_unpressed = -android.R.attr.state_pressed

然后就可以像这样在构建 TextView 的同时为其构建颜色状态列表:

class MainActivity : AppCompatActivity() {
    // 用 DSL 动态构建布局
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // 为 TextView 设置形状
                shape = shape {
                    corner_radius = 10
                    shape = shape_rectangle
                    gradientType = gradient_type_linear
                    orientation = gradient_left_right
                    gradient_colors = listOf("#ff00ff", "#800000ff")
                    strokeAttr = Stroke(2, "#ffff00")
                    // 为形状构建颜色状态列表
                    color_state_list = listOf(
                        // 在 enable 和 pressed 状态下显示一种颜色
                        intArrayOf(state_enable, state_pressed) to "#007EFF",
                        // 在 disable 和 unpressed 状态下显示另一种颜色 
                        intArrayOf(state_disable, state_unpressed) to "#FDB2DA"
                    )
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 将布局设置为 content view
        setContentView(contentView)
    }
}

新增 drawable 状态列表属性

将颜色和状态绑定只能满足一小部分需求,大多数时候需要将表现形式更多样的 drawable 和状态绑定。与<selector>对应的 Java 类是StateListDrawable,遂为控件新增 drawable 状态列表属性:

inline var View.background_drawable_state_list: List<Pair<IntArray, Drawable>>
    get() {
        return listOf(intArrayOf() to GradientDrawable())
    }
    set(value) {
        // 构建 StateListDrawable 实例并将属性值转换成一个个 state
        background = StateListDrawable().apply {
            value.forEach { pair ->
                // 为 StateListDrawable 添加一个状态值和 Drawable 实例的对应关系
                addState(pair.first, pair.second)
            }
        }
    }

之前,为了让一个按钮在可点击与不可点击时具备不一样的背景,需要定义三个 xml 文件:selector.xml + shape_clickable.xml + shape_unclickable.xml,现在一个都不需要了:

class MainActivity : AppCompatActivity() {
    // 用 DSL 动态构建布局
    private val contentView by lazy {
        ConstraintLayout {
            layout_width = match_parent
            layout_height = match_parent

            TextView {
                layout_width = wrap_content
                layout_height = wrap_content
                text = "commit"
                textSize = 30f
                gravity = gravity_center
                center_horizontal = true
                top_toTopOf = parent_id
                padding = 10
                // 为 TextView 设置 drawable 状态列表
                background_drawable_state_list = listOf(
                    // 可点击状态 = 带圆角的矩形
                    intArrayOf(state_enable) to shape {
                        shape = shape_rectangle
                        corner_radius = 10
                        solid_color = "#FDB2DA"
                    },
                    // 不可点击状态 = 带透明度的圆角矩形
                    intArrayOf(state_disable) to shape {
                        shape = shape_rectangle
                        corner_radius = 10
                        solid_color = "#80FDB2DA"
                    }
                )
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 将布局设置为 content view
        setContentView(contentView)
    }
}

Talk is cheap, show me the code

推荐阅读

目录
相关文章
|
1月前
|
XML 前端开发 Java
讲解SSM的xml文件
本文详细介绍了SSM框架中的xml配置文件,包括springMVC.xml和applicationContext.xml,涉及组件扫描、数据源配置、事务管理、MyBatis集成以及Spring MVC的视图解析器配置。
55 1
|
3月前
|
设计模式 Android开发 Kotlin
Android经典实战之Kotlin委托模式和by关键字
本文介绍了Kotlin中`by`关键字在类及属性委托中的运用,通过实例展示了如何利用类委托简化接口实现,以及如何借助标准与自定义属性委托管理属性的读写操作。通过`by`关键字的支持,Kotlin使得委托模式的实现更为直观且高效。
77 4
|
3月前
|
缓存 安全 Android开发
Android经典实战之用Kotlin泛型实现键值对缓存
本文介绍了Kotlin中泛型的基础知识与实际应用。泛型能提升代码的重用性、类型安全及可读性。文中详细解释了泛型的基本语法、泛型函数、泛型约束以及协变和逆变的概念,并通过一个数据缓存系统的实例展示了泛型的强大功能。
41 2
|
3天前
|
Java Maven
maven项目的pom.xml文件常用标签使用介绍
第四届人文,智慧教育与服务管理国际学术会议(HWESM 2025) 2025 4th International Conference on Humanities, Wisdom Education and Service Management
34 8
|
1月前
|
XML JavaScript Java
java与XML文件的读写
java与XML文件的读写
23 3
|
1月前
|
XML 存储 缓存
C#使用XML文件的详解及示例
C#使用XML文件的详解及示例
77 0
|
1月前
|
XML 存储 Web App开发
查看 XML 文件
查看 XML 文件
|
2月前
|
自然语言处理 Java 网络架构
解锁跨平台微服务新纪元:Micronaut与Kotlin联袂打造的多语言兼容服务——代码、教程、实战一次打包奉送!
【9月更文挑战第6天】Micronaut是一款轻量级、高性能的Java框架,适用于微服务开发。它支持Java、Groovy和Kotlin等多种语言,提供灵活的多语言开发环境。本文通过创建一个简单的多语言兼容服务,展示如何使用Micronaut及其注解驱动特性实现REST接口,并引入国际化支持。无论是个人项目还是企业应用,Micronaut都能提供高效、一致的开发体验,成为跨平台开发的利器。通过简单的配置和代码编写,即可实现多语言支持,展现其强大的跨平台优势。
51 3
|
2月前
|
SQL XML Java
mybatis :sqlmapconfig.xml配置 ++++Mapper XML 文件(sql/insert/delete/update/select)(增删改查)用法
当然,这些仅是MyBatis功能的初步介绍。MyBatis还提供了高级特性,如动态SQL、类型处理器、插件等,可以进一步提供对数据库交互的强大支持和灵活性。希望上述内容对您理解MyBatis的基本操作有所帮助。在实际使用中,您可能还需要根据具体的业务要求调整和优化SQL语句和配置。
44 1
|
3月前
|
算法 安全 数据安全/隐私保护
Android经典实战之常见的移动端加密算法和用kotlin进行AES-256加密和解密
本文介绍了移动端开发中常用的数据加密算法,包括对称加密(如 AES 和 DES)、非对称加密(如 RSA)、散列算法(如 SHA-256 和 MD5)及消息认证码(如 HMAC)。重点讲解了如何使用 Kotlin 实现 AES-256 的加密和解密,并提供了详细的代码示例。通过生成密钥、加密和解密数据等步骤,展示了如何在 Kotlin 项目中实现数据的安全加密。
114 1