开启成长之旅!这是我参加「日新计划 2 月更文应战」的第 4 天,点击查看活动详情

现在大多数的项目傍边都会有一个弹框组件,其意图是为了能够将涉及到弹框场景的逻辑,或许ui一致的进行管理保护,带来的长处是需求弹框的当地不必重新自己去自界说一个,导致弹框轮子泛滥,而是调用组件供给的api将一个契合规划规范的弹框烘托出来,假如规划规范更新了,只需更新一下组件,那么一切弹框都能够一起更新,节省了逐个修正的时刻。从另一个方面来说,因为弹框组件简直整个团队里边每个人都会运用,它的长处与缺陷将统统暴露出来,所以怎么去规划一个弹框组件是每一个开发者都要去考虑的问题,而现在咱们常见的弹框组件规划方法有两种

常见的规划方法

运用结构函数一键生成

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

这是一种规划方法,会将弹框标题,弹框内容,弹框按钮案牍,弹框按钮点击作业一起传给结构函数,再多重载几个函数来支撑一些特定场景比方没有标题,单个按钮,案牍色彩等等,我一般假如接手个项目,这个项目是多人开发的话,我都会主动揽下弹框组件开发的使命,不是因为写弹框有瘾,主要是担心他人运用这种方法写框子,说又不好说,做起来真的是噩梦,这种方法的长处缺陷总结如下

  • 长处:不知道
  • 缺陷:
    • 代码角度来讲,可读性比较差,大量的入参会让调用者在填写参数的时分发生迷惑,不知道具体某一个参数对应的是什么功用。
    • 关于保护人员来讲,每次组件需求改动一个元素,就需求将每个结构函数的逻辑都修正一遍,作业量大而且简单出错。
    • 关于调用方来讲,每次需求写大量参数,而且需求严格遵守参数的声明次序,组件假如更新了函数签名,调用处就会发生编译报错

运用制作者形式链式调用

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

另一种规划方法是运用制作者形式,这也是我惯用的方法,将弹框中的一切元素都一一对外暴露出一个方法,让调用方去设置,需求用到哪个元素就去设置设置哪个元素,组件内部默许完结一套款式,假如有的元素没有被调用方设置,就默许运用组件自带的完结方法,但这种方法也有优缺陷,总结如下

  • 长处:将功用用函数区分开来,职能明晰,调用方可依据自己的需求选择性的调用对应函数烘托弹框
  • 缺陷:保护者需求不断依据新的需求往组件里边添加新的方法供调用方运用,比方想要将标题加粗,假如组件没有供给对应的setTitleBold这样的方法,那么调用方将无法完结这个功用,多轮迭代下来,或许组件里边现已积累了各种各样的方法,假如不好好分类管理,那阅览起来也是很头疼的一件作业

第三种规划方法

鉴于上述提到的两种规划方法以及总结出来的优缺陷,咱们不由有个疑问,这种方法也不可,那个方法也不是很好,那么这么常用的组件莫非就没有更好的规划方法了吗,能够规划出来今后能够满足如下几个要求

  • 组件具有极强的扩展性,调用方能够随意界说自己需求的功用
  • 保护方不必频频的在组件中添加功用,保持组件的稳定性
  • 结构明晰,每个代码块负责一个组件元素的功用

DSL的界说

想要完结以上几点,咱们就要运用这篇文章的重点DSL了,那什么是DSL呢,那便是领域专用语言:专门处理某一特定问题的计算机语言,比方咱们常用的正则表达式便是一种DSL,它与咱们常用的api不相同,有着自己共同的结构,也叫做文法,在Kotlin里边这种结构咱们运用lambda表达式去完结

带接纳者的lambda

在运用DSL自界说弹框之前,咱们先看一个例子,咱们刚触摸kotlin的时分,必定触摸过它标准库里的let跟apply函数,也死记硬背的区分了一下这俩函数的差异,在实际开发傍边也用到过,比方有一个按钮,咱们需求去设置它的案牍,字体巨细以及点击作业,一般会这么做

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

咱们看到每次拜访按钮的一个特色就要重复写一下button,假如拜访的特色变多了,那代码就会显的特别的烦琐,所以这个时分,let跟apply函数就派上用场了

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

咱们看到两者的差异体现在了let后边的lambda表达式里边,运用it显现的替代了button,假如如果button需求改变一下变量名,咱们只需求更改let左边的button就好,而apply后边的表达式里边,彻底省掉了it,整个表达式的作用域便是button,能够直接拜访button的特色,咱们在紧记这个差异的同时,是不是也想一想,为什么这俩函数会存在这样的差异呢?答案就在这俩函数的源码傍边,咱们看一下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

