Kotlin 风格,应该这样写drawable !

时间:2022-09-24 19:57:29

前言

通常我们在res/drawable下面自定义shape和selector来满足一些UI的设计,但是由于xml最终转换为drawable需要经过IO或反射创建,会有一些性能损耗,另外随着项目的增大和模块化等,很多通用的样式并不能快速复用,需要合理的项目资源管理规范才能实施。那么通过代码直接创建这些drawable,可以在一定程度上降低这些副作用。本篇介绍用kotlin DSL简洁的语法特性来实现常见的drawable。

Kotlin 风格,应该这样写drawable !

代码对应效果预览

Kotlin 风格,应该这样写drawable !

Kotlin 风格,应该这样写drawable !

Kotlin 风格,应该这样写drawable !

Kotlin 风格,应该这样写drawable !

集成和使用

在项目级的build.gradle文件种添加仓库Jitpack:

  1. allprojects { 
  2.     repositories { 
  3.         ... 
  4.         maven { url 'https://jitpack.io' } 
  5.     } 

添加依赖

  1. dependencies {   
  2.  implementation 'com.github.forJrking:DrawableDsl:0.0.3’ 

抛弃xml创建方式示例(其他参见demo)

  1. // infix用法用于去掉括号更加简洁,详细后面说明 
  2. image src shapeDrawable { 
  3.     //指定shape样式 
  4.     shape(ShapeBuilder.Shape.RECTANGLE) 
  5.     //圆角,支持4个角单独设置 
  6.     corner(20f) 
  7.     //solid 颜色 
  8.     solid("#ABE2E3"
  9.     //stroke 颜色,边框dp,虚线设置 
  10.     stroke(R.color.white, 2f, 5f, 8f) 
  11. //按钮点击样式 
  12. btn.background = selectorDrawable { 
  13.     //默认样式 
  14.     normal = shapeDrawable { 
  15.         corner(20f) 
  16.         gradient(90, R.color.F97794, R.color.C623AA2) 
  17.     } 
  18.     //点击效果 
  19.     pressed = shapeDrawable { 
  20.         corner(20f) 
  21.         solid("#84232323"
  22.     } 

实现思路

xml如何转换成drawable

xml变成drawable,通过android.graphics.drawable.DrawableInflater这个类来IO解析标签创建,然后通过解析标签再设置属性:

  1. //标签创建 
  2. private Drawable inflateFromTag(@NonNull String name) { 
  3.     switch (name) { 
  4.         case "selector"
  5.             return new StateListDrawable(); 
  6.         case "level-list"
  7.             return new LevelListDrawable(); 
  8.         case "layer-list"
  9.             return new LayerDrawable(); 
  10.         .... 
  11.         case "color"
  12.             return new ColorDrawable(); 
  13.         case "shape"
  14.             return new GradientDrawable(); 
  15.         case "vector"
  16.             return new VectorDrawable(); 
  17.         ... 
  18.     } 
  19. //反射创建 
  20. private Drawable inflateFromClass(@NonNull String className) { 
  21.     try { 
  22.         Constructor<? extends Drawable> constructor; 
  23.         synchronized (CONSTRUCTOR_MAP) { 
  24.             constructor = CONSTRUCTOR_MAP.get(className); 
  25.             if (constructor == null) { 
  26.                 final Class<? extends Drawable> clazz = mClassLoader.loadClass(className).asSubclass(Drawable.class); 
  27.                 constructor = clazz.getConstructor(); 
  28.                 CONSTRUCTOR_MAP.put(className, constructor); 
  29.             } 
  30.         } 
  31.         return constructor.newInstance(); 
  32.     } catch (NoSuchMethodException e) { 
  33.     ... 

代码实现

由于创建shape等需要设置各种属性来构建,比较符合build设计模式,那我们首先封装build模式的shapeBuilder,这样做虽然代码比起直接使用apply{}要多,但是可以让纯java项目用起来很舒服,其他实现请查看源码:

  1. class ShapeBuilder : DrawableBuilder { 
  2.     private var mRadius = 0f 
  3.     private var mWidth = 0f 
  4.     private var mHeight = 0f 
  5.     ... 
  6.     private var mShape = GradientDrawable.RECTANGLE 
  7.     private var mSolidColor = 0 
  8.  
  9.     /**分别设置四个角的圆角*/ 
  10.     fun corner(leftTop: Float,rightTop: Float,leftBottom: Float,rightBottom: Float): ShapeBuilder { 
  11.         ....if(dp)dp2px(leftTop) else leftTop 
  12.         return this 
  13.     } 
  14.  
  15.     fun solid(@ColorRes colorId: Int): ShapeBuilder { 
  16.         mSolidColor = ContextCompat.getColor(context, colorId) 
  17.         return this 
  18.     } 
  19.     // 省略其他参数设置方法 详细代码查看源码 
  20.     override fun build(): Drawable { 
  21.         val gradientDrawable = GradientDrawable() 
  22.         gradientDrawable = GradientDrawable() 
  23.         gradientDrawable.setColor(mSolidColor) 
  24.         gradientDrawable.shape = mShape 
  25.         ....其他参数设置 
  26.         return gradientDrawable 
  27.     }     

把build模式转换为dsl

理论上所有的build模式都可以轻松转换为dsl写法:

  1. inline fun shapeDrawable(builder: ShapeBuilder.() -> Unit): Drawable { 
  2.     return ShapeBuilder().also(builder).build() 
  3. //使用方法  
  4. val drawable = shapeDrawable{ 
  5.     ... 

备注:dsl用法参见juejin.cn/post/695318… 中dsl小节

函数去括号

通过上面封装已经实现了dsl的写法,通常setBackground可以通过setter简化,但是我发现由于有些api设计还需要加括号,这样不太kotlin:

  1. //容易阅读 
  2. iv1.background = shapeDrawable { 
  3.     shape(ShapeBuilder.Shape.RECTANGLE) 
  4.     solid("#ABE2E3"
  5. //多了括号看起来不舒服 
  6. iv2.setImageDrawable(shapeDrawable { 
  7.     solid("#84232323"
  8. }) 

怎么去掉括号呢?有2种方式infix函数(中缀表达)和property setter

infix函数特点和规范:

  • Kotlin允许在不使用括号和点号的情况下调用函数
  • 必须只有一个参数
  • 必须是成员函数或扩展函数
  • 不支持可变参数和带默认值参数
  1. /**为所有ImageView添加扩展infix函数 来去掉括号*/ 
  2. infix fun ImageView.src(drawable: Drawable?) { 
  3.     this.setImageDrawable(drawable) 
  4. //使用如下 
  5. iv2 src shapeDrawable { 
  6.     shape(ShapeBuilder.Shape.OVAL) 
  7.     solid("#E3ABC2"

当然了代码是用来阅读的。个人认为如果我们大量使用infix函数,阅读困难会大大增加,所以建议函数命名必须可以直击函数功能,而且函数功能简单且单一。

property setter方式,主要使用kotlin可以简化setter为 变量 =来去括号:

  1. /**扩展变量*/ 
  2. var ImageView.src: Drawable 
  3.     get() = drawable 
  4.     set(value) { 
  5.         this.setImageDrawable(value) 
  6.     } 
  7. //使用如下    
  8. iv2.src = shapeDrawable { 
  9.     shape(ShapeBuilder.Shape.OVAL) 
  10.     solid("#E3ABC2"
  11. }  

感谢@叮凛凛 指点,欢迎大家讨论一起学习,共同进步。

优缺点

优点:

  • 代码直接创建比起xml方式可以提升性能
  • dsl方式比起build模式和调用方法设置更加简洁符合kotlin风格
  • 通过合适的代码管理可以复用这些代码,比xml管理方便

缺点:

  • 没有as的预览功能,只有通过上机观测
  • api还没有覆盖所有drawable属性(例如shape = ring等)

后语

上面把的DrawableDsl基础用法介绍完了,欢迎大家使用,欢迎提Issues,记得给个star哦。Github链接:https://github.com/forJrking/DrawableDsl

原文地址:https://juejin.cn/post/6953472037012635655