动画代码太丑,用Kotlin DSL来拯救!

640?wx_fmt=jpeg


/   今日科技快讯   /


欧盟宣布对高通罚款2.42亿欧元(约合2.72亿美元)。这也是欧盟第二次对高通重罚。高通被指控在2009-2011年之间进行掠夺性定价,将英国手机软件制造商Icere挤出市场。目前已Icere被英伟达收购。


/   作者简介   /


你没有猜错,今天又到周五了,美好的周末即将开始。祝大家周末愉快!


本篇文章来自唐子玄的投稿,和大家分享了Kotlin进阶开发,封装了动画的扩展函数,希望对大家有所帮助!同时也感谢作者贡献的精彩文章。


唐子玄的博客地址:

https://juejin.im/user/5771d75d2e958a0078f97976


/   前言   /


最近在看《新说唱》,突然就想到了这个带韵脚的标题。希望你喜欢~。言归正传,Android构建动画的代码语法啰嗦,可读性差。若能构建一套可读性更强的接口就能提高动画的开发效率。本文尝试用 Kotlin 的 DSL 重写了整套构建动画的 API ,使得构建动画的代码量锐减,语义一目了然。另外,Android提供了反转动画的接口,但只有在API level 26 以上才能使用,本文尝试突破这个限制。


/   原生动画代码   /


假设需求如下:“缩放 textView 的同时平移 button ,然后拉长 imageView,动画结束后 toast 提示”。用系统原生接口构建如下:


 
 


啰嗦!而且乍一看不知道在做啥,只能一行一行的细看,待看完整段代码后,才能在脑海中构建出整个需求的样子。


但逐行看也很费劲,不信就试着从第一行开始读:


 
 


原生 API 将“缩放 textView ”这短短的一句话拆分成一个个零散的逻辑单元,并以一种不符合自然语言的顺序排列,所以不得不读完所有单元,才能拼凑出整个语义。


如果有一种更符合自然语言的 API,就能更省力地构建动画,更快速地理解代码。


/   用 Kotlin 预定义扩展函数   /


 
 


使用 apply() 和 let() 避免了重复对象名,缩减了代码量。更重要的是 Kotlin 的代码有一种结构,这种结构让代码更符合自然语言。试着读一下:


 
 


虽然在语义上已经比较清晰,但结构还是显得啰嗦,此起彼伏的缩进看着有点乱。


/   用 DSL 进一步简化代码   /


如果使用自定义的 DSL,就可以做的更好!直接上代码


 
 


一目了然的语义和清晰的结构,就好像是一篇英语文章。


这里运用了多个 Kotlin 语言特性,包括扩展函数、带接收者的 lambda、顶层函数、抽象属性、属性访问器、中缀表示法、函数类型变量、apply()、also()、let()。


逐个讲解 Kotlin 语法知识点后,再分析整套 DSL 的实现方案。


/   带接收者的 lambda   /


代码中animSet()、objectAnim()、anim()都是带有一个参数的函数,这个参数是带接受者的 lambda。animSet()代码如下:


 
 


它是一个顶层函数,定义在类体外,即它不隶属于任何类。这样定义的目的是可以在任何地方调用animSet()来构造动画集。


它的参数类型是一个带接收者的 lambda AnimSet.() -> Unit,接收者是AnimSet类,它表示动画集(类似AnimatorSet)。这样定义的好处是,可以在传入animSet()的 lambda 中访问AnimSet中的非私有成员,若把构建单个动画的方法objectAnim()和anim()定义在AnimSet()中,就可以像写 HTML 一样使用结构化的语法构建动画。所以参数creation描述的是在动画集中构建动画的过程。


animSet()在函数体中,创建了一个动画集AnimSet实例,并将构建子动画的方法应用在此实例上。


关于带接收者的lambda和apply()、also()、let()更详细的讲解可以查看:

https://juejin.im/post/5d061caef265da1b8f1ac00a


构建动画的方法定义如下:


 
 


这两个函数和构建动画集的函数非常相似,都使用了带接收者的lambda作为参数,它定义了如何构建动画。ValueAnim和ObjectAnim分别对应于原生的ValueAnimator和ObjectAnimator。它们有一个共同的基类Anim对应于原生的Animator:


 
 


/   抽象属性   /


动画基类Anim是抽象类,因为animator属性和reverseValues()方法是抽象的。


animator属性对于ValueAnim来说是ValueAnimator实例,对于ObjectAnim来说是ObjectAnimator实例:


 
 


关于抽象属性更详细的介绍可以看这里

https://juejin.im/post/5d1d99e6f265da1b8b2b7be5


反转动画的算法对于ValueAnim和ObjectAnim有所不同,将反转算法作为抽象函数放在基类的好处时,在动画集AnimSet中可以无需关心算法细节而是直接调用reverseValues()实现反转动画:


 
 


反转动画的算法会在下面分析,先来看下一个用到的 Kotlin 特性。


/   属性访问器   /


 
 