咱们看到两个函数源码最大的差异在于let的入参是一个参数为T的函数类型的参数,所以在lambda表达式中咱们能够用it显现的替代T,而apply的入参稍显不同,它的入参也是个函数类型,可是T被挪到了括号的前面,当作一个接纳者来承受lambda表达式中回来的结果,所以才会导致apply函数后边只要它的特色以及值,结构及其精简,而kotlin中的DSL的主要语法点便是带接纳者的lambda,现在咱们就带着这个语法点开端一步步去自界说咱们的弹框吧

开端开发

首要咱们先从简略的完结一个AlertDialog弹框开端

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

AlertDialog的一个特色便是运用了制作者形式,每一个设置函数结束后都会回来给AlertDialog.Builder,那么从这一点上咱们就能够仿照apply函数那样,将生成Dialog的这个进程转换成带有接纳者的lambda表达式,那么先要做的便是给AlertDialog.Builder添加一个扩展函数,内部接纳一个带有接纳者的lambda表达式的参数

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

现在咱们能够运用新增的createDialog函数来改变下刚刚生成AlertDialog的代码

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

createDialog作用类似于函数apply,lambda代码块的作用域便是AlertDialog.Builder,能够拜访任何AlertDialog.Builder中的函数,上述代码咱们能够再简化一下,将createDialog作为一个顶层函数,在函数内部生成AlertDialog.Builder实例,顶层函数如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

而调用弹框的当地代码也一起更改成了

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

运转一下代码咱们就得到了一个系统自带的弹框

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

可是这样的一个弹框,我想国内应该没几个规划师会喜欢,所以按照规划师给的视觉图,在现有基础上去自界说弹框是咱们接下去要做的作业,放下一些特定的事务场景,一个弹框组件需求具有如下功用

  1. 弹框布局可自界说款式,比方圆角,背景色彩
  2. 弹框标题可自界说,比方案牍,字体色彩,巨细
  3. 弹框内容可自界说,比方案牍,字体色彩,巨细
  4. 弹框按钮数量可装备一个或两个

弹框布局

第一步咱们先做弹框的布局,关于一个弹框组件来讲,规划师会事先将一切弹框款式都规划出来,所以全体布局的大体款式是固定的,咱们以一个简略的dialog_layout布局文件作为弹框的款式

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

整个布局结构很简略,从上到下别离是标题,内容,按钮区,接下来咱们就在顶层函数createDialog的lambda表达式中把布局设置到弹框里去,而且让弹框的宽度与屏幕宽度成份额自适应,究竟不同app里边弹框的宽度都不必定相同

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

作用如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

一个纯白色弹框就出来了,接下来咱们简化一下代码,因为每次调用弹框,dialog.show以及下面的设置宽度以及弹框方位的代码都会去调用,所以为了避免重复,反复造轮子,咱们能够给AlertDialog添加一个扩展函数,将这些代码都放在扩展函数里边,上层只需求调用这个扩展函数就行,扩展函数咱们就命名为showDialog,代码如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

上层调用弹框的当地就变成了

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

是不是精简了很多呢,代码运转的作用是相同的,就不展现了,可是现在咱们这个框子还只是普通的款式,咱们假如想要给它设置个圆角,然后捎带一些渐变色作用的背景,该怎么做呢?咱们第一个想到的便是做一个drawable文件,在里边写上这些款式,再设置给布局根视图的background不就能够了吗,这的确是一个方法,可是假如有一天规划师突发奇想,觉得在某些场景下弹框运用款式A,某些场景下运用款式B,莫非在生成一个新的drawable文件吗,这样一来单单一个弹框组件就要保护两种款式文件,给项目保护又带来了必定的本钱,所以咱们得想个更好的方法,便是运用GradientDrawable动态给布局设置款式,作法如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

看到在代码中用红框子以及绿框子区分了两部分代码,咱们先看红框子里边,都能看明白主要是做烘托的作业,生成了一个GradientDrawable实例,然后别离对它设置了背景色,渐变方向,圆角巨细,而这个咱们就能够用带接纳者的lambda表达式替换,GradientDrawable便是接纳者,在看绿框子里边,尽管现在代码不多,可是setView之前必定还得对view里边的元素做初始化等一系列操作,所以view也是一个接纳者,初始化等操作能够放在lambda表达式中进行,理清了这些今后,咱们新增一个AlertDialog.Builder的扩展函数rootLayout

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

