赞
踩
现在大多数的项目当中都会有一个弹框组件,其目的是为了可以将涉及到弹框场景的逻辑,或者ui统一的进行管理维护,带来的好处是需要弹框的地方不用重新自己去自定义一个,导致弹框轮子泛滥,而是调用组件提供的api将一个符合设计规范的弹框渲染出来,如果设计规范更新了,只要更新一下组件,那么所有弹框都可以一起更新,节省了逐个修改的时间。从另一个方面来说,由于弹框组件几乎整个团队里面每个人都会使用,它的优点与缺点将统统暴露出来,所以如何去设计一个弹框组件是每一个开发者都要去考虑的问题,而目前我们常见的弹框组件设计方式有两种
这是一种设计方式,会将弹框标题,弹框内容,弹框按钮文案,弹框按钮点击事件一起传给构造函数,再多重载几个函数来支持一些特定场景比如没有标题,单个按钮,文案颜色等等,我一般如果接手个项目,这个项目是多人开发的话,我都会主动揽下弹框组件开发的任务,不是因为写弹框有瘾,主要是担心别人使用这种方式写框子,说又不好说,做起来真的是噩梦,这种方式的优点缺点总结如下
另一种设计方式是使用建造者模式,这也是我惯用的方式,将弹框中的所有元素都一一对外暴露出一个方法,让调用方去设置,需要用到哪个元素就去设置设置哪个元素,组件内部默认实现一套样式,如果有的元素没有被调用方设置,就默认使用组件自带的实现方式,但这种方式也有优缺点,总结如下
鉴于上述提到的两种设计方式以及总结出来的优缺点,我们不禁有个疑问,这种方式也不行,那个方式也不是很好,那么这么常用的组件难道就没有更好的设计方式了吗,能够设计出来以后可以满足如下几个要求
想要实现以上几点,我们就要使用这篇文章的重点DSL了,那什么是DSL呢,那就是领域专用语言:专门解决某一特定问题的计算机语言,比如我们常用的正则表达式就是一种DSL,它与我们常用的api不一样,有着自己独特的结构,也叫做文法,在Kotlin里面这种结构我们使用lambda表达式去完成
在使用DSL自定义弹框之前,我们先看一个例子,我们刚接触kotlin的时候,一定接触过它标准库里的let跟apply函数,也死记硬背的区分了一下这俩函数的区别,在实际开发当中也用到过,比如有一个按钮,我们需要去设置它的文案,字体大小以及点击事件,一般会这么做
我们看到每次访问按钮的一个属性就要重复写一下button,如果访问的属性变多了,那代码就会显的特别的啰嗦,所以这个时候,let跟apply函数就派上用场了
我们看到两者的区别体现在了let后面的lambda表达式里面,使用it显示的代替了button,如果万一button需要改变一下变量名,我们只需要更改let左边的button就好,而apply后面的表达式里面,完全省略了it,整个表达式的作用域就是button,可以直接访问button的属性,我们在牢记这个差异的同时,是不是也想一想,为什么这俩函数会存在这样的差异呢?答案就在这俩函数的源码当中,我们看一下
我们看到两个函数源码最大的区别在于let的入参是一个参数为T的函数类型的参数,所以在lambda表达式中我们可以用it显示的代替T,而apply的入参稍显不同,它的入参也是个函数类型,但是T被挪到了括号的前面,当作一个接收者来接受lambda表达式中返回的结果,所以才会导致apply函数后面只有它的属性以及值,结构及其精简,而kotlin中的DSL的主要语法点就是带接收者的lambda,现在我们就带着这个语法点开始一步步去自定义我们的弹框吧
首先我们先从简单的实现一个AlertDialog弹框开始
AlertDialog的一个特点就是使用了建造者模式,每一个设置函数结束后都会返回给AlertDialog.Builder,那么从这一点上我们就可以仿照apply函数那样,将生成Dialog的这个过程转换成带有接收者的lambda表达式,那么先要做的就是给AlertDialog.Builder增加一个扩展函数,内部接收一个带有接收者的lambda表达式的参数
现在我们可以使用新增的createDialog函数来改变下刚刚生成AlertDialog的代码
createDialog作用类似于函数apply,lambda代码块的作用域就是AlertDialog.Builder,可以访问任何AlertDialog.Builder中的函数,上述代码我们可以再简化一下,将createDialog作为一个顶层函数,在函数内部生成AlertDialog.Builder实例,顶层函数如下
而调用弹框的地方代码也一同更改成了
运行一下代码我们就得到了一个系统自带的弹框
但是这样的一个弹框,我想国内应该没几个设计师会喜欢,所以按照设计师给的视觉图,在现有基础上去自定义弹框是我们接下去要做的事情,撇开一些特定的业务场景,一个弹框组件需要具备如下功能
第一步我们先做弹框的布局,对于一个弹框组件来讲,设计师会事先将所有弹框样式都设计出来,所以整体布局的大体样式是固定的,我们以一个简单的dialog_layout布局文件作为弹框的样式
整个布局结构很简单,从上到下分别是标题,内容,按钮区,接下来我们就在顶层函数createDialog的lambda表达式中把布局设置到弹框里去,并且让弹框的宽度与屏幕宽度成比例自适应,毕竟不同app里面弹框的宽度都不一定相同
效果如下
一个纯白色弹框就出来了,接下来我们简化一下代码,由于每次调用弹框,dialog.show以及下面的设置宽度以及弹框位置的代码都会去调用,所以为了避免重复,反复造轮子,我们可以给AlertDialog增加一个扩展函数,将这些代码都放在扩展函数里面,上层只需要调用这个扩展函数就行,扩展函数我们就命名为showDialog,代码如下
上层调用弹框的地方就变成了
是不是精简了很多呢,代码运行的效果是一样的,就不展示了,但是目前我们这个框子还只是普通的样式,我们如果想要给它设置个圆角,然后捎带一些渐变色效果的背景,该怎么做呢?我们第一个想到的就是做一个drawable文件,在里面写上这些样式,再设置给布局根视图的background不就可以了吗,这的确是一个办法,但是如果有一天设计师突发奇想,觉得在某些场景下弹框使用样式A,某些场景下使用样式B,难道在生成一个新的drawable文件吗,这样一来单单一个弹框组件就要维护两种样式文件,给项目维护又带来了一定的成本,所以我们得想个更好的办法,就是使用GradientDrawable动态给布局设置样式,作法如下
看到在代码中用红框子以及绿框子区分了两部分代码,我们先看红框子里面,都能看明白主要是做渲染的工作,生成了一个GradientDrawable实例,然后分别对它设置了背景色,渐变方向,圆角大小,而这个我们就可以用带接收者的lambda表达式替换,GradientDrawable就是接收者,在看绿框子里面,虽然现在代码不多,但是setView之前肯定还得对view里面的元素做初始化等一系列操作,所以view也是一个接收者,初始化等操作可以放在lambda表达式中进行,理清了这些以后,我们新增一个AlertDialog.Builder的扩展函数rootLayout
rootLayout函数一共接收三个参数,root就是我们的弹框视图,render就是渲染操作,job是初始化view的操作,对于渲染操作来讲,rootLayout内部已经实现了一套默认的样式,如果调用方不使用render函数,那弹框就使用默认样式,如果使用了render函数,那么render里面有同样属性的就覆盖,有新增属性就累加,这个时候,上层调用方代码就更改为
我们运行一下看看效果
跟我们想要设置的效果一模一样,现在我们试试看不使用默认的样式,想要让弹框上面的圆角为12dp,下面没有圆角,背景渐变色变为从左到右方向由灰变白,我们在render函数里面加上这些设置
运行以后效果就变成了
有了弹框布局的开发经验,标题就容易多了,既然job函数的接收者是View,那么我们就给View先定一个扩展函数title
这个函数专门用来做标题相关部分的操作,而title的参数则是一个接收者为TextView的lambda表达式,用来在调用方额外给标题添加设置,那现在我们就可以给弹框添加个标题了,顺便把框的四个角都变成圆角,好看些
加了一个深色加粗标题,其中textColor属性是我添加的扩展属性,为的是让代码看上去整洁一些,效果等同于setTextColor(getColor(R.color.color_303F9F))
再次运行一下,标题就出来了
好像标题有点太靠上了,我们给弹框整体加个10dp的内边距在看下效果
效果出来了,我们再进行下一步
有了标题的例子,弹框内容基本都一样,不多说直接上代码
然后在弹框上添加一段文案
效果如下
通常弹框组件都会有单个按钮弹框(提示型)和两个按钮弹框(交互型)两种类型,我们的dialog_layout布局中有两个TextView分别用来作为按钮,默认左边的negativeBtn是隐藏的,右边positiveBtn是展示出来的,这里我是仿照着AlertDialog里面设置按钮的逻辑来做,当只调用setPositiveButton的时候,表示此时为单个按钮弹框,当同时又调用了setNegativeButton的时候,就表示两个按钮的弹框,我们这边也借用这个思想,定义两个函数来控制这俩个按钮
代码很简单,当然也可以在函数里面加入一些默认样式,比如positiveBtn一般为高亮色值,negativeBtn为灰色色值,现在我们去调用下这俩函数,首先展示只有一个按钮的弹框
像Alertdialog一样只调用了positiveBtn函数就可以了,效果图如下
当我们要在弹框上显示两个按钮的时候,只需要再增加一个negativeBtn就可以了,就像这样
接下来就是给按钮设置监听事件了,非常容易,只需要调用setOnClickListener就可以了
这样其实可以完事了,弹框可以正常点击完以后做一些业务逻辑并且让弹框消失,但是仅仅这样的话我们这代码里还是存在着一些设计不合理的地方
那么我们就要更改下rootLayout函数,让它的返回值从AlertDialog.Builder变成Unit,而上述说的create以及showDialog操作,就要在rootLayout中进行,更改完的代码如下
mDialog是组件中维护的一个顶层属性,这也是为了在点击弹框按钮时候,在组件内部关闭弹框,接下去我们开始处理弹框按钮的点击事件,由于点击事件是作用在TextView上的,所以先给TextView增加一个扩展函数clickEvent,用来处理关闭弹框和其他点击事件的逻辑
现在我们可以回到调用方那边,将弹框的代码更新一下,并给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 { } } } }
到这里我们的弹框组件就大功告成了,顺带贴上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
作者:Coffeeee
链接:https://juejin.cn/post/7204601386607706172
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。