在类属性的下面实现set()和get()方法,这样的语法叫属性访问器。当定义了访问器的属性被赋值时,set()函数会执行,属性被读取时,get()函数会执行,所以访问器定义了属性值的读写算法


访问器在这里的好处是提供了默认值并隐藏了赋值细节,如果在构建动画时没有提供 duration ,则默认为300ms,为Anim实例设置 duration 时,其实就是调用了原生的ValueAnimator.setDuration()方法,属性访问器隐藏了这一细节,使得可以使用如下这样简洁的语法构建动画:


 
 


/   函数类型   /


构建单个动画进行了4个属性赋值操作。其中action属性表示“如何将动画值的序列应用到 View 上”:


 
 


Kotlin 中可以将函数保存在一个变量中,这种变量的类型叫做函数类型,action的类型就是函数类型,用((Any) -> Unit)?描述,意思是这个函数接收一个Any类型的参数但什么也不返回。


这个属性也用到了访问器,当action被赋值时就会为原生动画设置AnimatorUpdateListener,并将属性值变化的序列作为参数传递给存放在action中的 lambda,这样在构建动画时,就可以用一个简单的 lambda 定义做什么样的动画,比如下面就是在做向右平移动画:


 
 


其中的values属性表示动画值序列:


 
 


values属性也使用了访问器,将根据类型调用ValueAnimator.setXXXValue()细节隐藏。


/   中缀表示法   /


Kotlin 中,当函数调用只有一个参数时,可以省略包括参数的(),以让代码更简洁,更符合自然语言,这种表示法叫中缀表示法。上述代码中用于连接多个动画的before()函数就使用了中缀表示法:


 
 


中缀表示的方法必须以关键词infix开头,且函数只能有一个参数。同时这也是一个Anim类的扩展函数。这个函数的调用者、参数、返回值都是一个Anim实例。所以可以像a1 with a2 with a3这样将多个Anim连接起来。(连接动画的原理会在下面分析。)


/   实现方案   /


将从“如何构建Object动画”、“如何反转动画”、“如何连接动画”这三个方面来分析整套 DSL 的实现方法,关于 DSL 更详细的解释可以查看:

https://juejin.im/post/5d1350eb5188251a362233aa


构建ObjectAnim


整套 DSL 并不是实现一个全新的动画框架。而是将原生动画提供的接口通过 DSL 封装成结构化的 API 以减少代码量并增加可读性。


ObjectAnim中定义了属性用于存放动画值序列:


 
 


当调用如下代码时,属性被赋值:


 
 


因为并不知道,每个动画会为哪些属性赋值,所以不能调用ObjectAnimator.ofPropertyValuesHolder(textView, scaleX, scaleY);来构建ObjectAnimator对象。而只能用一个数组存放所有被赋值的属性,并且通过遍历数组调用ObjectAnimator.setValues()异步构建ObjectAnimator对象:


 
 


反转动画


反转动画的思路是:“将动画值序列倒序并重新播放动画”。动画基类AnimSet中定义了反转算法的抽象方法:


 
 


ValueAnimator重写如下:


 
 


AnimSet提供反转动画对的外接口:


 
 


ObjectAnim的反转算法略有不同:


 
 


连接动画


DSL 中的连接方案抛弃了AnimatorSet.playTogether()和playSequentially(),而是采用更加灵活的AnimtorSet.Builder方式。


被加入到AnimatorSet的Animator会被保存在Node这个结构中:


 
 


Animator之间的播放顺序关系通过三个列表维护。兄弟列表中的动画会和自己同时播放,孩子列表会晚于自己播放,父亲列表会早于自己播放。


为了向这三个列表填值,系统定义了Builder类:


 
 


同时播放a1,a2,a3动画,只需要这样调用 java API:


 
 


此时结点间只有一个层级,即a1在外层,a2和a3存放在a1的兄弟列表中。将上述 java 代码转换成 Kotlin 的中缀表示法如下:


 
 


因为同时播放的动画只有一个层级,所以调用链中,只需要第一个动画调用一次play()即可。为Anim增加了builder属性以判断当前动画是否调用过play()来创建结点。


相比之下,顺序播放的代码层级就变多了,如果要先播放a1,再播放a2,最后播放a3,java api 如下:


 
 


这个结构有点像树,后续结点是之前结点的孩子。对应的中缀表达式定义如下:



每次都为当前动画调用play()创建Builder并将后续动画存入孩子列表。


/   最后   /


talk is cheap, show me the code

https://github.com/wisdomtl/Kotlin-Animation-DSL


代码会持续更新,欢迎提出问题。


推荐阅读:

测试也是Android开发的重要部分,单元测试和UI测试上手实践

Kotlin协程入门学习,看这一篇就足够了

分享一个能让你的代码变得更整洁的技巧


欢迎关注我的公众号

学习技术或投稿


640.png?


640?wx_fmt=jpeg

长按上图,识别图中二维码即可关注


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值