rootLayout函数一共接纳三个参数,root便是咱们的弹框视图,render便是烘托操作,job是初始化view的操作,关于烘托操作来讲,rootLayout内部现已完结了一套默许的款式,假如调用方不运用render函数,那弹框就运用默许款式,假如运用了render函数,那么render里边有同样特色的就覆盖,有新增特色就累加,这个时分,上层调用方代码就更改为

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

咱们运转一下看看作用

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

跟咱们想要设置的作用一模相同,现在咱们试试看不运用默许的款式,想要让弹框上面的圆角为12dp,下面没有圆角,背景渐变色变为从左到右方向由灰变白,咱们在render函数里边加上这些设置

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

运转今后作用就变成了

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

弹框标题

有了弹框布局的开发经验,标题就简单多了,既然job函数的接纳者是View,那么咱们就给View先定一个扩展函数title

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

这个函数专门用来做标题相关部分的操作,而title的参数则是一个接纳者为TextView的lambda表达式,用来在调用方额定给标题添加设置,那现在咱们就能够给弹框添加个标题了,顺便把框的四个角都变成圆角,好看些

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

加了一个深色加粗标题,其间textColor特色是我添加的扩展特色,为的是让代码看上去整齐一些,作用等同于setTextColor(getColor(R.color.color_303F9F))

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

再次运转一下,标题就出来了

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

如同标题有点太靠上了,咱们给弹框全体加个10dp的内边距在看下作用

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看
使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

作用出来了,咱们再进行下一步

弹框内容

有了标题的例子,弹框内容根本都相同,不多说直接上代码

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

然后在弹框上添加一段案牍

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

作用如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

弹框按钮

通常弹框组件都会有单个按钮弹框(提示型)和两个按钮弹框(交互型)两种类型,咱们的dialog_layout布局中有两个TextView别离用来作为按钮,默许左边的negativeBtn是躲藏的,右边positiveBtn是展现出来的,这儿我是仿照着AlertDialog里边设置按钮的逻辑来做,当只调用setPositiveButton的时分,表明此时为单个按钮弹框,当同时又调用了setNegativeButton的时分,就表明两个按钮的弹框,咱们这边也借用这个思想,界说两个函数来控制这俩个按钮

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

代码很简略,当然也能够在函数里边加入一些默许款式,比方positiveBtn一般为高亮色值,negativeBtn为灰色色值,现在咱们去调用下这俩函数,首要展现只要一个按钮的弹框

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

像Alertdialog相同只调用了positiveBtn函数就能够了,作用图如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

当咱们要在弹框上显现两个按钮的时分,只需求再添加一个negativeBtn就能够了,就像这样

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看
使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

接下来便是给按钮设置监听作业了,非常简单,只需求调用setOnClickListener就能够了

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

这样其实能够完事了,弹框能够正常点击完今后做一些事务逻辑而且让弹框消失,可是只是这样的话咱们这代码里仍是存在着一些规划不合理的当地

  • 每一次createDialog今后,都有必要showDialog今后弹框才干出来,这个能够让组件自己完结而不必调用方自己每次去showDialog
  • rootLayout回来的是AlertDialog.Builder对象,有必要调用create今后才干得到AlertDialog对象去操作弹框展现与躲藏,这些也应该放在组件里边进行
  • 弹框按钮点击的默许操作根本都是封闭弹框,所以也没有必要每次在点击作业中显现的调用dismiss函数,也能够将封闭的动作放在组件中进行

那么咱们就要更改下rootLayout函数,让它的回来值从AlertDialog.Builder变成Unit,而上述说的create以及showDialog操作,就要在rootLayout中进行,更改完的代码如下

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

mDialog是组件中保护的一个顶层特色,这也是为了在点击弹框按钮时分,在组件内部封闭弹框,接下去咱们开端处理弹框按钮的点击作业,因为点击作业是作用在TextView上的,所以先给TextView添加一个扩展函数clickEvent,用来处理封闭弹框和其他点击作业的逻辑

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

现在咱们能够回到调用方那边,将弹框的代码更新一下,并给positiveBtn和negativeBtn别离加上新增的clickEvent函数作为点击作业,而positiveBtn点击后还会弹出一个Toast作为呼应作业

