匿名内部类在 Java 中是经常用到的一个特性,例如在 Android 开发中的各种 Listener,使用时也很简单,比如:
//lambda
button.setOnClickListener(v -> {
//do some thing
});
//匿名内部类
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do some thing
}
});
只有一个函数的接口在 Java 和 Kotlin 中都可以很方便的使用 lambda 表达式来缩略,但是如果接口含有多个函数,使用起来就比较”不优雅“了,例如:
etString.addTextChangedListener(object :TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
TODO("Not yet implemented")
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
TODO("Not yet implemented")
}
override fun afterTextChanged(s: Editable?) {
TODO("Not yet implemented")
}
})
使用起来与 Java 基本差不多,通过 object 关键字实现了一个匿名内部类,这种方法没什么大问题,例如上面的例子中,三个回调函数并非每次都要使用,很多场景可能只会用到其中一个或者几个,其余的都是空实现,每次都写这样一个匿名内部类只不过是不优雅而已。
在 Kotlin 中我们可以有两种方式实现比较优雅的使用匿名内部类:
1. DSL
2. 高阶函数
DSL
DSL 方式实现封装可以分为以下几步:
1.创建接口实现类:XxxxInterfaceDslImpl
还有上面的 TextWatcher 作为例子:
class TextWatcherDslImpl : TextWatcher {
//原接口对应的kotlin函数对象
private var afterTextChanged: ((Editable?) -> Unit)? = null
private var beforeTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null
private var onTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = null
/**
* DSL中使用的函数,一般保持同名即可
*/
fun afterTextChanged(method: (Editable?) -> Unit) {
afterTextChanged = method
}
fun beforeTextChanged(method: (CharSequence?, Int, Int, Int) -> Unit) {
beforeTextChanged = method
}
fun onTextChanged(method: (CharSequence?, Int, Int, Int) -> Unit) {
onTextChanged = method
}
/**
* 实现原接口的函数
*/
override fun afterTextChanged(s: Editable?) {
afterTextChanged?.invoke(s)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
beforeTextChanged?.invoke(s, start, count, after)
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChanged?.invoke(s, start, before, count)
}
}
这个实现类由三个部分组成:
1. 原接口方法对应的 Kotlin 函数对象,函数对象的签名与对应的方法签名保持一致。
2. DSL 函数,函数名称、签名都与原接口的方法一一对应,用于接收 lambda 赋值给 Kotlin 函数对象。
3. 原接口方法的实现,每个接口方法的实现,都是对实现类中 Kotlin 函数对象的调用。
2.创建与原函数同名的扩展函数,函数参数为实现类扩展函数
fun TextView.addTextChangedListenerDsl(init: TextWatcherDslImpl.() -> Unit) {
val listener = TextWatcherDslImpl()
listener.init()
this.addTextChangedListener(listener)
}
扩展函数与原函数同名可以方便使用者调用,无需记忆其他函数名,如果担心混淆,可以在函数名后加上 Dsl 用以区分。该函数的参数是我们第一步创建的实现类的扩展函数,这是为了实现 DSL 语法。
3.使用
etString.addTextChangedListenerDsl {
afterTextChanged {
if (it.toString().length >= 4) {
KeyboardUtils.toggleSoftInput()
}
}
}
使用这种方式时,可以说相当之优雅,我们只需要调用我们需要实现的接口方法即可,不需要使用的接口方法默认空实现。
高阶函数
高阶函数方式比 DSL 方式更简单一点:
inline fun TextView.addTextChangedListenerClosure(
crossinline afterTextChanged: (Editable?) -> Unit = {},
crossinline beforeTextChanged: (CharSequence?, Int, Int, Int) -> Unit = { charSequence, start, count, after -> },
crossinline onTextChanged: (CharSequence?, Int, Int, Int) -> Unit = { charSequence, start, after, count -> }
) {
val listener = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
afterTextChanged.invoke(s)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
beforeTextChanged.invoke(s, start, count, after)
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextChanged.invoke(s, start, before, count)
}
}
this.addTextChangedListener(listener)
}
我们创建一个同名扩展函数,使用 Closure 尾缀作为区分,该函数的参数为与接口方法一一对应的 Kotlin 函数对象,并给其默认值赋值为 {} 即空实现,在函数体里通过 object 关键字构建匿名内部类实现对象,在其接口方法实现中调用与之一一对应的 Kotlin 函数对象。
使用方式上与普通的 Kotlin 高阶函数使用方式相同:
etString.addTextChangedListenerClosure(
afterTextChanged = {
if (it.toString().length >= 4) {
KeyboardUtils.toggleSoftInput()
}
},
)
tips:
上面示例的扩展函数中,我们使用了 inline 与 crossinline 两个关键字,这是 Koltin 特有的。inline 关键字通常用于修饰高阶函数,用于提升性能。crossinline 声明的 lambda 不允许局部返回,用于避免调用者错误的使用 return 导致函数中断。
提供一个示例代码,亲自尝试一下也许可以更好的理解:
@Test
fun testInline() {
testClosure {
return
}
}
private inline fun testClosure(test: (String) -> String ) {
println("step 1")
println(test("step test"))
println("step 2")
}
源从何来
在 Android 开发中 Spannable 实现富文本显示,也算是一个比较常见的使用场景,例如在登录页显示《隐私政策》、《服务协议》,通常这是一个有自定义颜色与点击事件的 Span,使用起来大致需要写如下代码:
private fun agreePrivate() {
val tv = findViewById<TextView>(R.id.tv_agree)
val builder = SpannableStringBuilder()
val text = "我已详细阅读并同意《隐私政策》"
builder.append(text)
//设置span点击事件
val clickableSpan = object :ClickableSpan(){
override fun onClick(widget: View) {
//do some thing
}
}
builder.setSpan(clickableSpan, 9, 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
//设置span无下划线
val noUnderlineSpan = NoUnderlineSpan()
builder.setSpan(noUnderlineSpan, 9, 15, Spanned.SPAN_MARK_MARK)
//设置span文字颜色
val foregroundColorSpan = ForegroundColorSpan(Color.parseColor("#0099FF"))
builder.setSpan(foregroundColorSpan, 9, 15, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
//设置可点击
tv.movementMethod = LinkMovementMethod.getInstance()
tv.setText(builder)
}
class NoUnderlineSpan : UnderlineSpan() {
override fun updateDrawState(ds: TextPaint) {
ds.color = ds.linkColor
ds.isUnderlineText = false
}
}
用起来还是比较麻烦的,就像上面的代码只是一个 span 就写了三个 setSpan,如果需要使用 Span 的地方比较多,这些代码看起来实在是不够优雅。有没有更优雅方式呢,答案就是 DSL,上面的代码最终通过 DSL 封装后如下:
tvTestDsl.buildSpannableString {
addText("我已详细阅读并同意")
addText("《隐私政策》"){
setColor("#0099FF")
onClick(false) {
//do some thing
}
}
}
他们的显示效果是完全一致的,无疑 DSL 的方式更加优雅,对于调用者而言也更加方便。
实现思路
当我有用 DSL 封装 Spannable 这个想法时,我首先写的是我应该如何去使用它,当时我在纸上胡乱的写下了上面的那段代码。
1. 它应该是 TextView的一个扩展函数。
2. 它的内部是 DSL 风格的代码。
3. 它的每段文字都有设置颜色 & 点击事件的函数。
所以就有了如下的两个接口与扩展函数:
interface DslSpannableStringBuilder {
//增加一段文字
fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null)
}
interface DslSpanBuilder {
//设置文字颜色
fun setColor(color: String)
//设置点击事件
fun onClick(useUnderLine: Boolean = true, onClick: (View) -> Unit)
}
//为 TextView 创建扩展函数,其参数为接口的扩展函数
fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) {
//具体实现类
val spanStringBuilderImpl = DslSpannableStringBuilderImpl()
spanStringBuilderImpl.init()
movementMethod = LinkMovementMethod.getInstance()
//通过实现类返回SpannableStringBuilder
text = spanStringBuilderImpl.build()
}
上一篇文章我们说了, 在 DSL 风格的函数中,其参数应当是某个接口(或者他的实现类)的扩展函数,这样我们相当于通过接口来限定了在 DSL 中可调用的函数。上一篇中使用的是实现类,本文中使用的是接口,原因很简单,上文是扩展原有接口变成 DSL 风格,本文是直接从无至有,实现的 DSL 风格。
实现相应接口
其实对于像我这样初次接触DSL 的新手而言,思路是最难的,有了接口,有了 DSL 层级,剩下的就是相对简单的实现了。直接看代码:
class DslSpannableStringBuilderImpl : DslSpannableStringBuilder {
private val builder = SpannableStringBuilder()
//记录上次添加文字后最后的索引值
var lastIndex: Int = 0
var isClickable = false
override fun addText(text: String, method: (DslSpanBuilder.() -> Unit)?) {
val start = lastIndex
builder.append(text)
lastIndex += text.length
val spanBuilder = DslSpanBuilderImpl()
method?.let { spanBuilder.it() }
spanBuilder.apply {
onClickSpan?.let {
builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
isClickable = true
}
if (!useUnderLine) {
val noUnderlineSpan = NoUnderlineSpan()
builder.setSpan(noUnderlineSpan, start, lastIndex, Spanned.SPAN_MARK_MARK)
}
foregroundColorSpan?.let {
builder.setSpan(it, start, lastIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
}
fun build(): SpannableStringBuilder {
return builder
}
}
class DslSpanBuilderImpl : DslSpanBuilder {
var foregroundColorSpan: ForegroundColorSpan? = null
var onClickSpan: ClickableSpan? = null
var useUnderLine = true
override fun setColor(color: String) {
foregroundColorSpan = ForegroundColorSpan(Color.parseColor(color))
}
override fun onClick(useUnderLine: Boolean, onClick: (View) -> Unit) {
onClickSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
onClick(widget)
}
}
this.useUnderLine = useUnderLine
}
}
class NoUnderlineSpan : UnderlineSpan() {
override fun updateDrawState(ds: TextPaint) {
ds.color = ds.linkColor
ds.isUnderlineText = false
}
}
想要使用 DSL 离不开接口与扩展函数,需要先创建想要在 DSL 中使用的函数的接口,然后声明函数参数为该接口的扩展函数。
如果 DSL 中存在像我这样的嵌套,那么就需要为这个嵌套再创建一个用于嵌套调用的接口(本文的嵌套是故意为之,使用单个接口传参也可以实现这样的效果)。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/sf9LHgBRutPkH7E-yFGfqQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。