我们使用DSL的方式自定义了一个弹框组件,完全撇弃了以往传统自定义View的命令式方式,采用了声明式构建UI的方式,无论是在代码的可读性上,组件的扩展性上,还有维护成本上,都有了不小的改善,那么在这篇文章中,我们继续用DSL去自定义我们常用的Drawable控件
作者:Coffeeee 链接:https://juejin.cn/post/7206732474112540727
常用的方式
xml文件
我们经常在项目当中遇到需要给控件设置背景样式的场景,比如圆角,渐变等,我们会在项目中新建一个drawable文件,在里面写上对应样式代码,比如这样
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:radius="10dp" />
<solid android:color="#000000"/>
</shape>
这样一个黑色背景,圆角是10dp的背景样式就做好了,非常简单,但是我们项目当中不可能只会有一种样式,不同圆角,不同背景色,边框颜色与粗细等等不同的样式组合搭配起来以后,我们项目中的drawable文件的数量是惊人的,造成很大的维护成本
自定义drawable控件
所以介于这种情况,有些项目当中就会加入一个自定义的drawable控件,通过自定义属性,使调用方在xml文件中根据不同需求场景,选择合适的属性去渲染背景,这种方式通常在项目初期可以解决大部分样式需求,可是长久下来依然存在一些不可避免的问题
属性必须确保唯一性,如果其他自定义控件或者三方控件使用了相同名字的属性,则会编译报错
一种控件只支持一种布局,如果想要让所有布局都支持自定义drawable样式,必须分别定义各自的子类,然后去代理drawable功能,开发成本较大
功能无法满足所有场景,维护方需要时常根据需求去扩展控件属性以及功能,造成控件负担过大,产生bug的概率增大
所以为了达到扩展性强,维护成本低,兼容性高的目的,我们现在用DSL的方式去实现一个drawable控件
开始开发
首先为了达到兼容性高的目的,我们的自定义drawable肯定可以设置在任何控件的background属性上,所以首先要做的就是确定个顶层函数,然后参数之一就是接收目标控件
fun rootView(root: View){
}
root可以是任何你想设置背景的控件,然后我们就要考虑要往这个控件上设置什么样的样式,先考虑一些常用样式,比如圆角,背景色,渐变色,边框颜色与粗细,马上脑海中我们就知道使用GradientDrawable这个类,用它作为接收者,在它的lambda表达式中将需要的属性渲染出来,我们在rootView中加入第二个参数以及逻辑代码
fun rootView(root: View,
normalRender: GradientDrawable.() -> Unit){
val mNormal = GradientDrawable()
with(mNormal) {
normalRender()
root.background = this
}
}
现在我们可以在上层调用这个函数了,比如需要给root设置个背景为灰色,边框为红色,圆角为10dp的样式,调用方的代码如下
rootView(
root = bindingView.mainLinear,
normalRender = {
cornerRadius = 10f.DP()
color = ColorStateList.valueOf(getColor(R.color.color_BBBBBB))
setStroke(2f.DP().toInt(), getColor(R.color.color_FF4081))
})
结构很清晰,在normalRender里面可以设置任何GradientDrawable支持的属性,运行一下这段代码得到如下效果
有了上一步经验,接下去的工作就方便了,我们刚刚只是设置了普通状态下的样式,但是对于一个控件来讲,被点击时候的反馈也是一种样式,有过自定义drawable控件经验的大佬马上清楚了,这里需要用到另一个Drawable的子类--StateListDrawable,做法就是在rootView函数中在增加一个GradientDrawable为接收者的lambda参数,让它作为点击状态的样式,然后在代码块中连通normalRender一起设置在StateListDrawable里面,我们将rootView函数的代码调整一下
fun rootView(root: View,
normalRender: GradientDrawable.() -> Unit,
pressedRender: GradientDrawable.() -> Unit = {}){
val mNormal = GradientDrawable()
with(mNormal) {
normalRender()
}
val mPressed = GradientDrawable()
with(mPressed) {
pressedRender()
}
with(StateListDrawable()) {
addState(
intArrayOf(
android.R.attr.state_focused,
android.R.attr.state_pressed,
android.R.attr.state_enabled
), mPressed
)
addState(
intArrayOf(android.R.attr.state_pressed),
mPressed
)
addState(
intArrayOf(android.R.attr.state_focused),
mPressed
)
addState(
intArrayOf(android.R.attr.state_enabled),
mNormal
)
addState(intArrayOf(), mNormal)
root.isClickable = true
root.background = this
}
}
这里别忘了给root的isClickable属性设置为true,不然点击是没有效果的,同时我们给pressedRender设置了个可空的默认值,兼容一些不需要点击效果的控件。现在我们在上层可以给原有的控件添加点击效果了,比如点击效果为背景色为黑白的渐变色,方向从上到下,边框颜色取消,那么调用方代码修改如下
rootView(
root = bindingView.mainLinear,
normalRender = {
cornerRadius = 10f.DP()
color = ColorStateList.valueOf(getColor(R.color.color_BBBBBB))
setStroke(2f.DP().toInt(), getColor(R.color.color_FF4081))
},
pressedRender = {
cornerRadius = 10f.DP()
colors = intArrayOf(getColor(R.color.black), getColor(R.color.white))
orientation = GradientDrawable.Orientation.TOP_BOTTOM
})
哪个代码块做什么事情一目了然,如果不需要点击事件只需要去掉pressedRender就好了,我们运行一遍得到效果如下
点击效果设置完成了,我们肯定还需要另一种效果,也就是水波纹效果,水波纹用到了RippleDrawable这个类,这里我们需要给rootView增加一个开关,需不需要用水波纹,打开就是使用水波纹效果,关闭则使用我们设置的点击效果,同时增加一个水波纹颜色的参数,rootView函数调整如下
inline fun rootView(
root: View,
normalRender: GradientDrawable.() -> Unit,
pressedRender: GradientDrawable.() -> Unit = {},
ripple: Boolean = false,
rippleColor: Int = 0
) {
val mNormal = GradientDrawable()
with(mNormal) {
normalRender()
}
val mPressed = GradientDrawable()
with(mPressed) {
pressedRender()
}
if (ripple) {
val rippleDrawable = RippleDrawable(
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_pressed),
intArrayOf(android.R.attr.state_focused),
intArrayOf(android.R.attr.state_activated)
),
intArrayOf(
rippleColor,
rippleColor,
rippleColor
)
), mNormal, ShapeDrawable()
)
root.isClickable = true
root.background = rippleDrawable
} else {
with(StateListDrawable()) {
addState(
intArrayOf(
android.R.attr.state_focused,
android.R.attr.state_pressed,
android.R.attr.state_enabled
), mPressed
)
addState(
intArrayOf(android.R.attr.state_pressed),
mPressed
)
addState(
intArrayOf(android.R.attr.state_focused),
mPressed
)
addState(
intArrayOf(android.R.attr.state_enabled),
mNormal
)
addState(intArrayOf(), mNormal)
root.isClickable = true
root.background = this
}
}
}
同样我们给开关与水波纹颜色设置了默认值,兼容那些不需要水波纹效果的场景,现在我们在调用方那里添加一个水波纹效果,水波纹颜色为黄色,代码如下
rootView(
root = bindingView.mainLinear,
normalRender = {
cornerRadius = 10f.DP()
color = ColorStateList.valueOf(getColor(R.color.color_BBBBBB))
setStroke(2f.DP().toInt(), getColor(R.color.color_FF4081))
},
pressedRender = {
cornerRadius = 10f.DP()
colors = intArrayOf(getColor(R.color.black), getColor(R.color.white))
orientation = GradientDrawable.Orientation.TOP_BOTTOM
}, ripple = true, rippleColor = getColor(R.color.color_f7c653)
)
代码运行效果如下
总结
一个简单实用的drawable控件就完成了,没有去继承任何View,但可以设置在任何View上面,也不用去attrs.xml文件里面去定义属性,样式的设置完全依赖于系统的api,降低了bug产生的概率,我们以后在开发过程当中,一些简单的自定义控件就都可以使用DSL的方式来实现。
关注我获取更多知识或者投稿