createDialog(this) {
    rootLayout(
        root = layoutInflater.inflate(R.layout.dialog_layout, null),
        render = {
            orientation = GradientDrawable.Orientation.LEFT_RIGHT
            colors = intArrayOf(
                getColor(R.color.color_BBBBBB),
                getColor(R.color.white)
            )
            cornerRadius = DensityUtil.dp2px(12f).toFloat()
        }
    ) {
        title {
            text = "DSL弹框"
            typeface = Typeface.DEFAULT_BOLD
            textColor = getColor(R.color.color_303F9F)
        }
        message {
            text = "用DSL方法自界说的弹框用DSL方法自界说的弹框用DSL方法自界说的弹框用DSL方法自界说的弹框"
            gravity = Gravity.CENTER
            textColor = getColor(R.color.black)
        }
        positiveBtn {
            text = "知道了"
            textColor = getColor(R.color.color_FF4081)
            clickEvent {
                Toast.makeText(this@MainActivity, "开端处理呼应作业", Toast.LENGTH_SHORT).show()
            }
        }
        negativeBtn {
            text = "撤销"
            textColor = getColor(R.color.color_303F9F)
            clickEvent { }
        }
    }
}

运转一下看看作用怎么

使用DSL的方式自定义了一个弹框,代码忽然变的有那么一点点好看

到这儿咱们的弹框组件就大功告成了,顺带贴上AlertDialog.kt的源码

弹框组件源码

lateinit var mDialog: AlertDialog
var TextView.textColor: Int
    get() {
        return this.textColors.defaultColor
    }
    set(value) {
        this.setTextColor(value)
    }
fun createDialog(ctx: Context, body: AlertDialog.Builder.() -> Unit) {
    val dialog = AlertDialog.Builder(ctx)
    dialog.body()
}
@RequiresApi(Build.VERSION_CODES.M)
inline fun AlertDialog.Builder.rootLayout(
    root: View,
    render: GradientDrawable.() -> Unit = {},
    job: View.() -> Unit
) {
    with(GradientDrawable()){
        //默许款式
        render()
        root.background = this
    }
    root.setPadding(DensityUtil.dp2px(10f))
    root.job()
    mDialog = setView(root).create()
    mDialog.showDialog()
}
inline fun View.title(titleJob: TextView.() -> Unit) {
    val title = findViewById<TextView>(R.id.dialog_title)
    //能够加一些标题的默许操作,比方字体色彩,字体巨细
    title.titleJob()
}
inline fun View.message(messageJob: TextView.() -> Unit) {
    val message = findViewById<TextView>(R.id.dialog_message)
    //能够加一些内容的默许操作,比方字体色彩,字体巨细,居左对齐仍是居中对齐
    message.messageJob()
}
inline fun View.negativeBtn(negativeJob: TextView.() -> Unit) {
    val negativeBtn = findViewById<TextView>(R.id.dialog_negative_btn_text)
    negativeBtn.visibility = View.VISIBLE
    negativeBtn.negativeJob()
}
inline fun View.positiveBtn(positiveJob: TextView.() -> Unit) {
    val positiveBtn = findViewById<TextView>(R.id.dialog_positive_btn_text)
    positiveBtn.positiveJob()
}
inline fun TextView.clickEvent(crossinline event: () -> Unit) {
    setOnClickListener {
        mDialog.dismiss()
        event()
    }
}
fun AlertDialog.showDialog() {
    show()
    val mWindow = window
    mWindow?.setBackgroundDrawableResource(R.color.transparent)
    val group: ViewGroup = mWindow?.decorView as ViewGroup
    val child: ViewGroup = group.getChildAt(0) as ViewGroup
    child.post {
        val param: WindowManager.LayoutParams? = mWindow.attributes
        param?.width = (DensityUtil.getScreenWidth() * 0.8).toInt()
        param?.gravity = Gravity.CENTER
        mWindow.setGravity(Gravity.CENTER)
        mWindow.attributes = param
    }
}

总结

或许早就有人现已发现了,咱们现在弹框的调用方法跟Compose,React很类似,也便是最近很流行的声明式UI,为什么说它流行,比咱们传统的指令式UI好用,主要的差别就在于声明式UI调用方只需求在乎视图的描绘就能够,而真正视图怎么烘托,怎么丈量,调用方不需求关心,在咱们的弹框的例子中,调用方全程需求做的便是对着视觉稿子,将弹框中的元素以及需求的特色款式一个个写上去就好了,就算弹框后期需求变化再频频,关于调用方来说只是增减几个元素特色的作业,而像弹框怎么设置自界说的视图,怎么丈量与屏幕之间的宽度份额等,不需求调用方去关心,所以这种方法在咱们今后的开发傍边能够逐步学习,适应,运用起来了,并不是说只要在写React,Flutter或许Compose之类的项目中才用到这种声明式UI